diff --git a/common/changes/@visactor/vchart/feat-convertDataToPosition_2023-07-13-16-23.json b/common/changes/@visactor/vchart/feat-convertDataToPosition_2023-07-13-16-23.json new file mode 100644 index 0000000000..7b83026ef0 --- /dev/null +++ b/common/changes/@visactor/vchart/feat-convertDataToPosition_2023-07-13-16-23.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vchart", + "comment": "feat: add a new api `convertDatumToPosition` for vchart, used for converting data to coordinate position", + "type": "patch" + } + ], + "packageName": "@visactor/vchart" +} diff --git a/packages/vchart/__tests__/unit/core/vchart.test.ts b/packages/vchart/__tests__/unit/core/vchart.test.ts index 130c85ff0c..e98a3bd034 100644 --- a/packages/vchart/__tests__/unit/core/vchart.test.ts +++ b/packages/vchart/__tests__/unit/core/vchart.test.ts @@ -1,9 +1,11 @@ -import type { Group, Text } from '@visactor/vrender'; +import type { Group, IArc, Text } from '@visactor/vrender'; import type { IBarChartSpec } from '../../../src'; import { default as VChart } from '../../../src'; import { createDiv, createCanvas, removeDom } from '../../util/dom'; import type { ICommonChartSpec } from '../../../src/chart/common'; import type { IAreaSeriesSpec } from '../../../src/series/area/interface'; +import type { IPoint } from '../../../src/typings'; +import { polarToCartesian } from '@visactor/vutils'; describe('VChart', () => { describe('render and update', () => { @@ -413,4 +415,264 @@ describe('VChart', () => { expect(legendEventSpy).toBeCalledTimes(2); }); }); + + describe('convertDatumToPosition', () => { + let canvasDom: HTMLCanvasElement; + let vchart: VChart; + beforeEach(() => { + canvasDom = createCanvas(); + canvasDom.style.position = 'relative'; + canvasDom.style.width = '500px'; + canvasDom.style.height = '500px'; + canvasDom.width = 500; + canvasDom.height = 500; + }); + + afterEach(() => { + removeDom(canvasDom); + vchart.release(); + }); + + it('should convert correctly in bar chart', () => { + vchart = new VChart( + { + type: 'bar', + data: [ + { + id: 'barData', + values: [ + { + State: 'WY', + 年龄段: '小于5岁', + 人口数量: 25635 + }, + { + State: 'WY', + 年龄段: '5至13岁', + 人口数量: 1890 + }, + { + State: 'WY', + 年龄段: '14至17岁', + 人口数量: 9314 + }, + { + State: 'DC', + 年龄段: '小于5岁', + 人口数量: 30352 + }, + { + State: 'DC', + 年龄段: '5至13岁', + 人口数量: 20439 + }, + { + State: 'DC', + 年龄段: '14至17岁', + 人口数量: 10225 + }, + { + State: 'VT', + 年龄段: '小于5岁', + 人口数量: 38253 + }, + { + State: 'VT', + 年龄段: '5至13岁', + 人口数量: 42538 + }, + { + State: 'VT', + 年龄段: '14至17岁', + 人口数量: 15757 + }, + { + State: 'ND', + 年龄段: '小于5岁', + 人口数量: 51896 + }, + { + State: 'ND', + 年龄段: '5至13岁', + 人口数量: 67358 + }, + { + State: 'ND', + 年龄段: '14至17岁', + 人口数量: 18794 + }, + { + State: 'AK', + 年龄段: '小于5岁', + 人口数量: 72083 + }, + { + State: 'AK', + 年龄段: '5至13岁', + 人口数量: 85640 + }, + { + State: 'AK', + 年龄段: '14至17岁', + 人口数量: 22153 + } + ] + } + ], + xField: 'State', + yField: '人口数量', + seriesField: '年龄段', + stack: true, + legends: { + visible: true + }, + bar: { + // 配置柱图 hover 时的样式 + state: { + hover: { + stroke: '#000', + lineWidth: 1 + } + } + } + }, + { + renderCanvas: canvasDom, + animation: false + } + ) as VChart; + vchart.renderSync(); + const mark = vchart.getChart()!.getVGrammarView().getMarksByType('rect')[0].elements[0].getGraphicItem(); + const point = vchart.convertDatumToPosition({ + State: 'WY', + 年龄段: '小于5岁', + 人口数量: 25635 + }) as IPoint; + expect(point.x).toBe(mark.attribute.x); + expect(point.y).toBe(mark.attribute.y); + }); + + it('should convert correctly in funnel chart', () => { + vchart = new VChart( + { + type: 'common', + data: [ + { + id: 'funnel', + values: [ + { + value: 100, + name: 'Step1' + }, + { + value: 80, + name: 'Step2' + }, + { + value: 60, + name: 'Step3' + }, + { + value: 40, + name: 'Step4' + }, + { + value: 20, + name: 'Step5' + } + ] + } + ], + series: [ + { + id: 'funnel', + type: 'funnel', + categoryField: 'name', + valueField: 'value', + label: { + visible: true + } + } + ] + }, + { + renderCanvas: canvasDom, + animation: false + } + ) as VChart; + vchart.renderSync(); + const mark = vchart.getChart()!.getVGrammarView().getMarksByType('polygon')[0].elements[1].getGraphicItem(); + // @ts-ignore + const centerX = (mark.attribute.points[0].x + mark.attribute.points[1].x) / 2; + // @ts-ignore + const centerY = (mark.attribute.points[0].y + mark.attribute.points[2].y) / 2; + + const point = vchart.convertDatumToPosition( + { + value: 80, + name: 'Step2' + }, + { seriesId: 'funnel' } + ) as IPoint; + + expect(point.x).toBe(centerX); + expect(point.y).toBe(centerY); + }); + + it('should convert correctly in pie chart', () => { + vchart = new VChart( + { + type: 'pie', + data: [ + { + id: 'id0', + values: [ + { type: 'oxygen', value: '46.60' }, + { type: 'silicon', value: '27.72' }, + { type: 'aluminum', value: '8.13' }, + { type: 'iron', value: '5' }, + { type: 'calcium', value: '3.63' }, + { type: 'sodium', value: '2.83' }, + { type: 'potassium', value: '2.59' }, + { type: 'others', value: '3.5' } + ] + } + ], + valueField: 'value', + categoryField: 'type' + }, + { + renderCanvas: canvasDom, + animation: false + } + ) as VChart; + vchart.renderSync(); + + const point = vchart.convertDatumToPosition( + { + value: 80, + name: 'Step2' + }, + { seriesId: 'funnel' } + ) as IPoint; + + expect(point).toBeNull(); + const point1 = vchart.convertDatumToPosition({ type: 'sodium', value: '2.83' }) as IPoint; + const mark = vchart + .getChart()! + .getVGrammarView() + .getMarksByType('arc')[0] + .elements.filter(ele => ele.key === 'sodium')[0] + .getGraphicItem() as IArc; + + const markCoord = polarToCartesian( + { x: mark.attribute.x as number, y: mark.attribute.y as number }, + mark.attribute.outerRadius as number, + ((mark.attribute.startAngle as number) + (mark.attribute.endAngle as number)) / 2 + ); + + expect(point1.x).toBe(markCoord.x); + expect(point1.y).toBe(markCoord.y); + }); + }); }); diff --git a/packages/vchart/jest.config.js b/packages/vchart/jest.config.js index 076ea9d580..0792016b02 100644 --- a/packages/vchart/jest.config.js +++ b/packages/vchart/jest.config.js @@ -9,15 +9,12 @@ module.exports = { silent: true, globals: { 'ts-jest': { - diagnostics: { - exclude: ['**'] - }, - tsconfig: { - resolveJsonModule: true, - esModuleInterop: true - } - }, - __DEV__: true + resolveJsonModule: true, + esModuleInterop: true, + experimentalDecorators: true, + module: 'ESNext', + tsconfig: './tsconfig.test.json' + } }, verbose: true, collectCoverage: true, diff --git a/packages/vchart/src/chart/base-chart.ts b/packages/vchart/src/chart/base-chart.ts index f33a9de0ca..a065ac80e8 100644 --- a/packages/vchart/src/chart/base-chart.ts +++ b/packages/vchart/src/chart/base-chart.ts @@ -524,7 +524,7 @@ export class BaseChart extends CompilableBase implements IChart { return this._series.filter(r => ids.includes(r.id)); }; - getSeriesInUserId = (userId: string): ISeries | undefined => { + getSeriesInUserId = (userId: StringOrNumber): ISeries | undefined => { if (!userId) { return undefined; } diff --git a/packages/vchart/src/chart/interface/chart.ts b/packages/vchart/src/chart/interface/chart.ts index 8244f8ad0d..daa973ed64 100644 --- a/packages/vchart/src/chart/interface/chart.ts +++ b/packages/vchart/src/chart/interface/chart.ts @@ -84,6 +84,7 @@ export interface IChart extends ICompilable { getSeriesInIndex: (index?: number[]) => ISeries[]; getSeriesInIds: (ids?: number[]) => ISeries[]; getSeriesInUserIdOrIndex: (user_ids?: StringOrNumber[], index?: number[]) => ISeries[]; + getSeriesInUserId: (userId: StringOrNumber) => ISeries | undefined; // component getComponentByIndex: (key: string, index: number) => IComponent | undefined; diff --git a/packages/vchart/src/core/interface.ts b/packages/vchart/src/core/interface.ts index 92e7e6a44f..707c2d5602 100644 --- a/packages/vchart/src/core/interface.ts +++ b/packages/vchart/src/core/interface.ts @@ -4,6 +4,7 @@ import type { IParserOptions } from '@visactor/vdataset/es/parser'; import type { Datum, IMarkStateSpec, + IPoint, IRegionQuerier, IShowTooltipOption, ISpec, @@ -24,6 +25,17 @@ import type { Compiler } from '../compile/compiler'; import type { IChart } from '../chart/interface'; import type { Stage } from '@visactor/vrender'; +export type DataLinkSeries = { + /** + * 关联的系列 id + */ + seriesId?: StringOrNumber; + /** + * 关联的系列索引 + */ + seriesIndex?: number; +}; + export interface IVChart { readonly id: number; @@ -299,6 +311,15 @@ export interface IVChart { * @returns DataSet 实例 */ getDataSet: () => Maybe; + + // 数据转换相关的 api + /** + * Convert the data to coordinate position + * @param datum the datum to convert + * @param dataLinkInfo the data link info, could be seriesId or seriesIndex, default is { seriesIndex: 0 } + * @returns + */ + convertDatumToPosition: (datum: Datum, dataLinkInfo?: DataLinkSeries) => IPoint | null; } export interface IGlobalConfig { diff --git a/packages/vchart/src/core/vchart.ts b/packages/vchart/src/core/vchart.ts index 8c9902631c..590bcfa936 100644 --- a/packages/vchart/src/core/vchart.ts +++ b/packages/vchart/src/core/vchart.ts @@ -13,7 +13,7 @@ import type { EventCallback, EventParams, EventQuery, EventType, IEvent, IEventD import type { IParserOptions } from '@visactor/vdataset/es/parser'; import type { Transform } from '@visactor/vdataset'; // eslint-disable-next-line no-duplicate-imports -import { DataSet, dataViewParser, DataView } from '@visactor/vdataset'; +import { DataSet, dataViewParser, DataView, filter } from '@visactor/vdataset'; import type { Stage } from '@visactor/vrender'; import { isString, @@ -42,7 +42,16 @@ import { stackSplit } from '../data/transforms/stack-split'; import { copyDataView } from '../data/transforms/copy-data-view'; import type { ITooltipHandler } from '../typings/tooltip'; import type { Tooltip } from '../component/tooltip'; -import type { Datum, IRegionQuerier, IShowTooltipOption, ISpec, Maybe, MaybeArray, StringOrNumber } from '../typings'; +import type { + Datum, + IPoint, + IRegionQuerier, + IShowTooltipOption, + ISpec, + Maybe, + MaybeArray, + StringOrNumber +} from '../typings'; import { AnimationStateEnum } from '../animation/interface'; import type { IBoundsLike } from '@visactor/vutils'; import { ThemeManager } from '../theme/theme-manager'; @@ -54,8 +63,8 @@ import type { ILegend } from '../component/legend/interface'; import { getCanvasDataURL, URLToImage } from '../util/image'; import { ChartEvent, DEFAULT_CHART_HEIGHT, DEFAULT_CHART_WIDTH } from '../constant'; // eslint-disable-next-line no-duplicate-imports -import { getContainerSize, isArray } from '@visactor/vutils'; -import type { IGlobalConfig, IVChart } from './interface'; +import { getContainerSize, isArray, isEmpty } from '@visactor/vutils'; +import type { DataLinkSeries, IGlobalConfig, IVChart } from './interface'; import { InstanceManager } from './instance-manager'; export class VChart implements IVChart { @@ -1062,4 +1071,40 @@ export class VChart implements IVChart { setDimensionIndex(value: StringOrNumber, opt: DimensionIndexOption = {}) { return this._chart?.setDimensionIndex(value, opt); } + + /** + * Convert the data to coordinate position + * @param datum the datum to convert + * @param dataLinkInfo the data link info, could be seriesId or seriesIndex, default is { seriesIndex: 0 } + * @returns + */ + convertDatumToPosition(datum: Datum, dataLinkInfo: DataLinkSeries = {}): IPoint | null { + if (!this._chart) { + return null; + } + if (isEmpty(datum)) { + return null; + } + const { seriesId, seriesIndex = 0 } = dataLinkInfo; + + let series: ISeries; + if (isValid(seriesId)) { + series = this._chart.getSeriesInUserId(seriesId); + } else if (isValid(seriesIndex)) { + series = this._chart.getSeriesInIndex([seriesIndex])?.[0]; + } + + if (series) { + const keys = Object.keys(datum); + const handledDatum = series + .getViewData() + // eslint-disable-next-line eqeqeq + .latestData.find((viewDatum: Datum) => keys.every(k => viewDatum[k] == datum[k])); + if (handledDatum) { + return series.dataToPosition(handledDatum); + } + } + + return null; + } } diff --git a/packages/vchart/src/series/base/base-series.ts b/packages/vchart/src/series/base/base-series.ts index d4bd812c61..123b81d48f 100644 --- a/packages/vchart/src/series/base/base-series.ts +++ b/packages/vchart/src/series/base/base-series.ts @@ -533,9 +533,9 @@ export abstract class BaseSeries extends BaseModel implem /** 数据到坐标点的映射 */ abstract dataToPosition(data: Datum): IPoint; /** 数据到 x 坐标点的映射 */ - abstract dataToPositionX(data: any): number; + abstract dataToPositionX(data: Datum): number; /** 数据到 y 坐标点的映射 */ - abstract dataToPositionY(data: any): number; + abstract dataToPositionY(data: Datum): number; abstract initMark(): void; abstract initMarkStyle(): void; diff --git a/packages/vchart/tsconfig.test.json b/packages/vchart/tsconfig.test.json new file mode 100644 index 0000000000..37837cb01b --- /dev/null +++ b/packages/vchart/tsconfig.test.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": {}, + "references": [] +}