From 87bd567f2772db88ff3fc3603d1129cf91bb0bb2 Mon Sep 17 00:00:00 2001 From: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Date: Sat, 16 May 2026 14:13:14 -0700 Subject: [PATCH] fix: align toolbox dataView columns by category when series differ The toolbox dataView built one column per series with rows in series order, which produced misaligned values when two series had different category sets. Build a unified category axis across series and emit one row per category so values line up under the same x label. Fixes #21610 --- src/component/toolbox/feature/DataView.ts | 67 ++++++++++++++-- .../spec/component/toolbox/DataView.test.ts | 77 +++++++++++++++++++ 2 files changed, 137 insertions(+), 7 deletions(-) create mode 100644 test/ut/spec/component/toolbox/DataView.test.ts diff --git a/src/component/toolbox/feature/DataView.ts b/src/component/toolbox/feature/DataView.ts index 300417a7b3..b378236b54 100644 --- a/src/component/toolbox/feature/DataView.ts +++ b/src/component/toolbox/feature/DataView.ts @@ -61,6 +61,27 @@ interface SeriesGroup { valueAxis: Axis } +function addCategory(categories: string[], category: unknown): void { + if (category == null) { + return; + } + const categoryName = category + ''; + zrUtil.indexOf(categories, categoryName) < 0 && categories.push(categoryName); +} + +function getCategoryNameByValue(categories: string[], categoryValue: unknown): string { + if (categoryValue == null) { + return; + } + if (typeof categoryValue === 'number') { + const category = categories[categoryValue]; + if (category != null) { + return category; + } + } + return categoryValue + ''; +} + /** * Group series into two types * 1. on category axis, like line, bar @@ -116,17 +137,47 @@ function assembleSeriesWithCategoryAxis(groups: Dictionary): string zrUtil.each(groups, function (group, key) { const categoryAxis = group.categoryAxis; const valueAxis = group.valueAxis; + const categoryAxisDim = categoryAxis.dim; const valueAxisDim = valueAxis.dim; + const categories: string[] = []; const headers = [' '].concat(zrUtil.map(group.series, function (series) { return series.name; })); + // @ts-ignore TODO Polar - const columns = [categoryAxis.model.getCategories()]; + zrUtil.each(categoryAxis.model.getCategories(), function (category) { + addCategory(categories, category); + }); + zrUtil.each(group.series, function (series) { const rawData = series.getRawData(); - columns.push(series.getRawData().mapArray(rawData.mapDimension(valueAxisDim), function (val) { - return val; + for (let i = 0, len = rawData.count(); i < len; i++) { + addCategory(categories, rawData.getName(i)); + } + }); + + const columns: unknown[][] = [categories]; + zrUtil.each(group.series, function (series) { + const rawData = series.getRawData(); + const valueDim = rawData.mapDimension(valueAxisDim); + const categoryDim = rawData.mapDimension(categoryAxisDim); + const valueByCategory: Dictionary = {}; + for (let i = 0, len = rawData.count(); i < len; i++) { + const name = rawData.getName(i); + if (name) { + valueByCategory[name] = rawData.get(valueDim, i); + } + else { + const categoryName = getCategoryNameByValue(categories, rawData.get(categoryDim, i)); + if (categoryName != null && valueByCategory[categoryName] == null) { + valueByCategory[categoryName] = rawData.get(valueDim, i); + } + } + } + columns.push(zrUtil.map(categories, function (category) { + const value = valueByCategory[category]; + return value == null || isNaN(value as number) ? '' : value; })); }); // Assemble table content @@ -164,7 +215,7 @@ function assembleOtherSeries(series: SeriesModel[]) { }).join('\n\n' + BLOCK_SPLITER + '\n\n'); } -function getContentFromModel(ecModel: GlobalModel) { +export function getContentFromModel(ecModel: GlobalModel) { const result = groupSeries(ecModel); @@ -202,7 +253,7 @@ const itemSplitRegex = new RegExp('[' + ITEM_SPLITER + ']+', 'g'); */ function parseTSVContents(tsv: string) { const tsvLines = tsv.split(/\n+/g); - const headers = trim(tsvLines.shift()).split(itemSplitRegex); + const headers = trim(tsvLines.shift()).split(ITEM_SPLITER); const categories: string[] = []; const series: {name: string, data: string[]}[] = zrUtil.map(headers, function (header) { @@ -212,10 +263,12 @@ function parseTSVContents(tsv: string) { }; }); for (let i = 0; i < tsvLines.length; i++) { - const items = trim(tsvLines[i]).split(itemSplitRegex); + const items = trim(tsvLines[i]).split(ITEM_SPLITER); categories.push(items.shift()); for (let j = 0; j < items.length; j++) { - series[j] && (series[j].data[i] = items[j]); + if (series[j] && items[j] !== '') { + series[j].data[i] = items[j]; + } } } return { diff --git a/test/ut/spec/component/toolbox/DataView.test.ts b/test/ut/spec/component/toolbox/DataView.test.ts new file mode 100644 index 0000000000..55a325a72a --- /dev/null +++ b/test/ut/spec/component/toolbox/DataView.test.ts @@ -0,0 +1,77 @@ +/* +* 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 { getContentFromModel } from '../../../../../src/component/toolbox/feature/DataView'; +import { createChart, getECModel } from '../../../core/utHelper'; +import { EChartsType } from '../../../../../src/echarts'; + + +describe('toolbox/DataView', function () { + + let chart: EChartsType; + + beforeEach(function () { + chart = createChart(); + }); + + afterEach(function () { + chart.dispose(); + }); + + it('aligns category-axis series values by data item name', function () { + chart.setOption({ + xAxis: { + type: 'category', + data: ['cat-1', 'cat-2', 'col-1', 'col-2', 'col-3', 'col-4'] + }, + yAxis: {}, + series: [ + { + type: 'line', + name: 'cats', + data: [ + { name: 'cat-1', value: 1 }, + { name: 'cat-2', value: 2 } + ] + }, + { + type: 'line', + name: 'cols', + data: [ + { name: 'col-1', value: 3 }, + { name: 'col-2', value: 4 }, + { name: 'col-3', value: 5 }, + { name: 'col-4', value: 6 } + ] + } + ] + }); + + expect(getContentFromModel(getECModel(chart)).value).toEqual([ + ' \tcats\tcols', + 'cat-1\t1\t', + 'cat-2\t2\t', + 'col-1\t\t3', + 'col-2\t\t4', + 'col-3\t\t5', + 'col-4\t\t6' + ].join('\n')); + }); + +});