diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 38d78327b5..61165500eb 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -500,6 +500,7 @@ importers: canvas: 2.11.2 eslint: ~8.18.0 jest: ^26.0.0 + lil-gui: ^0.17.0 react: ^18.0.0 react-dom: ^18.0.0 rollup: 3.20.5 @@ -533,6 +534,7 @@ importers: canvas: 2.11.2 eslint: 8.18.0 jest: 26.6.3_xxvpynkn5i4ehycnunrxxsezu4 + lil-gui: 0.17.0 react: 18.3.1 react-dom: 18.3.1_react@18.3.1 rollup: 3.20.5 @@ -15485,6 +15487,10 @@ packages: resolve: 1.22.8 dev: false + /lil-gui/0.17.0: + resolution: {integrity: sha512-MVBHmgY+uEbmJNApAaPbtvNh1RCAeMnKym82SBjtp5rODTYKWtM+MXHCifLe2H2Ti1HuBGBtK/5SyG4ShQ3pUQ==} + dev: true + /lilconfig/2.0.4: resolution: {integrity: sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA==} engines: {node: '>=10'} diff --git a/packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list-2.ts b/packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list-2.ts new file mode 100644 index 0000000000..0ab9b79e37 --- /dev/null +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list-2.ts @@ -0,0 +1,346 @@ +import { registerRankingList } from '../../../../src'; +import { VChart } from '@visactor/vchart'; +import { defaultSpec } from '../../../../src/charts/ranking-list/constant'; +import { merge } from '@visactor/vutils'; +import { GUI } from 'lil-gui'; + +const guiObject = { + name: 'rankingList', + labelLayout: 'top', + pageSize: 8, + scrollSize: 2, + animationType: 'both', + animationInterval: 4000, + animationDuration: 2000, + animationEasing: 'linear', + rankingIconVisible: true, + orderLabelVisible: true +}; + +const chartData = [ + { + y: '吐鲁番', + x: 33.8 + }, + { + y: '荥阳', + x: 31.9 + }, + { + y: '济源', + x: 31.8 + }, + { + y: '汉寿', + x: 31.5 + }, + { + y: '仙桃', + x: 31.3 + }, + { + y: '桃江', + x: 31.3 + }, + { + y: '博爱', + x: 31.2 + }, + { + y: '孟州', + x: 31.2 + } +]; +console.log(chartData); + +const spec = { + type: 'rankingList', + data: chartData, + xField: 'x', + yField: 'y', + // background: 'rgba(0,0,0,1)', + width: 600, + height: 800, + bar: { + height: 40, + style: { + cornerRadius: [0, 20, 20, 0], + fill: { + gradient: 'linear', + x0: 0, + y0: 0, + x1: 1, + y1: 0, + stops: [ + { + offset: 0, + color: 'rgba(210, 44, 59, 1)' + }, + { + offset: 1, + color: 'rgba(253, 117, 107,1)' + } + ] + } + } + }, + labelLayout: 'bothEnd', + + rankingIcon: { + visible: false, + style: { + size: 5, + symbolType: 'circle', + fill: 'yellow' + // stroke: 'yellow', + // symbolType: + // '' + } + }, + orderLabel: { + visible: false, + style: { + fontSize: 20 + } + }, + nameLabel: { + visible: true, + style: { + fontSize: 20, + fill: 'rgba(210, 44, 59, 1)' + } + }, + valueLabel: { + visible: true, + style: { + fontSize: 'rgba(210, 44, 59, 1)' + } + }, + decorateHaloIcons: [], + pageSize: guiObject.pageSize, + scrollSize: guiObject.scrollSize, + barBackground: { + visible: true, + // width: + style: { + // fill: 'red', + symbolType: 'rect' + } + }, + animation: false, + background: 'rgba(249,229,209)', + title: [ + { + text: '全国高温排行榜', + align: 'center', + textStyle: { + fill: 'rgba(210, 44, 59, 1)', + fontSize: 60 + } + }, + { + text: '中国天气', + align: 'center', + padding: 5, + textStyle: { + fill: '#fff', + fontSize: 30 + } + }, + { + text: '2023 0708时', + align: 'center', + padding: 5, + textStyle: { + fill: 'rgba(210, 44, 59, 1)', + fontSize: 15, + fontWeight: 'normal' + } + }, + { + text: '单位: °C', + align: 'right', + padding: 5, + textStyle: { + fill: 'rgba(210, 44, 59, 1)', + fontSize: 15, + fontWeight: 'normal' + } + }, + { + text: '注:数据基于国家级气象站实况气温,来源中央气象台,中国天气网制图。', + orient: 'bottom', + align: 'center', + textStyle: { + fill: 'rgba(100, 100, 100, 1)', + fontSize: 10, + fontWeight: 'normal' + } + } + ], + customMark: [ + { + type: 'symbol', + style: { + symbolType: + 'M161.046909 351.698183a350.755924 350.755924 0 0 1 76.087416-218.83966 40.046009 40.046009 0 0 0-32.036807-63.838049A117.782379 117.782379 0 0 0 120.765335 104.355187a353.347136 353.347136 0 0 0 183.740511 594.094318 359.236255 359.236255 0 0 0 126.733839 1.648954 2.591212 2.591212 0 0 0 0-4.94686A353.347136 353.347136 0 0 1 161.046909 351.698183z" fill="#00FFFF" p-id="5219">', + x: 200, + y: 120, + size: 50, + fill: 'rgba(210, 44, 59, 1)' + } + } + ] +}; + +const run = () => { + registerRankingList(); + const cs = new VChart(merge(defaultSpec, spec), { + dom: document.getElementById('chart') as HTMLElement, + //theme: 'dark', + onError: err => { + console.error(err); + } + }); + console.time('renderTime'); + + cs.renderSync(); + + // gui + const gui = new GUI(); + gui.add(guiObject, 'name'); + gui.add(guiObject, 'labelLayout', ['top', 'bothEnd']).onChange(value => { + cs.updateSpec( + { + ...spec, + labelLayout: value + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'pageSize').onChange(value => { + cs.updateSpec( + { + ...spec, + pageSize: value + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'scrollSize').onChange(value => { + cs.updateSpec( + { + ...spec, + scrollSize: value + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'animationType', ['both', 'scroll', 'grow']).onChange(value => { + cs.updateSpec( + { + ...spec, + animation: { + ...spec.animation, + type: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'animationInterval').onChange(value => { + cs.updateSpec( + { + ...spec, + animation: { + ...spec.animation, + interval: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + + gui.add(guiObject, 'animationDuration').onChange(value => { + cs.updateSpec( + { + ...spec, + animation: { + ...spec.animation, + duration: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + + gui.add(guiObject, 'animationEasing', ['linear', 'quadIn', 'quadOut', 'quadInOut']).onChange(value => { + cs.updateSpec( + { + ...spec, + animation: { + ...spec.animation, + easing: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'rankingIconVisible').onChange(value => { + cs.updateSpec( + { + ...spec, + rankingIcon: { + ...spec.rankingIcon, + visible: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'orderLabelVisible').onChange(value => { + cs.updateSpec( + { + ...spec, + orderLabel: { + ...spec.orderLabel, + visible: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + + console.timeEnd('renderTime'); + window['vchart'] = cs; + console.log(cs); +}; +run(); diff --git a/packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list-3.ts b/packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list-3.ts new file mode 100644 index 0000000000..85d4325438 --- /dev/null +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list-3.ts @@ -0,0 +1,395 @@ +import { registerRankingList } from '../../../../src'; +import { VChart } from '@visactor/vchart'; +import { defaultSpec } from '../../../../src/charts/ranking-list/constant'; +import { merge } from '@visactor/vutils'; +import { GUI } from 'lil-gui'; + +const guiObject = { + name: 'rankingList', + labelLayout: 'bothEnd', + pageSize: 8, + scrollSize: 2, + animationType: 'both', + animationInterval: 4000, + animationDuration: 2000, + animationEasing: 'linear', + rankingIconVisible: true, + orderLabelVisible: true +}; + +const chartData = [ + { + y: '南宁', + x: 38 + }, + { + y: '百色', + x: 37 + }, + { + y: '桂林', + x: 36 + }, + { + y: '柳州', + x: 35 + }, + { + y: '钦州', + x: 34 + }, + { + y: '玉林', + x: 33 + }, + { + y: '贵港', + x: 32 + }, + { + y: '河池', + x: 31 + }, + { + y: '崇左', + x: 30 + }, + { + y: '防城港', + x: 29 + } +]; +console.log(chartData); + +const spec = { + type: 'rankingList', + data: chartData, + xField: 'x', + yField: 'y', + // background: 'rgba(0,0,0,1)', + width: 400, + height: 900, + padding: { + right: 10, + left: 80 + }, + bar: { + height: 20, + visible: false, + style: { + visible: false, + cornerRadius: 10 + } + }, + barBackground: { + type: 'rect', + style: { + // fill: 'rgba(245,46,0,1)', + cornerRadius: [0, 10, 10, 0], + fill: datum => { + if (['南宁', '百色', '桂林'].includes(datum['y'])) { + return 'rgba(245,46,0,1)'; + } else if (['柳州', '钦州', '玉林', '贵港', '河池'].includes(datum['y'])) { + return 'rgba(255,95,0,1)'; + } else { + return 'rgba(255,135,0,1)'; + } + } + } + }, + labelLayout: guiObject.labelLayout, + + rankingIcon: { + visible: true, + style: { + symbolType: 'circle', + x: 0, + size: 50, + dx: -20, + fill: datum => { + if (['南宁', '百色', '桂林'].includes(datum['y'])) { + return 'rgba(245,46,0,1)'; + } else if (['柳州', '钦州', '玉林', '贵港', '河池'].includes(datum['y'])) { + return 'rgba(255,95,0,1)'; + } else { + return 'rgba(255,135,0,1)'; + } + } + } + }, + orderLabel: { + visible: true, + style: { + fontSize: 25, + fill: 'rgba(180, 70, 0, 1)' + } + }, + nameLabel: { + visible: true, + zIndex: 999, + style: { + fontSize: 15, + fill: '#fff', + dx: 4, + zIndex: 999 + } + }, + valueLabel: { + visible: true, + style: { + fontSize: 25, + fill: 'rgba(180, 70, 0, 1)', + dx: -70, + dy: -25 + } + }, + decorateHaloIcons: [ + // { + // visible: true, + // zIndex: -10, + // style: { + // symbolType: 'circle', + // fill: 'rgba(245,46,0,1)', + // x: 0, + // size: 50, + // dx: -20 + // } + // } + ], + pageSize: guiObject.pageSize, + scrollSize: guiObject.scrollSize, + animation: false, + background: { + gradient: 'linear', + x0: 0, + y0: 0, + x1: 0, + y1: 1, + stops: [ + { + offset: 0, + color: 'rgba(255,132,0,1)' + }, + { + offset: 1, + color: 'rgba(255,244,166,1)' + } + ] + }, + title: [ + { + text: '广西整点气温排行', + align: 'center', + textStyle: { + fill: 'rgba(128, 20, 2, 1)', + fontSize: 35, + stroke: '#fff', + lineWidth: 1, + fontFamily: 'SimSun, Songti SC', + dx: -20 + } + }, + { + text: '7月28日08时', + align: 'center', + padding: 5, + textStyle: { + fill: '#fff', + fontSize: 10, + fontWeight: 'normal', + dx: -20 + } + }, + { + text: '单位: °C', + align: 'right', + padding: 5, + textStyle: { + fill: '#fff', + fontSize: 10, + fontWeight: 'normal', + dx: -20 + } + }, + { + text: '注:数据基于国家级气象站实况气温,来源广西壮族自治区气象台', + orient: 'bottom', + align: 'center', + textStyle: { + fill: 'rgba(100, 100, 100, 1)', + fontSize: 10, + fontWeight: 'normal' + } + } + ], + markLine: chartData.map(datum => { + return { + y: datum['y'], + x: 26, + x1: 'max', + line: { + style: { + stroke: '#fff', + lineWidth: 2, + lineDash: [5, 5] + } + }, + endSymbol: { + style: { + fill: '#fff' + } + } + }; + }) +}; + +const run = () => { + registerRankingList(); + const cs = new VChart(merge(defaultSpec, spec), { + dom: document.getElementById('chart') as HTMLElement, + //theme: 'dark', + onError: err => { + console.error(err); + } + }); + console.time('renderTime'); + + cs.renderSync(); + + // gui + const gui = new GUI(); + gui.add(guiObject, 'name'); + gui.add(guiObject, 'labelLayout', ['top', 'bothEnd']).onChange(value => { + cs.updateSpec( + { + ...spec, + labelLayout: value + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'pageSize').onChange(value => { + cs.updateSpec( + { + ...spec, + pageSize: value + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'scrollSize').onChange(value => { + cs.updateSpec( + { + ...spec, + scrollSize: value + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'animationType', ['both', 'scroll', 'grow']).onChange(value => { + cs.updateSpec( + { + ...spec, + animation: { + ...spec.animation, + type: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'animationInterval').onChange(value => { + cs.updateSpec( + { + ...spec, + animation: { + ...spec.animation, + interval: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + + gui.add(guiObject, 'animationDuration').onChange(value => { + cs.updateSpec( + { + ...spec, + animation: { + ...spec.animation, + duration: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + + gui.add(guiObject, 'animationEasing', ['linear', 'quadIn', 'quadOut', 'quadInOut']).onChange(value => { + cs.updateSpec( + { + ...spec, + animation: { + ...spec.animation, + easing: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'rankingIconVisible').onChange(value => { + cs.updateSpec( + { + ...spec, + rankingIcon: { + ...spec.rankingIcon, + visible: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'orderLabelVisible').onChange(value => { + cs.updateSpec( + { + ...spec, + orderLabel: { + ...spec.orderLabel, + visible: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + + console.timeEnd('renderTime'); + window['vchart'] = cs; + console.log(cs); +}; +run(); diff --git a/packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list.ts b/packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list.ts new file mode 100644 index 0000000000..212087d4f0 --- /dev/null +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list.ts @@ -0,0 +1,314 @@ +import { registerRankingList } from '../../../../src'; +import { VChart } from '@visactor/vchart'; +import { defaultSpec } from '../../../../src/charts/ranking-list/constant'; +import { merge } from '@visactor/vutils'; +import { GUI } from 'lil-gui'; + +const guiObject = { + name: 'rankingList', + labelLayout: 'top', + pageSize: 5, + scrollSize: 2, + animationType: 'both', + animationInterval: 4000, + animationDuration: 2000, + animationEasing: 'linear', + rankingIconVisible: true, + orderLabelVisible: true +}; + +const chartData = [ + { + y: '吉林xx', + x: 50 + }, + { + y: '内蒙古', + x: 40 + }, + { + y: '河北', + x: 30 // + }, + { + y: '湖南', // + x: 30 + }, + { + y: '江西', + x: 24 + }, + { + y: '山西', + x: 20 + }, + { + y: '河南', + x: 200 + }, + { + y: '辽宁', + x: 10 + }, + { + y: '山东', + x: 10 + }, + { + y: '湖北', + x: 10 + } +]; +console.log(chartData); + +const spec = { + type: 'rankingList', + data: chartData, + xField: 'x', + yField: 'y', + background: 'rgba(0,0,0,1)', + bar: { + height: 10, + style: { + cornerRadius: 5, + fill: { + gradient: 'linear', + x0: 0, + y0: 0, + x1: 1, + y1: 0, + stops: [ + { + offset: 0, + color: 'rgba(0, 110, 255,0.2)' + }, + { + offset: 1, + color: 'rgba(0, 110, 255,1)' + } + ] + } + } + }, + labelLayout: guiObject.labelLayout, + + rankingIcon: { + visible: guiObject.rankingIconVisible, + style: { + size: 5, + symbolType: 'circle', + fill: 'yellow' + // stroke: 'yellow', + // symbolType: + // '' + } + }, + orderLabel: { + visible: guiObject.orderLabelVisible, + style: { + fontSize: 20 + } + }, + nameLabel: { + visible: true, + style: { + fontSize: 20 + } + }, + valueLabel: { + visible: true, + style: { + fontSize: 20 + } + }, + decorateHaloIcons: [ + { + visible: true, + style: { + symbolType: 'circle', + size: 8, + fill: 'rgba(255,255,255,0.5)' + } + }, + { + visible: true, + style: { + symbolType: 'circle', + size: 15, + lineWidth: 1, + stroke: 'rgba(255,255,255,0.8)', + fill: 'rgba(255,255,255,0.5)' + } + } + ], + pageSize: guiObject.pageSize, + scrollSize: guiObject.scrollSize, + width: 800, + height: 400, + animation: { + type: guiObject.animationType, + interval: guiObject.animationInterval, + duration: guiObject.animationDuration, + easing: guiObject.animationEasing + }, + barBackground: { + visible: true, + // width: + style: { + // fill: 'red', + symbolType: 'rect' + } + } + // animation: false +}; + +const run = () => { + registerRankingList(); + const cs = new VChart(merge(defaultSpec, spec), { + dom: document.getElementById('chart') as HTMLElement, + //theme: 'dark', + onError: err => { + console.error(err); + } + }); + console.time('renderTime'); + + cs.renderSync(); + + // gui + const gui = new GUI(); + gui.add(guiObject, 'name'); + gui.add(guiObject, 'labelLayout', ['top', 'bothEnd']).onChange(value => { + cs.updateSpec( + { + ...spec, + labelLayout: value + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'pageSize').onChange(value => { + cs.updateSpec( + { + ...spec, + pageSize: value + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'scrollSize').onChange(value => { + cs.updateSpec( + { + ...spec, + scrollSize: value + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'animationType', ['both', 'scroll', 'grow']).onChange(value => { + cs.updateSpec( + { + ...spec, + animation: { + ...spec.animation, + type: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'animationInterval').onChange(value => { + cs.updateSpec( + { + ...spec, + animation: { + ...spec.animation, + interval: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + + gui.add(guiObject, 'animationDuration').onChange(value => { + cs.updateSpec( + { + ...spec, + animation: { + ...spec.animation, + duration: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + + gui.add(guiObject, 'animationEasing', ['linear', 'quadIn', 'quadOut', 'quadInOut']).onChange(value => { + cs.updateSpec( + { + ...spec, + animation: { + ...spec.animation, + easing: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'rankingIconVisible').onChange(value => { + cs.updateSpec( + { + ...spec, + rankingIcon: { + ...spec.rankingIcon, + visible: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + gui.add(guiObject, 'orderLabelVisible').onChange(value => { + cs.updateSpec( + { + ...spec, + orderLabel: { + ...spec.orderLabel, + visible: value + } + }, + false, + { + enableExitAnimation: false + } + ); + }); + + console.timeEnd('renderTime'); + window['vchart'] = cs; + console.log(cs); +}; +run(); diff --git a/packages/vchart-extension/package.json b/packages/vchart-extension/package.json index 87457914e4..bbb25a90e4 100644 --- a/packages/vchart-extension/package.json +++ b/packages/vchart-extension/package.json @@ -53,7 +53,8 @@ "rollup-plugin-gzip": "3.1.0", "rollup-plugin-bundle-size": "1.0.3", "rollup-plugin-sizes": "1.0.5", - "rollup": "3.20.5" + "rollup": "3.20.5", + "lil-gui": "^0.17.0" }, "publishConfig": { "access": "public", diff --git a/packages/vchart-extension/src/charts/ranking-list/constant.ts b/packages/vchart-extension/src/charts/ranking-list/constant.ts new file mode 100644 index 0000000000..e648582daf --- /dev/null +++ b/packages/vchart-extension/src/charts/ranking-list/constant.ts @@ -0,0 +1,65 @@ +import { IRankingListSpec } from './interface'; + +export const defaultSpec: Omit = { + width: 400, + height: 225, + labelLayout: 'top', + bar: { + height: 100, + style: { + cornerRadius: 5 + } + }, + barBackground: { + type: 'rect', + style: { + fill: 'rgba(255,255,255,0.1)', + cornerRadius: 5 + } + }, + rankingIcon: { + visible: true, + style: { + fill: 'rgba(253,253,253,0.5)', + size: 12 + } + }, + nameLabel: { + visible: true, + style: { + // fontFamily: '' + fontSize: 20, + fontWeight: 'normal', + fill: 'rgba(255,255,255,0.7)', + textBaseline: 'middle' + } + }, + orderLabel: { + visible: true, + style: { + // fontFamily: '' + fontSize: 20, + fontWeight: 'normal', + fill: 'rgba(255,255,255,0.7)', + textBaseline: 'middle' + } + }, + valueLabel: { + visible: true, + style: { + // fontFamily: '' + fontSize: 14, + fontWeight: 'normal', + fill: 'rgba(255,255,255,1)', + textBaseline: 'middle' + } + }, + pageSize: 5, + scrollSize: 1, + animation: { + type: 'both', + interval: 4000, + duration: 2000, + easing: 'linear' + } +}; diff --git a/packages/vchart-extension/src/charts/ranking-list/interface.ts b/packages/vchart-extension/src/charts/ranking-list/interface.ts new file mode 100644 index 0000000000..e85589436e --- /dev/null +++ b/packages/vchart-extension/src/charts/ranking-list/interface.ts @@ -0,0 +1,123 @@ +import { Datum } from '@visactor/vchart/src/typings/common'; +import { + ITextGraphicAttribute, + ISymbolGraphicAttribute, + IRectGraphicAttribute, + EasingType +} from '@visactor/vrender-core'; + +type IRankingListData = Datum[]; + +export interface IRankingListSpec { + /** + * 图表类型 + */ + type: 'rankingList'; + /** + * 数据 + */ + data: IRankingListData; + /** + * x轴字段 + */ + xField: string; + /** + * y轴字段 + */ + yField: string; + width?: number; + height?: number; + /** + * 标签布局 + * @default 'top' + */ + labelLayout?: 'top' | 'bothEnd'; + /** + * 柱样式 + */ + bar?: { + height?: number; + style?: IRectGraphicAttribute; + }; + /** + * 柱图背景 + */ + barBackground?: { + visible?: boolean; + type?: string; + style?: ISymbolGraphicAttribute | IRectGraphicAttribute; + }; + /** + * 排名图标 + */ + rankingIcon?: { + visible?: boolean; + style?: ISymbolGraphicAttribute; + }; + /** + * 装饰图元 + */ + decorateHaloIcons?: [ + { + visible?: boolean; + // type?: 'circle' | 'square' | 'emptyCircle' | 'diamond' | 'halo' | 'concentric' | 'custom'; + style?: ISymbolGraphicAttribute; + } + ]; + /** + * 排名序号 + */ + orderLabel?: { + visible?: boolean; + style?: ITextGraphicAttribute; + formatMethod?: (text: string, ctx: any) => string; + }; + /** + * 名称标签(yField对应的标签) + */ + nameLabel?: { + visible?: boolean; + style?: ITextGraphicAttribute; + formatMethod?: (text: string, ctx: any) => string; + }; + /** + * 值标签(xField对应的标签) + */ + valueLabel?: { + visible?: boolean; + style?: ITextGraphicAttribute; + formatMethod?: (text: string, ctx: any) => string; + }; + /** + * 每页行数 + */ + pageSize?: number; + /** + * 滚动行数 + */ + scrollSize?: number; + /** + * 动画 + */ + animation?: { + /** + * 动画类型 + * @default 'both' + * 'scroll' 滚动 + * 'grow' 伸展 + */ + type?: 'scroll' | 'grow' | 'both'; + /** + * 动画间隔 + */ + interval?: number; + /** + * 动画时长 + */ + duration?: number; + /** + * 动画缓动效果 + */ + easing?: EasingType; + }; +} diff --git a/packages/vchart-extension/src/charts/ranking-list/ranking-list-transformer.ts b/packages/vchart-extension/src/charts/ranking-list/ranking-list-transformer.ts new file mode 100644 index 0000000000..7ad2c86909 --- /dev/null +++ b/packages/vchart-extension/src/charts/ranking-list/ranking-list-transformer.ts @@ -0,0 +1,573 @@ +import { Datum } from '@visactor/vchart/src/typings'; +import type { IRankingListSpec } from './interface'; +import { CommonChartSpecTransformer } from '@visactor/vchart'; +import { TextMeasure } from '@visactor/vutils'; +import { defaultSpec } from './constant'; +import { applyVisible, computeDataRange, mergeObjects } from './utils'; +import { IAnimationParameters, IElement } from '@visactor/vgammar-core'; + +const DATA_KEY = 'dataKey'; +const ORDER_KEY = 'VCHART_ORDER'; +const SUPPLY_DATA_KEY = 'SUPPLY_DATA_KEY'; +const NAME_LABEL_PADDING_RIGHT = 10; +const NAME_ORDER_PADDING_RIGHT = 5; +const NAME_SYMBOL_PADDING_RIGHT = 8; +const CHART_PADDING_LEFT = 5; +const CHART_PADDING_RIGHT = 5; +const VALUE_LABEL_PADDING_LEFT = 5; + +const LABEL_PADDING_BOTTOM = 5; + +export class RankingListChartSpecTransformer extends CommonChartSpecTransformer { + protected nameLabelTextMeasure: TextMeasure; + protected valueLabelTextMeasure: TextMeasure; + protected orderLabelTextMeasure: TextMeasure; + protected originalData: Datum[]; + protected dataSpecs: any[]; + + transformSpec(spec: any): void { + super.transformSpec(spec); + this.normalizeSpec(spec); + this.upgradeTextMeasure(spec); + this.processData(spec); + + // rankingList spec -> vchart spec + this.transformBaseSpec(spec); + this.transformAnimationSpec(spec); + this.transformAxesSpec(spec); + + spec.extensionMark = [ + // 柱图背景 + this.generateBarBackground(spec), + // 辅助图标 + ...this.generateDecorateHaloIcons(spec), + // 左侧图标 + this.generateRankingIcon(spec), + // 左侧label + this.generateNameLabel(spec), + // 左侧序号label + this.generateOrderLabel(spec), + // 右侧label + this.generateValueLabel(spec) + ]; + + super.transformSpec(spec); + } + + normalizeSpec(spec: any) { + // 处理配置 + mergeObjects(spec, defaultSpec); + applyVisible(spec, [ + 'barBackground', + 'rankingIcon', + 'decorateHaloIcon', + 'orderLabel', + 'nameLabel', + 'valueLabel' + // 'bar' + ]); + } + + upgradeTextMeasure(spec: any) { + // 初始化文字测量 + this.nameLabelTextMeasure?.release(); + this.valueLabelTextMeasure?.release(); + this.orderLabelTextMeasure?.release(); + this.nameLabelTextMeasure = new TextMeasure({ + defaultFontParams: spec.nameLabel?.style ?? {} + }); + this.valueLabelTextMeasure = new TextMeasure({ + defaultFontParams: spec.valueLabel?.style ?? {} + }); + this.orderLabelTextMeasure = new TextMeasure({ + defaultFontParams: spec.orderLabel?.style ?? {} + }); + } + + processData(spec: any) { + // ps: 如果updateSpec后, 同时执行2次processData会有问题, 在这里用比较hack的方式绕过第2次 + if (!spec.data[0]?.values) { + this.dataSpecs = this.processRankingData(spec as unknown as IRankingListSpec); + this.originalData = spec.data; + spec.data = this.dataSpecs[0].data; + // console.log('processdata'); + } + } + + transformBaseSpec(spec: any) { + spec.type = 'common'; + spec.dataKey = DATA_KEY; + spec.series = [ + { + type: 'bar', + direction: 'horizontal', + xField: spec.xField, + yField: spec.yField, + barWidth: spec.bar?.height ?? 10, + bar: { + ...spec.bar, + style: { + ...spec.bar?.style, + x1: 0, + visible: datum => { + if (datum[SUPPLY_DATA_KEY]) { + return false; + } + return spec.bar?.style ?? true; + } + } + } + } + ]; + } + + transformAnimationSpec(spec: any) { + const totalDuration = spec.animation.duration; + + if (spec.animation) { + spec.player = { + ...spec.player, + specs: this.dataSpecs, + auto: true, + visible: false, + interval: spec.animation.interval + spec.animation.duration / 2, + loop: true + }; + + spec.animationExit = this.getAnimationExit(spec, totalDuration); + spec.animationAppear = this.getAnimationEnter(spec, 'rect', totalDuration); + spec.animationEnter = this.getAnimationEnter(spec, 'rect', totalDuration); + } + } + + transformAxesSpec(spec: any) { + const { min, max } = computeDataRange(this.originalData, spec.xField); + spec.axes = [ + { + orient: 'left', + type: 'band', + visible: false, + inverse: true + }, + { + orient: 'bottom', + label: { visible: true }, + type: 'linear', + visible: false, + min, + max + } + ]; + } + + generateBarBackground(spec: any) { + const totalDuration = spec.animation.duration; + return { + type: spec.barBackground.type, + dataId: 'data', + visible: true, + dataKey: DATA_KEY, + zIndex: -99, + style: { + x: (datum: Datum, ctx: any) => + spec.barBackground.type === 'symbol' ? ctx.getRegion().getLayoutRect().width / 2 : 0, + y: (datum: Datum, ctx: any) => { + return ( + ctx.valueToY([datum[spec.yField]]) + + ctx.yBandwidth() / 2 - + (spec.barBackground.type === 'symbol' ? 0 : spec.bar.height / 2) + ); + }, + size: (datum: Datum, ctx: any) => [ctx.getRegion().getLayoutRect().width, spec.bar.height], + width: (datum: Datum, ctx: any) => ctx.getRegion().getLayoutRect().width, + height: spec.bar.height, + ...spec.barBackground.style, + visible: (datum: Datum) => { + if (datum[SUPPLY_DATA_KEY]) { + return false; + } + return spec.barBackground.style.visible; + } + }, + animation: Boolean(spec.animation), + animationEnter: this.getAnimationEnter(spec, 'barBack', totalDuration), + animationExit: this.getAnimationExit(spec, totalDuration), + animationAppear: this.getAnimationEnter(spec, 'barBack', totalDuration) + }; + } + + generateDecorateHaloIcons(spec: any) { + const totalDuration = spec.animation.duration; + return spec.decorateHaloIcons.map((decorateHaloIcon: any) => { + return { + type: 'symbol', + dataId: 'data', + visible: true, + dataKey: DATA_KEY, + style: { + x: (datum: Datum, ctx: any) => { + return ctx.valueToX([datum[spec.xField]]); + }, + y: (datum: Datum, ctx: any) => { + return ctx.valueToY([datum[spec.yField]]) + ctx.yBandwidth() / 2; + }, + ...decorateHaloIcon.style, + visible: (datum: Datum) => { + if (datum[SUPPLY_DATA_KEY]) { + return false; + } + return decorateHaloIcon.style.visible; + } + }, + animation: Boolean(spec.animation), + animationEnter: this.getAnimationEnter(spec, 'symbol', totalDuration), + animationExit: this.getAnimationExit(spec, totalDuration), + animationAppear: this.getAnimationEnter(spec, 'symbol', totalDuration) + }; + }); + } + + generateRankingIcon(spec: any) { + const totalDuration = spec.animation.duration; + return { + type: 'symbol', + dataId: 'data', + visible: true, + dataKey: DATA_KEY, + style: { + x: (datum: Datum) => { + if (spec.labelLayout === 'bothEnd') { + return -( + NAME_LABEL_PADDING_RIGHT + + this.nameLabelTextMeasure.fullMeasure(datum[spec.yField]).width + + (spec.orderLabel.style.visible + ? NAME_ORDER_PADDING_RIGHT + this.orderLabelTextMeasure.fullMeasure(datum[ORDER_KEY]).width + : 0) + + NAME_SYMBOL_PADDING_RIGHT + ); + } + return CHART_PADDING_LEFT; + }, + y: (datum: Datum, ctx: any) => { + if (spec.labelLayout === 'bothEnd') { + return ctx.valueToY([datum[spec.yField]]) + ctx.yBandwidth() / 2; + } + return ( + ctx.valueToY([datum[spec.yField]]) + + ctx.yBandwidth() / 2 - + spec.bar.height / 2 - + LABEL_PADDING_BOTTOM - + Math.max( + this.nameLabelTextMeasure.fullMeasure(datum[spec.yField]).height, + this.orderLabelTextMeasure.fullMeasure(datum[ORDER_KEY]).height + ) / + 2 + ); + }, + ...spec.rankingIcon.style, + lineWidth: 0, + stroke: null, + visible: (datum: Datum) => { + if (datum[SUPPLY_DATA_KEY]) { + return false; + } + return spec.rankingIcon.style.visible; + } + }, + animation: Boolean(spec.animation), + animationEnter: this.getAnimationEnter(spec, 'text', totalDuration), + animationExit: this.getAnimationExit(spec, totalDuration), + animationAppear: this.getAnimationEnter(spec, 'text', totalDuration) + }; + } + + generateNameLabel(spec: any) { + const totalDuration = spec.animation.duration; + return { + type: 'text', + dataId: 'data', + dataKey: DATA_KEY, + style: { + text: (datum: Datum) => { + return datum[spec.yField]; + }, + x: () => { + if (spec.labelLayout === 'bothEnd') { + return -NAME_LABEL_PADDING_RIGHT; + } + return ( + (spec.rankingIcon.style.visible ? NAME_SYMBOL_PADDING_RIGHT + (spec.rankingIcon.style.size ?? 10) : 0) + + (spec.orderLabel.style.visible + ? NAME_ORDER_PADDING_RIGHT + this.getMaxDataLabelLens(spec, ORDER_KEY, this.orderLabelTextMeasure) + : 0) + ); + }, + y: (datum: Datum, ctx: any) => { + if (spec.labelLayout === 'bothEnd') { + return ctx.valueToY([datum[spec.yField]]) + ctx.yBandwidth() / 2; + } + return ctx.valueToY([datum[spec.yField]]) + ctx.yBandwidth() / 2 - spec.bar.height / 2 - LABEL_PADDING_BOTTOM; + }, + ...spec.nameLabel.style, + textAlign: spec.labelLayout === 'bothEnd' ? 'right' : 'left', + textBaseline: spec.labelLayout === 'bothEnd' ? 'middle' : 'bottom', + visible: (datum: Datum) => { + if (datum[SUPPLY_DATA_KEY]) { + return false; + } + return spec.nameLabel.style.visible; + } + }, + animation: Boolean(spec.animation), + animationEnter: this.getAnimationEnter(spec, 'text', totalDuration), + animationExit: this.getAnimationExit(spec, totalDuration), + animationAppear: this.getAnimationEnter(spec, 'text', totalDuration) + }; + } + + generateOrderLabel(spec: any) { + const totalDuration = spec.animation.duration; + return { + type: 'text', + dataId: 'data', + dataKey: DATA_KEY, + style: { + text: (datum: Datum) => datum[ORDER_KEY], + x: (datum: Datum) => { + if (spec.labelLayout === 'bothEnd') { + return -( + NAME_LABEL_PADDING_RIGHT + + this.nameLabelTextMeasure.fullMeasure(datum[spec.yField]).width + + NAME_ORDER_PADDING_RIGHT + ); + } else { + return spec.rankingIcon.style.visible ? NAME_SYMBOL_PADDING_RIGHT + (spec.rankingIcon.style.size ?? 10) : 0; + } + }, + y: (datum: Datum, ctx: any) => { + if (spec.labelLayout === 'bothEnd') { + return ctx.valueToY([datum[spec.yField]]) + ctx.yBandwidth() / 2; + } + return ctx.valueToY([datum[spec.yField]]) + ctx.yBandwidth() / 2 - spec.bar.height / 2 - LABEL_PADDING_BOTTOM; + }, + ...spec.orderLabel.style, + textAlign: spec.labelLayout === 'bothEnd' ? 'right' : 'left', + textBaseline: spec.labelLayout === 'bothEnd' ? 'middle' : 'bottom', + visible: (datum: Datum) => { + if (datum[SUPPLY_DATA_KEY]) { + return false; + } + return spec.orderLabel.style.visible; + } + }, + animation: Boolean(spec.animation), + animationEnter: this.getAnimationEnter(spec, 'text', totalDuration), + animationExit: this.getAnimationExit(spec, totalDuration), + animationAppear: this.getAnimationEnter(spec, 'text', totalDuration) + }; + } + + generateValueLabel(spec: any) { + const totalDuration = spec.animation.duration; + return { + type: 'text', + dataId: 'data', + visible: true, + dataKey: DATA_KEY, + style: { + text: (datum: Datum) => datum[spec.xField], + x: (datum: Datum, ctx: any) => { + if (spec.labelLayout === 'bothEnd') { + return ( + ctx.getRegion().getLayoutRect().width + + this.getMaxDataLabelLens(spec, spec.xField, this.nameLabelTextMeasure) + + VALUE_LABEL_PADDING_LEFT + ); + } else { + return ctx.getRegion().getLayoutRect().width; + } + }, + y: (datum: Datum, ctx: any) => { + if (spec.labelLayout === 'bothEnd') { + return ctx.valueToY([datum[spec.yField]]) + ctx.yBandwidth() / 2; + } + return ctx.valueToY([datum[spec.yField]]) + ctx.yBandwidth() / 2 - spec.bar.height / 2 - LABEL_PADDING_BOTTOM; + }, + ...spec.valueLabel.style, + textAlign: 'right', + textBaseline: spec.labelLayout === 'bothEnd' ? 'middle' : 'bottom', + visible: (datum: Datum) => { + if (datum[SUPPLY_DATA_KEY]) { + return false; + } + return spec.valueLabel.style.visible; + } + }, + animation: Boolean(spec.animation), + animationEnter: this.getAnimationEnter(spec, 'text', totalDuration), + animationExit: this.getAnimationExit(spec, totalDuration), + animationAppear: this.getAnimationEnter(spec, 'text', totalDuration) + }; + } + + transformPaddingSpec(spec: any) { + spec.padding = { + left: + spec.labelLayout === 'bothEnd' + ? NAME_LABEL_PADDING_RIGHT + + this.getMaxDataLabelLens(spec, spec.yField, this.nameLabelTextMeasure) + + (spec.orderLabel.style.visible + ? NAME_ORDER_PADDING_RIGHT + this.getMaxDataLabelLens(spec, ORDER_KEY, this.orderLabelTextMeasure) + : 0) + + (spec.rankingIcon.style.visible ? NAME_SYMBOL_PADDING_RIGHT + (spec.rankingIcon.style.size ?? 10) : 0) + + CHART_PADDING_LEFT + : CHART_PADDING_LEFT, + right: + spec.labelLayout === 'bothEnd' + ? VALUE_LABEL_PADDING_LEFT + + this.getMaxDataLabelLens(spec, spec.yField, this.valueLabelTextMeasure) + + CHART_PADDING_RIGHT + : CHART_PADDING_RIGHT + 10, + top: 0, + bottom: 0, + ...spec.padding + }; + } + + paginateDataArr = (spec: IRankingListSpec) => { + const { data: arr, scrollSize = 1, pageSize = 5 } = spec; + const result: { [key: string]: Datum[] } = {}; + let pageOrder = 0; + for (let i = 0; i < arr.length; i += scrollSize) { + pageOrder++; + result[`page${pageOrder}`] = arr.slice(i, i + pageSize); + if (i + pageSize - 1 >= arr.length - 1) { + arr.push( + ...Array.from({ length: i + pageSize - arr.length }, _ => { + return { + [spec.yField]: Math.random() * 100, + [spec.xField]: null, + [SUPPLY_DATA_KEY]: true + }; + }) + ); + break; + } + } + return { + orderCount: pageOrder, + result: result + }; + }; + + processRankingData = (spec: IRankingListSpec) => { + const result: any[] = []; + spec.data.forEach((datum, index) => (datum[ORDER_KEY] = index + 1 < 10 ? `0${index + 1}` : index + 1)); + const pagerData = this.paginateDataArr(spec).result; + const orderCount = this.paginateDataArr(spec).orderCount; + const supplyCount = spec.pageSize - pagerData[`page${orderCount}`].length; + pagerData[`page${orderCount}`].push( + ...Array.from({ length: supplyCount }, _ => { + return { + [spec.yField]: Math.random() * 100, + [spec.xField]: null, + [SUPPLY_DATA_KEY]: true + }; + }) + ); + + Object.keys(pagerData).forEach(order => { + result.push({ + data: [ + { + id: 'datas', + values: pagerData[order].map((d, i) => { + return { ...d, [DATA_KEY]: order + '_' + i + '_' + new Date().getTime() }; + }) + }, + { + id: 'order', + values: [ + { + order + } + ] + } + ] + }); + }); + return result; + }; + + getMaxDataLabelLens(spec: IRankingListSpec, field: string, textMeasure: TextMeasure) { + const textWidths = this.originalData.map(datum => + datum[SUPPLY_DATA_KEY] ? 0 : textMeasure.fullMeasure(datum[field]).width + ); + return Math.max(...textWidths); + } + + getLabelWidth(padding: number, width: number) { + return width + padding; + } + + getAnimationExit(spec: IRankingListSpec, duration: number) { + if (spec.animation.type === 'grow') { + return {}; + } + return { + type: 'moveOut', + options: { + direction: 'y', + orient: 'negative', + point: (datum: Datum, element: IElement, opt: IAnimationParameters) => { + const channelAttr = element.getGraphicAttribute('y'); + const barSpace = (spec.height / spec.pageSize - spec.bar.height) / 2; + return { y: channelAttr - opt.height + barSpace }; + } + }, + duration: spec.animation.type === 'both' ? duration / 2 : duration, + easing: spec.animation.easing + }; + } + + getAnimationEnter(spec: IRankingListSpec, markType: 'rect' | 'text' | 'symbol' | 'barBack', totalDuration: number) { + const { animation } = spec; + const { type: animationType, easing } = animation; + const scrollDuration = animationType === 'both' ? totalDuration / 2 : totalDuration; + const growDuration = animationType === 'grow' ? totalDuration : totalDuration / 2; + const result = []; + if (animationType === 'scroll' || animationType === 'both') { + result.push({ + type: 'moveIn', + options: { + direction: 'y', + orient: 'negative', + excludeChannels: ['y'], + point: (datum: Datum, element: IElement, opt: IAnimationParameters) => { + const channelAttr = element.getGraphicAttribute('y'); + const barSpace = (spec.height / spec.pageSize - spec.bar.height) / 2; + return { y: channelAttr + opt.height - barSpace }; + } + }, + duration: scrollDuration, + easing + }); + } + if ((animationType === 'grow' || animationType === 'both') && markType !== 'text' && markType !== 'barBack') { + result.push({ + channel: { + x: { + from: 0, + to: (datum: Datum, element: IElement) => { + return element.getGraphicItem().attribute.x; + } + } + }, + duration: growDuration, + delay: animationType === 'both' ? scrollDuration : 0, + easing + }); + } + return result; + } +} diff --git a/packages/vchart-extension/src/charts/ranking-list/ranking-list.ts b/packages/vchart-extension/src/charts/ranking-list/ranking-list.ts new file mode 100644 index 0000000000..5b9c9020b2 --- /dev/null +++ b/packages/vchart-extension/src/charts/ranking-list/ranking-list.ts @@ -0,0 +1,41 @@ +import { IRankingListSpec } from './interface'; +import { VChart, BaseChart, BarChart } from '@visactor/vchart'; +import { RankingListChartSpecTransformer } from './ranking-list-transformer'; + +export class RankingList extends BaseChart> { + type = 'rankingList'; + static type = 'rankingList'; + static readonly view: string = 'singleDefault'; + + declare _spec: IRankingListSpec; + + static readonly transformerConstructor = RankingListChartSpecTransformer; + readonly transformerConstructor = RankingListChartSpecTransformer; + + init() { + if (!this.isValid()) { + return; + } + super.init(); + } + + protected isValid() { + const { xField, yField, data } = this._spec; + if (!xField || !yField) { + this._option.onError?.('Missing Required Config: `xField`, `yField` '); + return false; + } + if (!data) { + this._option.onError?.('Data is required'); + return false; + } + return true; + } +} + +export const registerRankingList = (option?: { VChart?: typeof VChart }) => { + const vchartConstructor = option?.VChart || VChart; + if (vchartConstructor) { + vchartConstructor.useChart([RankingList, BarChart]); + } +}; diff --git a/packages/vchart-extension/src/charts/ranking-list/utils.ts b/packages/vchart-extension/src/charts/ranking-list/utils.ts new file mode 100644 index 0000000000..ddbe55f9cb --- /dev/null +++ b/packages/vchart-extension/src/charts/ranking-list/utils.ts @@ -0,0 +1,56 @@ +import { Datum } from '@visactor/vchart/src/typings'; + +export const applyVisible = (spec, keyList: string[]) => { + keyList.forEach(key => { + spec[key] = { + ...spec[key], + style: { + ...spec[key]?.style, + visible: spec[key]?.style?.visible ?? spec[key]?.visible ?? true + } + }; + }); +}; + +export const mergeObjects = (objA, objB) => { + function recursiveMerge(target, source) { + for (const key in source) { + if (typeof source[key] === 'object' && source[key] !== null) { + if (!target[key]) { + target[key] = Array.isArray(source[key]) ? [] : {}; + } + recursiveMerge(target[key], source[key]); + } else if (!target.hasOwnProperty(key)) { + target[key] = source[key]; + } + } + return target; + } + return recursiveMerge(objA, objB); +}; + +export const computeDataRange = (data: Datum[], field: string) => { + let dataMin, dataMax; + const datumX = data.map(d => d[field]).filter(d => typeof d !== 'undefined' && d !== null); + + // 避免数据都为null, 即xField都为null, 导致scale异常, 图表为空 + // 这里只要设置dataMin和dataMax为任意数字并保证其不想等, 即可达到只显示yField而不显示xField的效果 + if (datumX.length === 0) { + dataMin = 0; + dataMax = 1; + } else { + dataMin = Math.min(...datumX) - (Math.max(...datumX) - Math.min(...datumX)) / 3; + dataMax = (Math.max(...datumX) - dataMin) / 0.8 + dataMin; + const delta_value = 10; // 可以是任意值, 只要大于0就行, 目的是为了让最小值和最大值不一样, 便于scale做插值计算 + const data = dataMin; + if (dataMin === dataMax) { + // 避免domain[0] = domain[1], 导致scale映射有问题 + // 数学计算: + // 1.保证 (dataMax - data) / (data - dataMin) = 4 + // 2. dataMax > data & dataMin < data => delta_value > 0 + dataMin = data - delta_value; + dataMax = (4 * data + delta_value) / 4; + } + } + return { min: dataMin, max: dataMax }; +}; diff --git a/packages/vchart-extension/src/index.ts b/packages/vchart-extension/src/index.ts index 5d7bbec5c1..e5baf35301 100644 --- a/packages/vchart-extension/src/index.ts +++ b/packages/vchart-extension/src/index.ts @@ -1,4 +1,5 @@ export * from './charts/ranking-bar/ranking-bar'; +export * from './charts/ranking-list/ranking-list'; export * from './charts/conversion-funnel'; export * from './components/series-break';