From b75ca464c2180e49d5067e57fd2f9fbd7bb8bf01 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Thu, 2 Jan 2025 23:13:34 +0800 Subject: [PATCH 1/6] feat: add ranking list extension chart --- common/config/rush/pnpm-lock.yaml | 6 + .../browser/test-page/ranking-list-2.ts | 346 +++++++++++ .../browser/test-page/ranking-list-3.ts | 395 ++++++++++++ .../runtime/browser/test-page/ranking-list.ts | 314 ++++++++++ packages/vchart-extension/package.json | 3 +- .../src/charts/ranking-list/constant.ts | 65 ++ .../src/charts/ranking-list/interface.ts | 123 ++++ .../ranking-list/ranking-list-transformer.ts | 573 ++++++++++++++++++ .../src/charts/ranking-list/ranking-list.ts | 41 ++ .../src/charts/ranking-list/utils.ts | 56 ++ packages/vchart-extension/src/index.ts | 1 + 11 files changed, 1922 insertions(+), 1 deletion(-) create mode 100644 packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list-2.ts create mode 100644 packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list-3.ts create mode 100644 packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list.ts create mode 100644 packages/vchart-extension/src/charts/ranking-list/constant.ts create mode 100644 packages/vchart-extension/src/charts/ranking-list/interface.ts create mode 100644 packages/vchart-extension/src/charts/ranking-list/ranking-list-transformer.ts create mode 100644 packages/vchart-extension/src/charts/ranking-list/ranking-list.ts create mode 100644 packages/vchart-extension/src/charts/ranking-list/utils.ts 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'; From 0cfe45e1fb56beb53b810a4c33fdf5d0a451bfe4 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Fri, 3 Jan 2025 01:43:30 +0800 Subject: [PATCH 2/6] feat: add formatMethod for label --- .../runtime/browser/test-page/ranking-list.ts | 12 ++++++ .../src/charts/ranking-list/interface.ts | 6 +-- .../ranking-list/ranking-list-transformer.ts | 42 +++++++++++++------ packages/vchart/src/index.ts | 1 + 4 files changed, 46 insertions(+), 15 deletions(-) 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 index 212087d4f0..2bcd2154d5 100644 --- a/packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list.ts +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list.ts @@ -107,18 +107,30 @@ const spec = { visible: guiObject.orderLabelVisible, style: { fontSize: 20 + }, + formatMethod: (text, ctx) => { + console.log('ctx-order', ctx); + return `order${text}`; } }, nameLabel: { visible: true, style: { fontSize: 20 + }, + formatMethod: (text, ctx) => { + console.log('ctx-name', ctx); + return `name${text}`; } }, valueLabel: { visible: true, style: { fontSize: 20 + }, + formatMethod: (text, ctx) => { + console.log('ctx-value', ctx); + return `value${text}`; } }, decorateHaloIcons: [ diff --git a/packages/vchart-extension/src/charts/ranking-list/interface.ts b/packages/vchart-extension/src/charts/ranking-list/interface.ts index e85589436e..bf674032b2 100644 --- a/packages/vchart-extension/src/charts/ranking-list/interface.ts +++ b/packages/vchart-extension/src/charts/ranking-list/interface.ts @@ -70,7 +70,7 @@ export interface IRankingListSpec { orderLabel?: { visible?: boolean; style?: ITextGraphicAttribute; - formatMethod?: (text: string, ctx: any) => string; + formatMethod?: (text: string, datum: Datum) => string; }; /** * 名称标签(yField对应的标签) @@ -78,7 +78,7 @@ export interface IRankingListSpec { nameLabel?: { visible?: boolean; style?: ITextGraphicAttribute; - formatMethod?: (text: string, ctx: any) => string; + formatMethod?: (text: string, datum: Datum) => string; }; /** * 值标签(xField对应的标签) @@ -86,7 +86,7 @@ export interface IRankingListSpec { valueLabel?: { visible?: boolean; style?: ITextGraphicAttribute; - formatMethod?: (text: string, ctx: any) => string; + formatMethod?: (text: string, datum: Datum) => string; }; /** * 每页行数 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 index 7ad2c86909..81b62d1d44 100644 --- a/packages/vchart-extension/src/charts/ranking-list/ranking-list-transformer.ts +++ b/packages/vchart-extension/src/charts/ranking-list/ranking-list-transformer.ts @@ -24,11 +24,13 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer protected orderLabelTextMeasure: TextMeasure; protected originalData: Datum[]; protected dataSpecs: any[]; + protected formatMap: { [key: string]: (text: string, ctx: any) => string } = {}; transformSpec(spec: any): void { super.transformSpec(spec); this.normalizeSpec(spec); this.upgradeTextMeasure(spec); + this.upgradeFormatMap(spec); this.processData(spec); // rankingList spec -> vchart spec @@ -51,6 +53,8 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer this.generateValueLabel(spec) ]; + this.transformPaddingSpec(spec); + super.transformSpec(spec); } @@ -84,6 +88,12 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer }); } + upgradeFormatMap(spec: any) { + this.formatMap[spec.yField] = spec.nameLabel.formatMethod; + this.formatMap[spec.xField] = spec.valueLabel.formatMethod; + this.formatMap[ORDER_KEY] = spec.orderLabel.formatMethod; + } + processData(spec: any) { // ps: 如果updateSpec后, 同时执行2次processData会有问题, 在这里用比较hack的方式绕过第2次 if (!spec.data[0]?.values) { @@ -109,7 +119,7 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer style: { ...spec.bar?.style, x1: 0, - visible: datum => { + visible: (datum: Datum) => { if (datum[SUPPLY_DATA_KEY]) { return false; } @@ -239,9 +249,10 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer if (spec.labelLayout === 'bothEnd') { return -( NAME_LABEL_PADDING_RIGHT + - this.nameLabelTextMeasure.fullMeasure(datum[spec.yField]).width + + this.nameLabelTextMeasure.fullMeasure(this.formatDatum(spec.yField, datum)).width + (spec.orderLabel.style.visible - ? NAME_ORDER_PADDING_RIGHT + this.orderLabelTextMeasure.fullMeasure(datum[ORDER_KEY]).width + ? NAME_ORDER_PADDING_RIGHT + + this.orderLabelTextMeasure.fullMeasure(this.formatDatum(ORDER_KEY, datum)).width : 0) + NAME_SYMBOL_PADDING_RIGHT ); @@ -258,8 +269,8 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer spec.bar.height / 2 - LABEL_PADDING_BOTTOM - Math.max( - this.nameLabelTextMeasure.fullMeasure(datum[spec.yField]).height, - this.orderLabelTextMeasure.fullMeasure(datum[ORDER_KEY]).height + this.nameLabelTextMeasure.fullMeasure(this.formatDatum(spec.yField, datum)).height, + this.orderLabelTextMeasure.fullMeasure(this.formatDatum(ORDER_KEY, datum)).height ) / 2 ); @@ -288,9 +299,7 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer dataId: 'data', dataKey: DATA_KEY, style: { - text: (datum: Datum) => { - return datum[spec.yField]; - }, + text: (datum: Datum) => this.formatDatum(spec.yField, datum), x: () => { if (spec.labelLayout === 'bothEnd') { return -NAME_LABEL_PADDING_RIGHT; @@ -332,12 +341,12 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer dataId: 'data', dataKey: DATA_KEY, style: { - text: (datum: Datum) => datum[ORDER_KEY], + text: (datum: Datum) => this.formatDatum(ORDER_KEY, datum), x: (datum: Datum) => { if (spec.labelLayout === 'bothEnd') { return -( NAME_LABEL_PADDING_RIGHT + - this.nameLabelTextMeasure.fullMeasure(datum[spec.yField]).width + + this.nameLabelTextMeasure.fullMeasure(this.formatDatum(spec.yField, datum)).width + NAME_ORDER_PADDING_RIGHT ); } else { @@ -375,7 +384,7 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer visible: true, dataKey: DATA_KEY, style: { - text: (datum: Datum) => datum[spec.xField], + text: (datum: Datum) => this.formatDatum(spec.xField, datum), x: (datum: Datum, ctx: any) => { if (spec.labelLayout === 'bothEnd') { return ( @@ -501,11 +510,20 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer getMaxDataLabelLens(spec: IRankingListSpec, field: string, textMeasure: TextMeasure) { const textWidths = this.originalData.map(datum => - datum[SUPPLY_DATA_KEY] ? 0 : textMeasure.fullMeasure(datum[field]).width + datum[SUPPLY_DATA_KEY] ? 0 : textMeasure.fullMeasure(this.formatDatum(field, datum)).width ); return Math.max(...textWidths); } + formatDatum(field: string, datum: Datum) { + // console.log('field', field, datum); + if (this.formatMap?.[field]) { + return this.formatMap[field](datum[field], datum); + } else { + return datum[field]; + } + } + getLabelWidth(padding: number, width: number) { return width + padding; } diff --git a/packages/vchart/src/index.ts b/packages/vchart/src/index.ts index bf511139d9..29048362bb 100644 --- a/packages/vchart/src/index.ts +++ b/packages/vchart/src/index.ts @@ -7,6 +7,7 @@ export * from './core'; // chart model for extension export * from './chart'; export * from './chart/base'; +export * from './chart/common'; export * from './series'; export * from './mark'; export * from './component'; From f6b608b192c98fe145e8dc8a28dbcbb3b21d28da Mon Sep 17 00:00:00 2001 From: skie1997 Date: Fri, 3 Jan 2025 18:27:37 +0800 Subject: [PATCH 3/6] fix: animation false not work --- .../runtime/browser/test-page/ranking-list-2.ts | 4 +--- .../runtime/browser/test-page/ranking-list-3.ts | 4 +--- .../runtime/browser/test-page/ranking-list.ts | 4 +--- .../vchart-extension/src/charts/ranking-list/utils.ts | 10 +++++----- 4 files changed, 8 insertions(+), 14 deletions(-) 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 index 0ab9b79e37..ee0827146e 100644 --- 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 @@ -1,7 +1,5 @@ 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 = { @@ -196,7 +194,7 @@ const spec = { const run = () => { registerRankingList(); - const cs = new VChart(merge(defaultSpec, spec), { + const cs = new VChart(spec, { dom: document.getElementById('chart') as HTMLElement, //theme: 'dark', onError: err => { 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 index 85d4325438..939e24d713 100644 --- 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 @@ -1,7 +1,5 @@ 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 = { @@ -245,7 +243,7 @@ const spec = { const run = () => { registerRankingList(); - const cs = new VChart(merge(defaultSpec, spec), { + const cs = new VChart(spec, { dom: document.getElementById('chart') as HTMLElement, //theme: 'dark', onError: err => { 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 index 2bcd2154d5..cadcc4a333 100644 --- a/packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list.ts +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list.ts @@ -1,7 +1,5 @@ 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 = { @@ -176,7 +174,7 @@ const spec = { const run = () => { registerRankingList(); - const cs = new VChart(merge(defaultSpec, spec), { + const cs = new VChart(spec, { dom: document.getElementById('chart') as HTMLElement, //theme: 'dark', onError: err => { diff --git a/packages/vchart-extension/src/charts/ranking-list/utils.ts b/packages/vchart-extension/src/charts/ranking-list/utils.ts index ddbe55f9cb..94a417b446 100644 --- a/packages/vchart-extension/src/charts/ranking-list/utils.ts +++ b/packages/vchart-extension/src/charts/ranking-list/utils.ts @@ -1,6 +1,6 @@ import { Datum } from '@visactor/vchart/src/typings'; -export const applyVisible = (spec, keyList: string[]) => { +export const applyVisible = (spec: any, keyList: string[]) => { keyList.forEach(key => { spec[key] = { ...spec[key], @@ -12,15 +12,15 @@ export const applyVisible = (spec, keyList: string[]) => { }); }; -export const mergeObjects = (objA, objB) => { - function recursiveMerge(target, source) { +export const mergeObjects = (objA: any, objB: any) => { + function recursiveMerge(target: any, source: any) { for (const key in source) { if (typeof source[key] === 'object' && source[key] !== null) { - if (!target[key]) { + if (!target.hasOwnProperty(key)) { target[key] = Array.isArray(source[key]) ? [] : {}; } recursiveMerge(target[key], source[key]); - } else if (!target.hasOwnProperty(key)) { + } else if (!target.hasOwnProperty(key) && typeof target === 'object') { target[key] = source[key]; } } From 13eb9984f8475768754281f283f9d7e900dc54a4 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Sat, 4 Jan 2025 02:14:52 +0800 Subject: [PATCH 4/6] feat: make sure animation effect when one page --- .../ranking-list/ranking-list-transformer.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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 index 81b62d1d44..e6d81df6fc 100644 --- a/packages/vchart-extension/src/charts/ranking-list/ranking-list-transformer.ts +++ b/packages/vchart-extension/src/charts/ranking-list/ranking-list-transformer.ts @@ -505,6 +505,29 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer ] }); }); + + // 只有1页时, player循环播放时, prePage和curPage data一致, 导致没有动画效果 + // 在此手动复制1页, 且prePage和curPage dataKey不一致, 保证动画效果 + if (result.length === 1) { + result.push({ + data: [ + { + id: 'datas', + values: pagerData['page1'].map((d, i) => { + return { ...d, [DATA_KEY]: 'page2' + '_' + i + '_' + new Date().getTime() }; + }) + }, + { + id: 'order', + values: [ + { + order: 'page2' + } + ] + } + ] + }); + } return result; }; From 32a1a40256c22cbb0c09123137a3fb4c238bb78e Mon Sep 17 00:00:00 2001 From: skie1997 Date: Tue, 7 Jan 2025 00:40:55 +0800 Subject: [PATCH 5/6] feat: add highlight effect --- .../runtime/browser/test-page/ranking-list.ts | 44 ++++++++++++++++++- .../src/charts/ranking-list/interface.ts | 16 +++++++ .../ranking-list/ranking-list-transformer.ts | 16 ++++++- 3 files changed, 74 insertions(+), 2 deletions(-) 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 index cadcc4a333..c4d3d32c34 100644 --- a/packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list.ts +++ b/packages/vchart-extension/__tests__/runtime/browser/test-page/ranking-list.ts @@ -86,6 +86,11 @@ const spec = { } ] } + }, + state: { + blur: { + opacity: 0.2 + } } }, labelLayout: guiObject.labelLayout, @@ -99,6 +104,11 @@ const spec = { // stroke: 'yellow', // symbolType: // '' + }, + state: { + blur: { + opacity: 0.2 + } } }, orderLabel: { @@ -109,6 +119,11 @@ const spec = { formatMethod: (text, ctx) => { console.log('ctx-order', ctx); return `order${text}`; + }, + state: { + blur: { + opacity: 0.2 + } } }, nameLabel: { @@ -119,6 +134,11 @@ const spec = { formatMethod: (text, ctx) => { console.log('ctx-name', ctx); return `name${text}`; + }, + state: { + blur: { + opacity: 0.2 + } } }, valueLabel: { @@ -129,6 +149,11 @@ const spec = { formatMethod: (text, ctx) => { console.log('ctx-value', ctx); return `value${text}`; + }, + state: { + blur: { + opacity: 0.2 + } } }, decorateHaloIcons: [ @@ -138,6 +163,11 @@ const spec = { symbolType: 'circle', size: 8, fill: 'rgba(255,255,255,0.5)' + }, + state: { + blur: { + opacity: 0.2 + } } }, { @@ -148,6 +178,11 @@ const spec = { lineWidth: 1, stroke: 'rgba(255,255,255,0.8)', fill: 'rgba(255,255,255,0.5)' + }, + state: { + blur: { + opacity: 0.2 + } } } ], @@ -168,7 +203,14 @@ const spec = { // fill: 'red', symbolType: 'rect' } - } + }, + interactions: [ + { + type: 'element-highlight-by-key', + trigger: ['pointerdown'], + triggerOff: null + } + ] // animation: false }; diff --git a/packages/vchart-extension/src/charts/ranking-list/interface.ts b/packages/vchart-extension/src/charts/ranking-list/interface.ts index bf674032b2..7427d74650 100644 --- a/packages/vchart-extension/src/charts/ranking-list/interface.ts +++ b/packages/vchart-extension/src/charts/ranking-list/interface.ts @@ -1,3 +1,5 @@ +import { IMarkStateSpec, IMarkStateStyleSpec } from '@visactor/vchart'; +import { StateValue } from '@visactor/vchart/src/compile/mark'; import { Datum } from '@visactor/vchart/src/typings/common'; import { ITextGraphicAttribute, @@ -38,6 +40,7 @@ export interface IRankingListSpec { bar?: { height?: number; style?: IRectGraphicAttribute; + state?: Record | IMarkStateStyleSpec>; }; /** * 柱图背景 @@ -46,6 +49,11 @@ export interface IRankingListSpec { visible?: boolean; type?: string; style?: ISymbolGraphicAttribute | IRectGraphicAttribute; + state?: Record< + StateValue, + | IMarkStateSpec + | IMarkStateStyleSpec + >; }; /** * 排名图标 @@ -53,6 +61,7 @@ export interface IRankingListSpec { rankingIcon?: { visible?: boolean; style?: ISymbolGraphicAttribute; + state?: Record | IMarkStateStyleSpec>; }; /** * 装饰图元 @@ -62,6 +71,10 @@ export interface IRankingListSpec { visible?: boolean; // type?: 'circle' | 'square' | 'emptyCircle' | 'diamond' | 'halo' | 'concentric' | 'custom'; style?: ISymbolGraphicAttribute; + state?: Record< + StateValue, + IMarkStateSpec | IMarkStateStyleSpec + >; } ]; /** @@ -71,6 +84,7 @@ export interface IRankingListSpec { visible?: boolean; style?: ITextGraphicAttribute; formatMethod?: (text: string, datum: Datum) => string; + state?: Record | IMarkStateStyleSpec>; }; /** * 名称标签(yField对应的标签) @@ -79,6 +93,7 @@ export interface IRankingListSpec { visible?: boolean; style?: ITextGraphicAttribute; formatMethod?: (text: string, datum: Datum) => string; + state?: Record | IMarkStateStyleSpec>; }; /** * 值标签(xField对应的标签) @@ -87,6 +102,7 @@ export interface IRankingListSpec { visible?: boolean; style?: ITextGraphicAttribute; formatMethod?: (text: string, datum: Datum) => string; + state?: Record | IMarkStateStyleSpec>; }; /** * 每页行数 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 index e6d81df6fc..338ecf61e3 100644 --- a/packages/vchart-extension/src/charts/ranking-list/ranking-list-transformer.ts +++ b/packages/vchart-extension/src/charts/ranking-list/ranking-list-transformer.ts @@ -178,6 +178,7 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer visible: true, dataKey: DATA_KEY, zIndex: -99, + state: spec.barBackground?.state, style: { x: (datum: Datum, ctx: any) => spec.barBackground.type === 'symbol' ? ctx.getRegion().getLayoutRect().width / 2 : 0, @@ -214,8 +215,12 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer dataId: 'data', visible: true, dataKey: DATA_KEY, + state: decorateHaloIcon?.state, style: { x: (datum: Datum, ctx: any) => { + if (datum[spec.xField] === undefined || datum[spec.xField] === null) { + return undefined; + } return ctx.valueToX([datum[spec.xField]]); }, y: (datum: Datum, ctx: any) => { @@ -244,6 +249,7 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer dataId: 'data', visible: true, dataKey: DATA_KEY, + state: spec.rankingIcon.state, style: { x: (datum: Datum) => { if (spec.labelLayout === 'bothEnd') { @@ -298,6 +304,7 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer type: 'text', dataId: 'data', dataKey: DATA_KEY, + state: spec.nameLabel?.state, style: { text: (datum: Datum) => this.formatDatum(spec.yField, datum), x: () => { @@ -340,6 +347,7 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer type: 'text', dataId: 'data', dataKey: DATA_KEY, + state: spec.orderLabel?.state, style: { text: (datum: Datum) => this.formatDatum(ORDER_KEY, datum), x: (datum: Datum) => { @@ -383,6 +391,11 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer dataId: 'data', visible: true, dataKey: DATA_KEY, + state: { + blur: { + opacity: 0.2 + } + }, style: { text: (datum: Datum) => this.formatDatum(spec.xField, datum), x: (datum: Datum, ctx: any) => { @@ -420,6 +433,7 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer } transformPaddingSpec(spec: any) { + const maxHaloIconSize = Math.max(...spec.decorateHaloIcons.map((icon: any) => icon.style.size ?? 18)); spec.padding = { left: spec.labelLayout === 'bothEnd' @@ -430,7 +444,7 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer : 0) + (spec.rankingIcon.style.visible ? NAME_SYMBOL_PADDING_RIGHT + (spec.rankingIcon.style.size ?? 10) : 0) + CHART_PADDING_LEFT - : CHART_PADDING_LEFT, + : CHART_PADDING_LEFT + maxHaloIconSize / 2, right: spec.labelLayout === 'bothEnd' ? VALUE_LABEL_PADDING_LEFT + From d83b1420e7c6085bf9f251c4a0ee249f614b1a02 Mon Sep 17 00:00:00 2001 From: skie1997 Date: Wed, 8 Jan 2025 01:50:32 +0800 Subject: [PATCH 6/6] fix: decorateHaloIcons empty and value label textAlign problem --- .../ranking-list/ranking-list-transformer.ts | 12 ++++++--- .../src/charts/ranking-list/utils.ts | 27 ++++++++++++++----- 2 files changed, 28 insertions(+), 11 deletions(-) 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 index 338ecf61e3..c6ebcc3169 100644 --- a/packages/vchart-extension/src/charts/ranking-list/ranking-list-transformer.ts +++ b/packages/vchart-extension/src/charts/ranking-list/ranking-list-transformer.ts @@ -54,6 +54,7 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer ]; this.transformPaddingSpec(spec); + // console.log('original-spec', spec); super.transformSpec(spec); } @@ -64,7 +65,7 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer applyVisible(spec, [ 'barBackground', 'rankingIcon', - 'decorateHaloIcon', + 'decorateHaloIcons', 'orderLabel', 'nameLabel', 'valueLabel' @@ -209,7 +210,7 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer generateDecorateHaloIcons(spec: any) { const totalDuration = spec.animation.duration; - return spec.decorateHaloIcons.map((decorateHaloIcon: any) => { + return spec.decorateHaloIcons?.map((decorateHaloIcon: any) => { return { type: 'symbol', dataId: 'data', @@ -416,7 +417,7 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer return ctx.valueToY([datum[spec.yField]]) + ctx.yBandwidth() / 2 - spec.bar.height / 2 - LABEL_PADDING_BOTTOM; }, ...spec.valueLabel.style, - textAlign: 'right', + textAlign: spec.labelLayout === 'bothEnd' ? 'left' : 'right', textBaseline: spec.labelLayout === 'bothEnd' ? 'middle' : 'bottom', visible: (datum: Datum) => { if (datum[SUPPLY_DATA_KEY]) { @@ -433,7 +434,10 @@ export class RankingListChartSpecTransformer extends CommonChartSpecTransformer } transformPaddingSpec(spec: any) { - const maxHaloIconSize = Math.max(...spec.decorateHaloIcons.map((icon: any) => icon.style.size ?? 18)); + const maxHaloIconSize = + spec.decorateHaloIcons.length > 0 + ? Math.max(...spec.decorateHaloIcons.map((icon: any) => icon.style?.size ?? 18)) + : 0; spec.padding = { left: spec.labelLayout === 'bothEnd' diff --git a/packages/vchart-extension/src/charts/ranking-list/utils.ts b/packages/vchart-extension/src/charts/ranking-list/utils.ts index 94a417b446..f5128bb85a 100644 --- a/packages/vchart-extension/src/charts/ranking-list/utils.ts +++ b/packages/vchart-extension/src/charts/ranking-list/utils.ts @@ -1,14 +1,27 @@ import { Datum } from '@visactor/vchart/src/typings'; +import { isArray } from '@visactor/vutils'; export const applyVisible = (spec: any, keyList: string[]) => { keyList.forEach(key => { - spec[key] = { - ...spec[key], - style: { - ...spec[key]?.style, - visible: spec[key]?.style?.visible ?? spec[key]?.visible ?? true - } - }; + if (isArray(spec[key])) { + spec[key].forEach((s, i) => { + spec[key][i] = { + ...s, + style: { + ...s?.style, + visible: s?.style?.visible ?? s?.visible ?? true + } + }; + }); + } else { + spec[key] = { + ...spec[key], + style: { + ...spec[key]?.style, + visible: spec[key]?.style?.visible ?? spec[key]?.visible ?? true + } + }; + } }); };