diff --git a/common/changes/@visactor/vchart/feat-convertValueToPosition_2023-07-18-10-41.json b/common/changes/@visactor/vchart/feat-convertValueToPosition_2023-07-18-10-41.json new file mode 100644 index 0000000000..8bd2cb6f23 --- /dev/null +++ b/common/changes/@visactor/vchart/feat-convertValueToPosition_2023-07-18-10-41.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@visactor/vchart", + "comment": "feat: add `convertValueToPosition` api for vchart", + "type": "patch" + } + ], + "packageName": "@visactor/vchart" +} \ No newline at end of file diff --git a/packages/vchart/__tests__/unit/core/vchart.test.ts b/packages/vchart/__tests__/unit/core/vchart.test.ts index e98a3bd034..a75cec79a3 100644 --- a/packages/vchart/__tests__/unit/core/vchart.test.ts +++ b/packages/vchart/__tests__/unit/core/vchart.test.ts @@ -416,7 +416,7 @@ describe('VChart', () => { }); }); - describe('convertDatumToPosition', () => { + describe('convertDatumToPosition and convertValueToPosition', () => { let canvasDom: HTMLCanvasElement; let vchart: VChart; beforeEach(() => { @@ -534,7 +534,17 @@ describe('VChart', () => { lineWidth: 1 } } - } + }, + axes: [ + { + orient: 'bottom', + id: 'bottom' + }, + { + orient: 'left', + id: 'left' + } + ] }, { renderCanvas: canvasDom, @@ -550,6 +560,26 @@ describe('VChart', () => { }) as IPoint; expect(point.x).toBe(mark.attribute.x); expect(point.y).toBe(mark.attribute.y); + + const point2 = vchart.convertDatumToPosition( + { + State: 'WY', + 年龄段: '小于5岁', + 人口数量: 25635 + }, + { + seriesIndex: 0 + }, + true + ) as IPoint; + expect(point2.x).toBe(mark.attribute.x + vchart.getChart()?.getAllSeries()[0].getLayoutStartPoint().x); + expect(point2.y).toBe(mark.attribute.y + vchart.getChart()?.getAllSeries()[0].getLayoutStartPoint().y); + + const value1 = vchart.convertValueToPosition('WY', { axisId: 'bottom' }); + expect(value1).toBe(mark.attribute.x); + + const value2 = vchart.convertValueToPosition(0, { axisId: 'left' }); + expect(value2).toBe(370); }); it('should convert correctly in funnel chart', () => { @@ -617,6 +647,8 @@ describe('VChart', () => { expect(point.x).toBe(centerX); expect(point.y).toBe(centerY); + + expect(vchart.convertValueToPosition(['Step2', 80], { seriesId: 'funnel' })).toEqual({ x: centerX, y: centerY }); }); it('should convert correctly in pie chart', () => { diff --git a/packages/vchart/src/core/interface.ts b/packages/vchart/src/core/interface.ts index 707c2d5602..8ffa663848 100644 --- a/packages/vchart/src/core/interface.ts +++ b/packages/vchart/src/core/interface.ts @@ -28,14 +28,29 @@ import type { Stage } from '@visactor/vrender'; export type DataLinkSeries = { /** * 关联的系列 id + * the binding series id */ seriesId?: StringOrNumber; /** * 关联的系列索引 + * the binding series index */ seriesIndex?: number; }; +export type DataLinkAxis = { + /** + * 关联的轴 id,目前仅支持直角坐标轴 + * the binding axis id + */ + axisId?: StringOrNumber; + /** + * 关联的轴索引,目前仅支持直角坐标轴 + * the binding axis index + */ + axisIndex?: number; +}; + export interface IVChart { readonly id: number; @@ -317,9 +332,28 @@ export interface IVChart { * 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 } + * @param isRelativeToCanvas 是否相对画布坐标,默认为 false Whether relative to canvas coordinates, default is false * @returns */ - convertDatumToPosition: (datum: Datum, dataLinkInfo?: DataLinkSeries) => IPoint | null; + convertDatumToPosition: (datum: Datum, dataLinkInfo?: DataLinkSeries, isRelativeToCanvas?: boolean) => IPoint | null; + + /** + * Convert the value to coordinate position + * @param value number | [number, number], the value to convert + * @param dataLinkInfo the data link info, could be seriesId,seriesIndex,axisId,axisIndex + * @param isRelativeToCanvas 是否相对画布坐标,默认为 false Whether relative to canvas coordinates, default is false + * returns + */ + convertValueToPosition: (( + value: StringOrNumber, + dataLinkInfo: DataLinkAxis, + isRelativeToCanvas?: boolean + ) => number | null) & + (( + value: [StringOrNumber, StringOrNumber], + dataLinkInfo: DataLinkSeries, + isRelativeToCanvas?: boolean + ) => IPoint | null); } export interface IGlobalConfig { diff --git a/packages/vchart/src/core/vchart.ts b/packages/vchart/src/core/vchart.ts index 590bcfa936..b9916e2482 100644 --- a/packages/vchart/src/core/vchart.ts +++ b/packages/vchart/src/core/vchart.ts @@ -1,3 +1,4 @@ +import { series } from './../theme/buildin-theme/light/series/index'; import type { ISeries } from '../series/interface/series'; import { arrayParser } from '../data/parser/array'; import type { ILayoutConstructor, LayoutCallBack } from '../layout/interface'; @@ -26,7 +27,8 @@ import { isTrueBrowser, warn, error, - specTransform + specTransform, + convertPoint } from '../util'; import { Factory } from './factory'; import { Event } from '../event/event'; @@ -64,8 +66,9 @@ 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, isEmpty } from '@visactor/vutils'; -import type { DataLinkSeries, IGlobalConfig, IVChart } from './interface'; +import type { DataLinkAxis, DataLinkSeries, IGlobalConfig, IVChart } from './interface'; import { InstanceManager } from './instance-manager'; +import type { IAxis } from '../component/axis'; export class VChart implements IVChart { readonly id = createID(); @@ -1072,13 +1075,20 @@ export class VChart implements IVChart { return this._chart?.setDimensionIndex(value, opt); } + // TODO: 后续需要考虑滚动场景 /** - * 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 } + * Convert the data corresponding to the graph into coordinates + * 将图形对应的数据转换为坐标,该数据需要从传入图表的数据集中获取,如果数据不存在数据集中,可以使用 `convertValueToPosition` 方法 + * @param datum 要转化的数据 the datum(from data source)to convert + * @param dataLinkInfo 数据的绑定信息,the data link info, could be seriesId or seriesIndex, default is { seriesIndex: 0 } + * @param isRelativeToCanvas 是否相对画布坐标 Whether relative to canvas coordinates * @returns */ - convertDatumToPosition(datum: Datum, dataLinkInfo: DataLinkSeries = {}): IPoint | null { + convertDatumToPosition( + datum: Datum, + dataLinkInfo: DataLinkSeries = {}, + isRelativeToCanvas: boolean = false + ): IPoint | null { if (!this._chart) { return null; } @@ -1100,11 +1110,82 @@ export class VChart implements IVChart { .getViewData() // eslint-disable-next-line eqeqeq .latestData.find((viewDatum: Datum) => keys.every(k => viewDatum[k] == datum[k])); + const seriesLayoutStartPoint = series.getLayoutStartPoint(); + let point: IPoint; if (handledDatum) { - return series.dataToPosition(handledDatum); + point = series.dataToPosition(handledDatum); + } else { + point = series.dataToPosition(datum); } + return convertPoint(point, seriesLayoutStartPoint, isRelativeToCanvas); } return null; } + + // TODO: 1. 后续需要考虑滚动场景 2. 极坐标场景支持 + convertValueToPosition( + value: StringOrNumber, + dataLinkInfo: DataLinkAxis, + isRelativeToCanvas?: boolean + ): number | null; + convertValueToPosition( + value: [StringOrNumber, StringOrNumber], + dataLinkInfo: DataLinkSeries, + isRelativeToCanvas?: boolean + ): IPoint | null; + convertValueToPosition( + value: StringOrNumber | [StringOrNumber, StringOrNumber], + dataLinkInfo: DataLinkAxis | DataLinkSeries, + isRelativeToCanvas: boolean = false + ): number | IPoint | null { + if (!this._chart || isNil(value) || isEmpty(dataLinkInfo)) { + return null; + } + + if (!isArray(value)) { + // 如果单个值,则默认使用 axis 绑定信息 + const { axisId, axisIndex } = dataLinkInfo as DataLinkAxis; + let axis; + if (isValid(axisId)) { + axis = this._chart.getComponentsByKey('axes').find(s => s.userId === axisId); + } else if (isValid(axisIndex)) { + axis = this._chart.getComponentsByKey('axes')?.[axisIndex]; + } + if (!axis) { + warn('Please check whether the `axisId` or `axisIndex` is set!'); + return null; + } + + const pointValue = (axis as IAxis)?.valueToPosition(value); + if (isRelativeToCanvas) { + const axisLayoutStartPoint = axis.getLayoutStartPoint(); + const axisOrient = (axis as IAxis).orient; + return ( + pointValue + + (axisOrient === 'bottom' || axisOrient === 'top' ? axisLayoutStartPoint.x : axisLayoutStartPoint.y) + ); + } + + return pointValue; + } + const { seriesId, seriesIndex } = dataLinkInfo as DataLinkSeries; + let series; + if (isValid(seriesId)) { + series = this._chart.getSeriesInUserId(seriesId); + } else if (isValid(seriesIndex)) { + series = this._chart.getSeriesInIndex([seriesIndex])?.[0]; + } + + if (!series) { + warn('Please check whether the `seriesId` or `seriesIndex` is set!'); + return null; + } + + return convertPoint( + (series as ISeries).valueToPosition(value[0], value[1]), + series.getLayoutStartPoint(), + isRelativeToCanvas + ); + } } diff --git a/packages/vchart/src/series/base/base-series.ts b/packages/vchart/src/series/base/base-series.ts index 123b81d48f..d63f8ef8f4 100644 --- a/packages/vchart/src/series/base/base-series.ts +++ b/packages/vchart/src/series/base/base-series.ts @@ -28,7 +28,8 @@ import type { ISeriesSpec, IExtensionMarkSpec, IExtensionGroupMarkSpec, - EnableMarkType + EnableMarkType, + StringOrNumber } from '../../typings'; import { BaseModel } from '../../model/base-model'; // eslint-disable-next-line no-duplicate-imports @@ -536,6 +537,8 @@ export abstract class BaseSeries extends BaseModel implem abstract dataToPositionX(data: Datum): number; /** 数据到 y 坐标点的映射 */ abstract dataToPositionY(data: Datum): number; + /** 数据到坐标点的映射 */ + abstract valueToPosition(value1: any, value2?: any): IPoint; abstract initMark(): void; abstract initMarkStyle(): void; diff --git a/packages/vchart/src/series/cartesian/cartesian.ts b/packages/vchart/src/series/cartesian/cartesian.ts index 166f7443bd..bf76861d66 100644 --- a/packages/vchart/src/series/cartesian/cartesian.ts +++ b/packages/vchart/src/series/cartesian/cartesian.ts @@ -277,14 +277,13 @@ export abstract class CartesianSeries this.getXAxisHelper().getBandwidth?.(depth) ?? 0; this._markAttributeContext.yBandwidth = (depth: number = 0) => this.getYAxisHelper().getBandwidth?.(depth) ?? 0; - this._markAttributeContext.valueToPosition = ( - valueX: StringOrNumber | StringOrNumber[], - valueY: StringOrNumber | StringOrNumber[] - ) => { - return { - x: this.valueToPositionX(valueX), - y: this.valueToPositionY(valueY) - }; + this._markAttributeContext.valueToPosition = this.valueToPosition.bind(this); + } + + valueToPosition(xValue: StringOrNumber | StringOrNumber[], yValue: StringOrNumber | StringOrNumber[]) { + return { + x: this.valueToPositionX(xValue), + y: this.valueToPositionY(yValue) }; } diff --git a/packages/vchart/src/series/geo/geo.ts b/packages/vchart/src/series/geo/geo.ts index e2e6d9e494..e15c5b7e61 100644 --- a/packages/vchart/src/series/geo/geo.ts +++ b/packages/vchart/src/series/geo/geo.ts @@ -117,6 +117,13 @@ export abstract class GeoSeries exten return dataToLatitude(lonValue); } + valueToPosition(lonValue: number, latValue: number): IPoint { + return { + x: this.dataToLongitude(lonValue), + y: this.dataToLatitude(latValue) + }; + } + positionToData(p: IPoint) { // TODO } diff --git a/packages/vchart/src/series/interface/series.ts b/packages/vchart/src/series/interface/series.ts index 3f90c22616..dcb0f681d9 100644 --- a/packages/vchart/src/series/interface/series.ts +++ b/packages/vchart/src/series/interface/series.ts @@ -11,7 +11,7 @@ import type { IAxisHelper } from '../../component/axis/cartesian/interface'; import type { IPolarAxisHelper } from '../../component/axis/polar/interface'; import type { ISeriesSeriesInfo, ISeriesStackData, ISeriesUpdateDataOption } from './common'; import type { ISeriesTooltipHelper } from './tooltip-helper'; -import type { IInvalidType, Datum, DirectionType } from '../../typings'; +import type { IInvalidType, Datum, DirectionType, StringOrNumber } from '../../typings'; import type { StateValueType } from '../../compile/mark'; import type { StatisticOperations } from '../../data/transforms/dimension-statistics'; import type { IGroupMark } from '../../mark/group'; @@ -161,6 +161,7 @@ export interface ISeries extends IModel, ILayoutItem { dataToPositionX: (datum: Datum) => number | null; dataToPositionY: (datum: Datum) => number | null; dataToPositionZ?: (datum: Datum) => number | null; + valueToPosition: (value1: any, value2?: any) => IPoint; getColorAttribute: () => { scale: IBaseScale; field: string }; getDefaultColorDomain: () => any[]; @@ -215,6 +216,8 @@ export interface ICartesianSeries extends ISeries { dataToPositionX1: (datum: Datum) => number | null; dataToPositionY1: (datum: Datum) => number | null; + + valueToPosition: (value1: any, value2: any) => IPoint; } export interface IPolarSeries extends ISeries { @@ -242,6 +245,8 @@ export interface IPolarSeries extends ISeries { // 轴 radiusAxisHelper: IPolarAxisHelper; angleAxisHelper: IPolarAxisHelper; + + valueToPosition: (value1: any, value2: any) => IPoint; } export interface IGeoSeries extends ISeries { @@ -262,6 +267,8 @@ export interface IGeoSeries extends ISeries { getCoordinateHelper: () => IGeoCoordinateHelper; setCoordinateHelper: (helper: IGeoCoordinateHelper) => void; + + valueToPosition: (value1: any, value2: any) => IPoint; } // 收拢扇区标签形式依赖的 api @@ -278,4 +285,6 @@ export interface IArcSeries extends IPolarSeries { export interface IFunnelSeries extends ISeries { getPoints: (datum: any) => IPoint[]; getCategoryField: () => string; + + valueToPosition: (value: any) => IPoint; } diff --git a/packages/vchart/src/series/word-cloud/base.ts b/packages/vchart/src/series/word-cloud/base.ts index fefff93ba1..84fd3bf4d0 100644 --- a/packages/vchart/src/series/word-cloud/base.ts +++ b/packages/vchart/src/series/word-cloud/base.ts @@ -394,16 +394,19 @@ export class BaseWordCloudSeries