From b4bdac8204c66bd1972858378ea44a927de9c023 Mon Sep 17 00:00:00 2001 From: Archie Wood <58074498+archiewood@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:18:30 -0500 Subject: [PATCH 1/2] fix: stack time axes --- src/data/SeriesData.ts | 23 +- src/data/helper/dataStackHelper.ts | 4 +- test/ut/spec/data/dataStack.test.ts | 332 ++++++++++++++++++++++++++++ 3 files changed, 352 insertions(+), 7 deletions(-) create mode 100644 test/ut/spec/data/dataStack.test.ts diff --git a/src/data/SeriesData.ts b/src/data/SeriesData.ts index 472d3b3c7e..0a21f6ec60 100644 --- a/src/data/SeriesData.ts +++ b/src/data/SeriesData.ts @@ -17,7 +17,7 @@ * under the License. */ -/* global Int32Array */ +/* global Int32Array, Map */ import * as zrUtil from 'zrender/src/core/util'; @@ -225,7 +225,7 @@ class SeriesData< private _dimSummary: DimensionSummary; - private _invertedIndicesMap: Record>; + private _invertedIndicesMap: Record | Map>; private _calculationInfo: DataCalculationInfo = {} as DataCalculationInfo; @@ -861,7 +861,7 @@ class SeriesData< * Only support the dimension which inverted index created. * Do not support other cases until required. * @param dim concrete dim - * @param value ordinal index + * @param value ordinal index or time value * @return rawIndex */ rawIndexOf(dim: SeriesDimensionName, value: OrdinalNumber): number { @@ -871,7 +871,13 @@ class SeriesData< throw new Error('Do not supported yet'); } } - const rawIndex = invertedIndices && invertedIndices[value]; + let rawIndex: number | undefined; + if (invertedIndices instanceof Map) { + rawIndex = invertedIndices.get(value); + } + else { + rawIndex = invertedIndices && invertedIndices[value]; + } if (rawIndex == null || isNaN(rawIndex)) { return INDEX_NOT_FOUND; } @@ -1393,7 +1399,6 @@ class SeriesData< const invertedIndicesMap = data._invertedIndicesMap; zrUtil.each(invertedIndicesMap, function (invertedIndices, dim) { const dimInfo = data._dimInfos[dim]; - // Currently, only dimensions that has ordinalMeta can create inverted indices. const ordinalMeta = dimInfo.ordinalMeta; const store = data._store; if (ordinalMeta) { @@ -1410,6 +1415,14 @@ class SeriesData< invertedIndices[store.get(dimInfo.storeDimIndex, i) as number] = i; } } + else if (dimInfo.type === 'time') { + const timeInvertedIndices = invertedIndicesMap[dim] = new Map(); + for (let i = 0; i < store.count(); i++) { + const timeValue = store.get(dimInfo.storeDimIndex, i) as number; + // Only support the case that all values are distinct. + timeInvertedIndices.set(timeValue, i); + } + } }); }; diff --git a/src/data/helper/dataStackHelper.ts b/src/data/helper/dataStackHelper.ts index 549820c912..038a3eb534 100644 --- a/src/data/helper/dataStackHelper.ts +++ b/src/data/helper/dataStackHelper.ts @@ -102,8 +102,8 @@ export function enableDataStack( } if (mayStack && !dimensionInfo.isExtraCoord) { - // Find the first ordinal dimension as the stackedByDimInfo. - if (!byIndex && !stackedByDimInfo && dimensionInfo.ordinalMeta) { + // Find the first ordinal or time dimension as the stackedByDimInfo. + if (!byIndex && !stackedByDimInfo && (dimensionInfo.ordinalMeta || dimensionInfo.type === 'time')) { stackedByDimInfo = dimensionInfo; } // Find the first stackable dimension as the stackedDimInfo. diff --git a/test/ut/spec/data/dataStack.test.ts b/test/ut/spec/data/dataStack.test.ts new file mode 100644 index 0000000000..1a1cc88f41 --- /dev/null +++ b/test/ut/spec/data/dataStack.test.ts @@ -0,0 +1,332 @@ +/* +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, +* software distributed under the License is distributed on an +* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +* KIND, either express or implied. See the License for the +* specific language governing permissions and limitations +* under the License. +*/ + +import { createChart, removeChart, getECModel } from '../../core/utHelper'; +import { EChartsType } from '@/src/echarts.all'; + +describe('dataStack', function () { + + let chart: EChartsType; + + beforeEach(function () { + chart = createChart(); + }); + + afterEach(function () { + removeChart(chart); + }); + + describe('time axis stacking', function () { + + it('stack_by_time_value_not_array_index', function () { + chart.setOption({ + xAxis: { type: 'time' }, + yAxis: { type: 'value' }, + series: [ + { + name: 'A', + type: 'bar', + data: [['2025-01-01', 4], ['2025-02-01', 8]], + stack: 'stack1' + }, + { + name: 'B', + type: 'bar', + data: [['2025-02-01', 3], ['2025-01-01', 9]], + stack: 'stack1' + } + ] + }); + + const dataA = getECModel(chart).getSeriesByIndex(0).getData(); + const dataB = getECModel(chart).getSeriesByIndex(1).getData(); + + const stackResultDimA = dataA.getCalculationInfo('stackResultDimension'); + expect(dataA.get(stackResultDimA, 0)).toEqual(4); + expect(dataA.get(stackResultDimA, 1)).toEqual(8); + + const stackResultDimB = dataB.getCalculationInfo('stackResultDimension'); + const stackedOverDimB = dataB.getCalculationInfo('stackedOverDimension'); + + expect(dataB.get(stackResultDimB, 0)).toEqual(11); + expect(dataB.get(stackedOverDimB, 0)).toEqual(8); + expect(dataB.get(stackResultDimB, 1)).toEqual(13); + expect(dataB.get(stackedOverDimB, 1)).toEqual(4); + + expect(dataB.get(stackResultDimB, 0)).not.toEqual(7); + expect(dataB.get(stackResultDimB, 1)).not.toEqual(17); + }); + + it('time_axis_uses_value_based_stacking', function () { + chart.setOption({ + xAxis: { type: 'time' }, + yAxis: { type: 'value' }, + series: [{ + name: 'A', + type: 'bar', + data: [['2025-01-01', 4], ['2025-02-01', 8]], + stack: 'stack1' + }] + }); + + const data = getECModel(chart).getSeriesByIndex(0).getData(); + + expect(data.getDimensionInfo('x')?.type).toBe('time'); + expect(data.getCalculationInfo('stackedDimension')).toBeTruthy(); + expect(data.getCalculationInfo('stackedByDimension')).toBeTruthy(); + expect(data.getCalculationInfo('isStackedByIndex')).not.toBe(true); + }); + + it('category_axis_stacking', function () { + chart.setOption({ + xAxis: { type: 'category', data: ['Jan', 'Feb'] }, + yAxis: { type: 'value' }, + series: [ + { name: 'A', type: 'bar', data: [4, 8], stack: 'stack1' }, + { name: 'B', type: 'bar', data: [3, 9], stack: 'stack1' } + ] + }); + + const dataB = getECModel(chart).getSeriesByIndex(1).getData(); + const stackResultDimB = dataB.getCalculationInfo('stackResultDimension'); + const stackedOverDimB = dataB.getCalculationInfo('stackedOverDimension'); + + expect(dataB.get(stackResultDimB, 0)).toEqual(7); + expect(dataB.get(stackedOverDimB, 0)).toEqual(4); + expect(dataB.get(stackResultDimB, 1)).toEqual(17); + expect(dataB.get(stackedOverDimB, 1)).toEqual(8); + }); + + it('undefined_value_in_first_series', function () { + chart.setOption({ + xAxis: { type: 'time' }, + yAxis: { type: 'value' }, + series: [ + { + name: 'A', + type: 'bar', + data: [['2025-01-01', undefined], ['2025-02-01', 8]], + stack: 'stack1' + }, + { + name: 'B', + type: 'bar', + data: [['2025-01-01', 5], ['2025-02-01', 3]], + stack: 'stack1' + } + ] + }); + + const dataA = getECModel(chart).getSeriesByIndex(0).getData(); + const dataB = getECModel(chart).getSeriesByIndex(1).getData(); + + const stackResultDimA = dataA.getCalculationInfo('stackResultDimension'); + const stackResultDimB = dataB.getCalculationInfo('stackResultDimension'); + const stackedOverDimB = dataB.getCalculationInfo('stackedOverDimension'); + + expect(dataA.get(stackResultDimA, 0)).toBeNaN(); + expect(dataA.get(stackResultDimA, 1)).toEqual(8); + + expect(dataB.get(stackResultDimB, 0)).toEqual(5); + expect(dataB.get(stackedOverDimB, 0)).toBeNaN(); + expect(dataB.get(stackResultDimB, 1)).toEqual(11); + expect(dataB.get(stackedOverDimB, 1)).toEqual(8); + }); + + it('null_value_in_first_series', function () { + chart.setOption({ + xAxis: { type: 'time' }, + yAxis: { type: 'value' }, + series: [ + { + name: 'A', + type: 'bar', + data: [['2025-01-01', null], ['2025-02-01', 8]], + stack: 'stack1' + }, + { + name: 'B', + type: 'bar', + data: [['2025-01-01', 5], ['2025-02-01', 3]], + stack: 'stack1' + } + ] + }); + + const dataA = getECModel(chart).getSeriesByIndex(0).getData(); + const dataB = getECModel(chart).getSeriesByIndex(1).getData(); + + const stackResultDimA = dataA.getCalculationInfo('stackResultDimension'); + const stackResultDimB = dataB.getCalculationInfo('stackResultDimension'); + const stackedOverDimB = dataB.getCalculationInfo('stackedOverDimension'); + + expect(dataA.get(stackResultDimA, 0)).toBeNaN(); + expect(dataA.get(stackResultDimA, 1)).toEqual(8); + + expect(dataB.get(stackResultDimB, 0)).toEqual(5); + expect(dataB.get(stackedOverDimB, 0)).toBeNaN(); + expect(dataB.get(stackResultDimB, 1)).toEqual(11); + expect(dataB.get(stackedOverDimB, 1)).toEqual(8); + }); + + it('many_datapoints_100', function () { + const seriesDataA: [string, number][] = []; + const seriesDataB: [string, number][] = []; + + for (let i = 0; i < 100; i++) { + const dateStr = new Date(2025, 0, i + 1).toISOString().split('T')[0]; + seriesDataA.push([dateStr, i + 1]); + seriesDataB.push([dateStr, (i + 1) * 2]); + } + + chart.setOption({ + xAxis: { type: 'time' }, + yAxis: { type: 'value' }, + series: [ + { name: 'A', type: 'bar', data: seriesDataA, stack: 'stack1' }, + { name: 'B', type: 'bar', data: seriesDataB, stack: 'stack1' } + ] + }); + + const dataA = getECModel(chart).getSeriesByIndex(0).getData(); + const dataB = getECModel(chart).getSeriesByIndex(1).getData(); + + const stackResultDimA = dataA.getCalculationInfo('stackResultDimension'); + const stackResultDimB = dataB.getCalculationInfo('stackResultDimension'); + const stackedOverDimB = dataB.getCalculationInfo('stackedOverDimension'); + + expect(dataA.count()).toEqual(100); + expect(dataB.count()).toEqual(100); + + expect(dataA.get(stackResultDimA, 0)).toEqual(1); + expect(dataB.get(stackResultDimB, 0)).toEqual(3); + expect(dataB.get(stackedOverDimB, 0)).toEqual(1); + + expect(dataA.get(stackResultDimA, 50)).toEqual(51); + expect(dataB.get(stackResultDimB, 50)).toEqual(153); + expect(dataB.get(stackedOverDimB, 50)).toEqual(51); + + expect(dataA.get(stackResultDimA, 99)).toEqual(100); + expect(dataB.get(stackResultDimB, 99)).toEqual(300); + expect(dataB.get(stackedOverDimB, 99)).toEqual(100); + }); + + it('many_datapoints_reverse_order', function () { + const seriesDataA: [string, number][] = []; + const seriesDataB: [string, number][] = []; + + for (let i = 0; i < 50; i++) { + const dateStr = new Date(2025, 0, i + 1).toISOString().split('T')[0]; + seriesDataA.push([dateStr, i + 1]); + } + + for (let i = 49; i >= 0; i--) { + const dateStr = new Date(2025, 0, i + 1).toISOString().split('T')[0]; + seriesDataB.push([dateStr, (i + 1) * 2]); + } + + chart.setOption({ + xAxis: { type: 'time' }, + yAxis: { type: 'value' }, + series: [ + { name: 'A', type: 'bar', data: seriesDataA, stack: 'stack1' }, + { name: 'B', type: 'bar', data: seriesDataB, stack: 'stack1' } + ] + }); + + const dataB = getECModel(chart).getSeriesByIndex(1).getData(); + const stackResultDimB = dataB.getCalculationInfo('stackResultDimension'); + const stackedOverDimB = dataB.getCalculationInfo('stackedOverDimension'); + + expect(dataB.get(stackResultDimB, 0)).toEqual(150); + expect(dataB.get(stackedOverDimB, 0)).toEqual(50); + expect(dataB.get(stackResultDimB, 49)).toEqual(3); + expect(dataB.get(stackedOverDimB, 49)).toEqual(1); + }); + + it('sparse_data_with_gaps', function () { + chart.setOption({ + xAxis: { type: 'time' }, + yAxis: { type: 'value' }, + series: [ + { + name: 'A', + type: 'bar', + data: [['2025-01-01', 10], ['2025-03-01', 30]], + stack: 'stack1' + }, + { + name: 'B', + type: 'bar', + data: [['2025-01-01', 1], ['2025-02-01', 2], ['2025-03-01', 3]], + stack: 'stack1' + } + ] + }); + + const dataB = getECModel(chart).getSeriesByIndex(1).getData(); + const stackResultDimB = dataB.getCalculationInfo('stackResultDimension'); + const stackedOverDimB = dataB.getCalculationInfo('stackedOverDimension'); + + expect(dataB.get(stackResultDimB, 0)).toEqual(11); + expect(dataB.get(stackedOverDimB, 0)).toEqual(10); + expect(dataB.get(stackResultDimB, 1)).toEqual(2); + expect(dataB.get(stackedOverDimB, 1)).toBeNaN(); + expect(dataB.get(stackResultDimB, 2)).toEqual(33); + expect(dataB.get(stackedOverDimB, 2)).toEqual(30); + }); + + it('mixed_null_values_multiple_series', function () { + chart.setOption({ + xAxis: { type: 'time' }, + yAxis: { type: 'value' }, + series: [ + { + name: 'A', + type: 'bar', + data: [['2025-01-01', null], ['2025-02-01', 10], ['2025-03-01', 20]], + stack: 'stack1' + }, + { + name: 'B', + type: 'bar', + data: [['2025-01-01', 5], ['2025-02-01', null], ['2025-03-01', 15]], + stack: 'stack1' + }, + { + name: 'C', + type: 'bar', + data: [['2025-01-01', 3], ['2025-02-01', 7], ['2025-03-01', null]], + stack: 'stack1' + } + ] + }); + + const dataC = getECModel(chart).getSeriesByIndex(2).getData(); + const stackResultDimC = dataC.getCalculationInfo('stackResultDimension'); + const stackedOverDimC = dataC.getCalculationInfo('stackedOverDimension'); + + expect(dataC.get(stackResultDimC, 0)).toEqual(8); + expect(dataC.get(stackedOverDimC, 0)).toEqual(5); + expect(dataC.get(stackResultDimC, 1)).toEqual(17); + expect(dataC.get(stackedOverDimC, 1)).toEqual(10); + expect(dataC.get(stackResultDimC, 2)).toBeNaN(); + }); + }); +}); From 9d88b0a874224b3f6b4afa85661d7d5fcd7048e6 Mon Sep 17 00:00:00 2001 From: Archie Wood <58074498+archiewood@users.noreply.github.com> Date: Fri, 23 Jan 2026 16:33:22 -0500 Subject: [PATCH 2/2] add test file --- test/bar-stack-time.html | 177 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 test/bar-stack-time.html diff --git a/test/bar-stack-time.html b/test/bar-stack-time.html new file mode 100644 index 0000000000..e194e9496a --- /dev/null +++ b/test/bar-stack-time.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + + +
+
+
+
+
+ + + +