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';