mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-15 21:02:10 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0555186655 |
@@ -47,9 +47,66 @@ export function fillDataGapsAndRoundCaps(
|
||||
)
|
||||
).sort((a, b) => a - b);
|
||||
|
||||
// make sure all datasets have the same buckets
|
||||
// otherwise the chart will render incorrectly in some cases
|
||||
buckets.forEach((bucket, index) => {
|
||||
// Phase 1: align every dataset to the shared buckets, inserting zero-valued
|
||||
// gap fillers. The original implementation spliced gaps in-place while
|
||||
// iterating bucket-by-bucket, which shifts the whole tail of the array on
|
||||
// every gap (O(points * gaps) in the worst case). Rebuilding each dataset in
|
||||
// a single forward merge against the bucket axis yields the byte-identical
|
||||
// aligned array without the per-gap shifting cost.
|
||||
const bucketCount = buckets.length;
|
||||
for (let i = datasets.length - 1; i >= 0; i--) {
|
||||
const data = datasets[i].data;
|
||||
if (!data || data.length === 0) {
|
||||
continue;
|
||||
}
|
||||
const dataLength = data.length;
|
||||
const aligned: typeof data = [];
|
||||
let srcIdx = 0;
|
||||
// The original loop runs once per bucket index and reads data[index],
|
||||
// which (thanks to the in-place splices) always points at the next
|
||||
// unconsumed source element. A defined element with a non-matching x
|
||||
// inserts a gap and keeps the element for the next index; a matching or
|
||||
// otherwise-defined element is kept and the pointer advances; once the
|
||||
// source is exhausted, no further gaps are added.
|
||||
for (let index = 0; index < bucketCount; index++) {
|
||||
if (srcIdx >= dataLength) {
|
||||
break;
|
||||
}
|
||||
const dataPoint = data[srcIdx];
|
||||
const item: any =
|
||||
dataPoint && typeof dataPoint === "object" && "value" in dataPoint
|
||||
? dataPoint
|
||||
: { value: dataPoint };
|
||||
const x = item.value?.[0];
|
||||
if (x === undefined) {
|
||||
// Malformed element: left untouched in place, pointer advances.
|
||||
aligned.push(dataPoint);
|
||||
srcIdx++;
|
||||
} else if (Number(x) !== buckets[index]) {
|
||||
aligned.push({
|
||||
value: [buckets[index], 0],
|
||||
itemStyle: {
|
||||
borderWidth: 0,
|
||||
},
|
||||
} as (typeof data)[number]);
|
||||
} else {
|
||||
aligned.push(dataPoint);
|
||||
srcIdx++;
|
||||
}
|
||||
}
|
||||
// Trailing source elements past the last consumed bucket index are never
|
||||
// reached by the original loop and stay in place unchanged.
|
||||
for (; srcIdx < dataLength; srcIdx++) {
|
||||
aligned.push(data[srcIdx]);
|
||||
}
|
||||
datasets[i].data = aligned;
|
||||
}
|
||||
|
||||
// Phase 2: per bucket, mark only the uppermost positive and lowermost
|
||||
// negative bar of each stack with a rounded cap, and strip the border from
|
||||
// zero values. Datasets are now aligned, so data[index] always corresponds
|
||||
// to buckets[index] (or is undefined past the dataset's last point).
|
||||
for (let index = 0; index < bucketCount; index++) {
|
||||
const capRounded = {};
|
||||
const capRoundedNegative = {};
|
||||
for (let i = datasets.length - 1; i >= 0; i--) {
|
||||
@@ -63,14 +120,7 @@ export function fillDataGapsAndRoundCaps(
|
||||
if (x === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (Number(x) !== bucket) {
|
||||
datasets[i].data?.splice(index, 0, {
|
||||
value: [bucket, 0],
|
||||
itemStyle: {
|
||||
borderWidth: 0,
|
||||
},
|
||||
});
|
||||
} else if (item.value?.[1] === 0) {
|
||||
if (item.value?.[1] === 0) {
|
||||
// remove the border for zero values or it will be rendered
|
||||
datasets[i].data![index] = {
|
||||
...item,
|
||||
@@ -99,5 +149,5 @@ export function fillDataGapsAndRoundCaps(
|
||||
capRoundedNegative[stack] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,6 +441,166 @@ describe("fillDataGapsAndRoundCaps", () => {
|
||||
|
||||
assert.equal(datasets[0].data!.length, 0);
|
||||
});
|
||||
|
||||
it("fills several consecutive gaps for one dataset", () => {
|
||||
const datasets: BarSeriesOption[] = [
|
||||
{
|
||||
type: "bar",
|
||||
stack: "a",
|
||||
data: [
|
||||
[1000, 1],
|
||||
[4000, 4],
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "bar",
|
||||
stack: "a",
|
||||
data: [[2000, 2]],
|
||||
},
|
||||
{
|
||||
type: "bar",
|
||||
stack: "a",
|
||||
data: [
|
||||
[1000, 1],
|
||||
[2000, 2],
|
||||
[3000, 3],
|
||||
[4000, 4],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
fillDataGapsAndRoundCaps(datasets);
|
||||
|
||||
// First dataset is aligned across all four buckets with 2 zero gaps.
|
||||
assert.equal(datasets[0].data!.length, 4);
|
||||
assert.equal(getBarItem(datasets[0], 0).value[0], 1000);
|
||||
assert.equal(getBarItem(datasets[0], 1).value[0], 2000);
|
||||
assert.equal(getBarItem(datasets[0], 1).value[1], 0);
|
||||
assert.equal(getBarItem(datasets[0], 1).itemStyle.borderWidth, 0);
|
||||
assert.equal(getBarItem(datasets[0], 2).value[0], 3000);
|
||||
assert.equal(getBarItem(datasets[0], 2).value[1], 0);
|
||||
assert.equal(getBarItem(datasets[0], 3).value[0], 4000);
|
||||
|
||||
// Second dataset only has a point at bucket 2000; the leading 1000 bucket
|
||||
// is filled and no trailing buckets are added past its last point.
|
||||
assert.equal(datasets[1].data!.length, 2);
|
||||
assert.equal(getBarItem(datasets[1], 0).value[0], 1000);
|
||||
assert.equal(getBarItem(datasets[1], 0).value[1], 0);
|
||||
assert.equal(getBarItem(datasets[1], 1).value[0], 2000);
|
||||
});
|
||||
|
||||
it("does not grow a dataset past its last bucket (duplicate x)", () => {
|
||||
// Only one unique bucket exists, so the second duplicate point is never
|
||||
// reached by the alignment loop and is left in place untouched.
|
||||
const datasets: BarSeriesOption[] = [
|
||||
{
|
||||
type: "bar",
|
||||
stack: "a",
|
||||
data: [
|
||||
[1000, 1],
|
||||
[1000, 2],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
fillDataGapsAndRoundCaps(datasets);
|
||||
|
||||
assert.equal(datasets[0].data!.length, 2);
|
||||
const first = getBarItem(datasets[0], 0);
|
||||
assert.equal(first.value[0], 1000);
|
||||
assert.deepEqual(first.itemStyle.borderRadius, [4, 4, 0, 0]);
|
||||
// The duplicate stays as a raw tuple, untouched.
|
||||
assert.deepEqual(datasets[0].data![1], [1000, 2]);
|
||||
});
|
||||
|
||||
it("rounds only the first positive and first negative scanned from the top per stack", () => {
|
||||
// Datasets are scanned from last to first, so the first positive and first
|
||||
// negative encountered from the top of the stack receive the rounded caps;
|
||||
// the remaining bars in the same stack are left untouched.
|
||||
const datasets: BarSeriesOption[] = [
|
||||
{
|
||||
type: "bar",
|
||||
stack: "a",
|
||||
data: [[1000, -10]],
|
||||
},
|
||||
{
|
||||
type: "bar",
|
||||
stack: "a",
|
||||
data: [[1000, -5]],
|
||||
},
|
||||
{
|
||||
type: "bar",
|
||||
stack: "a",
|
||||
data: [[1000, 5]],
|
||||
},
|
||||
{
|
||||
type: "bar",
|
||||
stack: "a",
|
||||
data: [[1000, 10]],
|
||||
},
|
||||
];
|
||||
|
||||
fillDataGapsAndRoundCaps(datasets);
|
||||
|
||||
// First positive scanned from the top (last dataset) rounds the top.
|
||||
assert.deepEqual(
|
||||
getBarItem(datasets[3], 0).itemStyle.borderRadius,
|
||||
[4, 4, 0, 0]
|
||||
);
|
||||
// Lower positive is left untouched.
|
||||
assert.equal(getBarItem(datasets[2], 0).itemStyle?.borderRadius, undefined);
|
||||
// First negative scanned from the top rounds the bottom.
|
||||
assert.deepEqual(
|
||||
getBarItem(datasets[1], 0).itemStyle.borderRadius,
|
||||
[0, 0, 4, 4]
|
||||
);
|
||||
// Lower negative is left untouched.
|
||||
assert.equal(getBarItem(datasets[0], 0).itemStyle?.borderRadius, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fillDataGapsAndRoundCaps non-stacked", () => {
|
||||
it("rounds the top of every series and bottom of negative points", () => {
|
||||
const datasets: BarSeriesOption[] = [
|
||||
{
|
||||
type: "bar",
|
||||
data: [
|
||||
[1000, 5],
|
||||
[2000, -3],
|
||||
[3000, 0],
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
fillDataGapsAndRoundCaps(datasets, false);
|
||||
|
||||
// The series gets an overall top border radius.
|
||||
assert.deepEqual(datasets[0].itemStyle!.borderRadius, [4, 4, 0, 0]);
|
||||
// Negative point is overridden to a bottom border radius.
|
||||
const negative = getBarItem(datasets[0], 1);
|
||||
assert.deepEqual(negative.itemStyle.borderRadius, [0, 0, 4, 4]);
|
||||
// Positive and zero points stay as raw tuples (no per-point override).
|
||||
assert.deepEqual(datasets[0].data![0], [1000, 5]);
|
||||
assert.deepEqual(datasets[0].data![2], [3000, 0]);
|
||||
});
|
||||
|
||||
it("preserves existing series itemStyle and per-point style", () => {
|
||||
const datasets: BarSeriesOption[] = [
|
||||
{
|
||||
type: "bar",
|
||||
itemStyle: { color: "red" },
|
||||
data: [{ value: [1000, -2], itemStyle: { opacity: 0.5 } }],
|
||||
},
|
||||
];
|
||||
|
||||
fillDataGapsAndRoundCaps(datasets, false);
|
||||
|
||||
assert.equal(datasets[0].itemStyle!.color, "red");
|
||||
assert.deepEqual(datasets[0].itemStyle!.borderRadius, [4, 4, 0, 0]);
|
||||
const point = getBarItem(datasets[0], 0);
|
||||
assert.equal(point.itemStyle.opacity, 0.5);
|
||||
assert.deepEqual(point.itemStyle.borderRadius, [0, 0, 4, 4]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCompareTransform", () => {
|
||||
|
||||
Reference in New Issue
Block a user