diff --git a/__tests__/unit/plots/multi-view/index-spec.ts b/__tests__/unit/plots/multi-view/index-spec.ts index 23d5582ac8..76c0f9bb02 100644 --- a/__tests__/unit/plots/multi-view/index-spec.ts +++ b/__tests__/unit/plots/multi-view/index-spec.ts @@ -185,6 +185,30 @@ describe('multi-view', () => { expect(plot.chart.views[0].geometries[0].animateOption.appear.animation).toBe('fade-in'); }); + it('plots 支持配置在顶层', () => { + const data = [ + { date: '03-01', sales: 100 }, + { date: '03-02', sales: 95 }, + { date: '03-03', sales: 69 }, + ]; + + const plot = new Mix(createDiv(), { + data, + // 共享 slider + slider: {}, + plots: [ + { type: 'line', top: true, options: { xField: 'date', yField: 'sales', color: 'red' } }, + { type: 'column', top: true, options: { xField: 'date', yField: 'sales', color: 'date' } }, + ], + }); + + plot.render(); + + expect(plot.chart.geometries.length).toBe(2); + const line = plot.chart.geometries[0]; + expect(line.getAttribute('color').values).toEqual(['red']); + }); + it('MultiView 依然可以使用', () => { const data = partySupport.filter((o) => ['FF', 'Lab'].includes(o.type)); const line = new MultiView(createDiv(), { diff --git a/docs/api/advanced-plots/mix.zh.md b/docs/api/advanced-plots/mix.zh.md index b36a878b41..92a4e986d1 100644 --- a/docs/api/advanced-plots/mix.zh.md +++ b/docs/api/advanced-plots/mix.zh.md @@ -121,6 +121,32 @@ plots: [ ]; ``` +#### IPlot.top + +是否设置在顶层。设置为 true 时,几何图形挂在顶层 chart 对象上,而不是子 view 下。此时 region 设置无效,data 继承顶层 data 配置。 + +**示例**: + +```ts +const data = [{ date: '03-01', sales: 100 }, { date: '03-02', sales: 95 }, { date: '03-03', sales: 69 }]; +const plot = new Mix('container', { + data, + // 共享 slider + slider: {}, + plots: [ + { type: 'line', options: { xField: 'date', yField: 'sales', color: 'red' } }, + { type: 'column', options: { xField: 'date', yField: 'sales', color: 'date', } }, + ] +}); + +// 以上写法,等价于 G2 写法 +chart.data(data); +chart.line().position('date*sales').color('red'); +chart.interval().position('date*sales').color('date'); +chart.option('slider', {}); +``` + +更多见:[定制股票图](/zh/examples/plugin/multi-view#customized-stock) ### 其他 @@ -133,3 +159,11 @@ plots: [ #### legend 顶层 legend 配置(统一在 chart 层配置)。 + +#### slider + +顶层 slider 配置(顶层配置)。 + +#### annotations + +图表标注配置(顶层配置)。 diff --git a/examples/plugin/multi-view/demo/customized-stock.jsx b/examples/plugin/multi-view/demo/customized-stock.jsx new file mode 100644 index 0000000000..7612b1d698 --- /dev/null +++ b/examples/plugin/multi-view/demo/customized-stock.jsx @@ -0,0 +1,136 @@ +import React, { useState, useEffect } from 'react'; +import ReactDOM from 'react-dom'; +import { Mix } from '@antv/g2plot'; + +const DemoStock = () => { + const chartNodeRef = React.useRef(); + const chartRef = React.useRef(); + + const [data, setData] = useState([]); + + useEffect(() => { + asyncFetch(); + }, []); + + const asyncFetch = () => { + fetch('https://gw.alipayobjects.com/os/antfincdn/qtQ9nYfYJe/stock-data.json') + .then((response) => response.json()) + .then((json) => setData(json)) + .catch((error) => { + console.log('fetch data failed', error); + }); + }; + + const mixConfig = { + appendPadding: 8, + tooltip: { + shared: true, + showCrosshairs: true, + }, + syncViewPadding: true, + slider: {}, + data, + plots: [ + { + type: 'stock', + // 共享顶层 data + top: true, + options: { + xField: 'trade_date', + yField: ['open', 'close', 'high', 'low'], + }, + }, + { + type: 'line', + // 共享顶层 data + top: true, + options: { + xField: 'trade_date', + yField: 'amount', + yAxis: false, + yAxis: { + type: 'log', + grid: null, + line: null, + }, + meta: { + amount: { + alias: '平均租金(元)', + formatter: (v) => (v > 10000 ? `${(v / 10000).toFixed(0)}万` : `${v}`), + }, + }, + color: '#FACC14', + }, + }, + { + type: 'line', + top: true, + options: { + xField: 'trade_date', + yField: 'vol', + yAxis: false, + meta: { + vol: { + alias: '数值(元)', + }, + }, + color: '#5FB1EE', + }, + }, + ], + annotations: [ + { + type: 'dataMarker', + // 外部获取最大值的数据 + position: ['2020-03-05', 3074.2571], + top: true, + autoAdjust: false, + direction: 'upward', + text: { + content: '顶峰值', + style: { + fontSize: 13, + } + }, + line: { + length: 12, + }, + }, + { + type: 'dataMarker', + // 外部获取最大值的数据 + position: ['2020-03-13', 2799.9841], + top: true, + direction: 'downward', + text: { + content: '2799.9841', + style: { + fontSize: 12, + } + }, + point: null, + line: { + length: 12, + }, + } + ], + }; + + React.useEffect(() => { + const chartDom = chartNodeRef?.current; + if (!chartDom) return; + + let plot = chartRef.current; + if (!plot) { + plot = new Mix(chartDom, mixConfig); + plot.render(); + chartRef.current = plot; + } else { + plot.update(mixConfig); + } + }, [mixConfig]); + + return
; +}; + +ReactDOM.render(, document.getElementById('container')); diff --git a/examples/plugin/multi-view/demo/meta.json b/examples/plugin/multi-view/demo/meta.json index cdc49bf21c..c2f084a57b 100644 --- a/examples/plugin/multi-view/demo/meta.json +++ b/examples/plugin/multi-view/demo/meta.json @@ -36,14 +36,6 @@ }, "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/GMzV%24DSDAX/68a9190b-a4eb-4ad4-be3b-4fef31595a3f.png" }, - { - "filename": "nobel-prize.ts", - "title": { - "zh": "星空诺贝尔奖", - "en": "Noble Prize Visual" - }, - "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*Zi6cQJN9L7EAAAAAAAAAAAAAARQnAQ" - }, { "filename": "drinks.ts", "title": { @@ -75,6 +67,14 @@ "en": "Line Chart with Anomaly Detection Style" }, "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/9MkGXoBNaH/20220310212354.jpg" + }, + { + "filename": "customized-stock.jsx", + "title": { + "zh": "定制股票图", + "en": "Customized stock" + }, + "screenshot": "https://gw.alipayobjects.com/zos/antfincdn/%26P96Yg5fKh/78f5836e-efde-409d-a5b6-7dfe91cd2d04.png" } ] } diff --git a/examples/plugin/multi-view/demo/nobel-prize.ts b/examples/plugin/multi-view/demo/nobel-prize.ts deleted file mode 100644 index 58a5d578bf..0000000000 --- a/examples/plugin/multi-view/demo/nobel-prize.ts +++ /dev/null @@ -1,348 +0,0 @@ -// @ts-nocheck -import { DataView } from '@antv/data-set'; -import { Mix } from '@antv/g2plot'; -import { keys, groupBy } from '@antv/util'; - -function generateYearData() { - const r = []; - for (let i = 1900; i <= 2016; i++) { - r.push({ year: i }); - } - return r; -} - -fetch('https://gw.alipayobjects.com/os/antfincdn/NHer2zyRYE/nobel-prize-data.json') - .then((data) => data.json()) - .then((originalData) => { - const data = [...originalData.main, { year: 2016, number: 0, age: 0 }]; - let currentYear = 1901; - const types = keys(groupBy(data, (d) => d.type)); - - /** 散点图数据(各国诺贝尔奖获奖者的年龄分布) */ - const getPointViewData = (year) => { - const r = data.map((d) => (d.year <= year ? d : { ...d, ageGroup: null })).filter((d) => d.age !== 0); - const ds = new DataView().source(r); - ds.transform({ - type: 'summary', // 别名 summary - fields: ['number'], // 统计字段集 - operations: ['sum'], // 统计操作集 - as: ['number'], // 存储字段集 - groupBy: ['country', 'ageGroup', 'type'], // 分组字段集 - }); - return ds.rows; - }; - - /** 占比环图数据(各领域诺贝尔奖获奖分布) */ - const getIntervalViewData = (year) => { - const ds = new DataView().source(data.map((d) => (d.year <= year ? d : { ...d, number: 0 }))); - const othersCnt = data.filter((d) => d.year > year).reduce((a, b) => a + b.number, 0); - ds.transform({ - type: 'summary', // 别名 summary - fields: ['number'], // 统计字段集 - operations: ['sum'], // 统计操作集 - as: ['counts'], // 存储字段集 - groupBy: ['type'], // 分组字段集 - }); - return [...ds.rows, { type: '其它', counts: othersCnt }]; - }; - - const yearData = generateYearData(); - const plot = new Mix('container', { - height: 500, - padding: 'auto', - appendPadding: [20, 0, 20, 0], - legend: { - type: { position: 'bottom' }, - number: false, - }, - tooltip: { - fields: ['country', 'age', 'number', 'ageGroup'], - showMarkers: false, - domStyles: { - 'g2-tooltip': { - minWidth: '200px', - }, - 'g2-tooltip-list-item': { - display: 'flex', - justifyContent: 'space-between', - }, - }, - customContent: (title, items) => { - const datum = items[0]?.data || {}; - const fixed = (v) => v.toFixed(0); - const tooltipItems = [ - { name: '年龄段:', value: `${fixed(Math.max(datum.ageGroup - 10, 0))}~${fixed(datum.ageGroup + 10)}` }, - { name: '奖项学科:', value: datum.type }, - { name: '获奖人数:', value: datum.number }, - ]; - let tooltipItemsStr = ''; - tooltipItems.forEach((item) => { - tooltipItemsStr += `
  • - ${item.name} - ${item.value} -
  • `; - }); - return ` -
    ${title}
    - - `; - }, - }, - views: [ - { - data: getIntervalViewData(currentYear), - region: { start: { x: 0, y: 0.35 }, end: { x: 1, y: 0.65 } }, - coordinate: { - type: 'theta', - cfg: { innerRadius: 0.84, radius: 0.96 }, - }, - geometries: [ - { - type: 'interval', - yField: 'counts', - colorField: 'type', - mapping: { - color: ({ type }) => { - const idx = types.indexOf(type); - const { colors10 = [] } = plot.chart.getTheme(); - return colors10[idx] || '#D9D9D9'; - }, - }, - adjust: { type: 'stack' }, - }, - ], - annotations: [ - { - type: 'text', - content: 'G2Plot', - position: ['50%', '50%'], - style: { - textAlign: 'center', - fontWeight: 400, - fontSize: 28, - }, - }, - ], - }, - { - data: getPointViewData(currentYear), - region: { start: { x: 0, y: 0 }, end: { x: 1, y: 1 } }, - coordinate: { - type: 'polar', - cfg: { - innerRadius: 0.45, - radius: 0.64, - }, - }, - axes: { - country: { - tickLine: null, - label: null, - }, - ageGroup: { - tickLine: null, - min: 20, - max: 100, - tickInterval: 20, - title: { - text: '获奖\n年龄', - autoRotate: false, - offset: 12, - style: { fontSize: 10, textBaseline: 'bottom' }, - }, - label: { - offset: -4, - style: { - textBaseline: 'bottom', - fontSize: 10, - }, - formatter: (v) => (v === '100' ? '' : v), - }, - grid: { - line: { - style: { - lineWidth: 0.5, - }, - }, - }, - }, - }, - geometries: [ - { - type: 'point', - xField: 'country', - yField: 'ageGroup', - colorField: 'type', - sizeField: 'number', - adjust: { - type: 'dodge', - }, - mapping: { - size: [2, 8], - shape: 'circle', - style: { - fillOpacity: 0.65, - lineWidth: 0, - }, - }, - }, - { - // 国家标签 - type: 'interval', - xField: 'country', - label: { - labelEmit: true, - fields: ['country'], - offset: 50, - style: { - fontSize: 10, - }, - }, - mapping: { - color: 'transparent', - }, - }, - ], - }, - { - // 年度 label 展示 - data: yearData, - region: { - start: { x: 0.05, y: 0.05 }, - end: { x: 0.95, y: 0.95 }, - }, - axes: { - year: { - tickCount: 10, - label: null, - line: { - style: { - lineWidth: 0.5, - }, - }, - }, - }, - coordinate: { type: 'polar', cfg: { innerRadius: 0.99, radius: 1 } }, - geometries: [ - { - type: 'line', - xField: 'year', - label: { - labelEmit: true, - content: ({ year }) => { - if (year === 1900) { - return ''; - } - if (year === 2016) { - return ' ALL '; - } - return Number(year) % 10 === 0 ? year : '-'; - }, - }, - mapping: { - color: 'transparent', - }, - }, - { - type: 'interval', - xField: 'year', - label: { - labelEmit: true, - fields: ['year'], - callback: (year) => { - const { defaultColor } = plot.chart.getTheme(); - return { - style: { - fill: year === currentYear ? 'rgba(255,255,255,0.85)' : 'transparent', - }, - content: () => `${currentYear === 2016 ? ' ALL ' : currentYear}`, - background: { - padding: 2, - style: { - radius: 1, - fill: year === currentYear ? defaultColor : 'transparent', - }, - }, - }; - }, - }, - mapping: { - color: 'transparent', - }, - }, - ], - }, - ], - }); - - plot.render(); - - const view1 = plot.chart.views[0]; - const view2 = plot.chart.views[1]; - const view3 = plot.chart.views[2]; - // 根据 view3 中创建的顺序,可知 滑块对应的第 2 个几何标记对象 geometry - const sliderBlock = view3.geometries[1]; - function rerender(specYear) { - view1.changeData(getIntervalViewData(specYear)); - view2.changeData(getPointViewData(specYear)); - sliderBlock.label('year', (year) => { - const { defaultColor } = plot.chart.getTheme(); - return { - labelEmit: true, - style: { - fill: year === specYear ? 'rgba(255,255,255,0.85)' : 'transparent', - }, - content: () => `${specYear === 2016 ? ' ALL ' : specYear}`, - background: { - padding: 2, - style: { - radius: 1, - // 非当前年份,进行透明展示 - fill: year === specYear ? defaultColor : 'transparent', - }, - }, - }; - }); - // 传入参数 true,重新绘制,不重新触发更新流程。 - view3.render(true); - } - - let interval; - function start() { - clearInterval(interval); - interval = setInterval(() => { - if (currentYear++ < 2016) { - rerender(currentYear); - } else { - end(); - } - }, 800); - } - function end() { - clearInterval(interval); - interval = null; - } - - function handldSlideBlockClick(evt) { - const data = evt.data?.data; - if (data) { - if (typeof data?.year === 'number') { - currentYear = data.year; - rerender(currentYear); - } - } - } - // 监听 element click 事件,指定当前年份,并且启动轮播 - view3.on('element:click', (evt) => { - handldSlideBlockClick(evt); - start(); - }); - // 监听 element click 事件,指定当前年份,并且暂停轮播 - view3.on('element:dblclick', (evt) => { - handldSlideBlockClick(evt); - end(); - }); - - start(); - }); diff --git a/src/plots/mix/adaptor.ts b/src/plots/mix/adaptor.ts index 16644c47ef..08cfc1fa22 100644 --- a/src/plots/mix/adaptor.ts +++ b/src/plots/mix/adaptor.ts @@ -1,7 +1,7 @@ import { each } from '@antv/util'; import { Geometry } from '@antv/g2'; import { geometry as geometryAdaptor } from '../../adaptor/geometries/base'; -import { interaction, animation, theme, tooltip } from '../../adaptor/common'; +import { interaction, animation, theme, tooltip, annotation } from '../../adaptor/common'; import { Params } from '../../core/adaptor'; import { PLOT_CONTAINER_OPTIONS } from '../../core/plot'; import { AXIS_META_CONFIG_KEYS } from '../../constant'; @@ -122,24 +122,41 @@ function multiView(params: Params): Params { */ function multiPlot(params: Params): Params { const { chart, options } = params; - const { plots } = options; + const { plots, data = [] } = options; each(plots, (plot) => { - const { type, region, options = {} } = plot; + const { type, region, options = {}, top } = plot; const { tooltip } = options; + if (top) { + execPlotAdaptor(type, chart, { ...options, data }); + return; + } + const viewOfG2 = chart.createView({ region, ...pick(options, PLOT_CONTAINER_OPTIONS) }); if (tooltip) { // 配置 tooltip 交互 viewOfG2.interaction('tooltip'); } - execPlotAdaptor(type, viewOfG2, options); + execPlotAdaptor(type, viewOfG2, { data, ...options }); }); return params; } +/** + * 处理缩略轴的 adaptor (mix) + * @param params + */ +export function slider(params: Params): Params { + const { chart, options } = params; + + chart.option('slider', options.slider); + + return params; +} + /** * 图适配器 * @param chart @@ -153,7 +170,9 @@ export function adaptor(params: Params) { interaction, animation, theme, - tooltip + tooltip, + slider, + annotation() // ... 其他的 adaptor flow )(params); } diff --git a/src/plots/mix/types.ts b/src/plots/mix/types.ts index af3cb10c59..baad61ebf5 100644 --- a/src/plots/mix/types.ts +++ b/src/plots/mix/types.ts @@ -6,6 +6,7 @@ import { Geometry } from '../../adaptor/geometries/base'; import { Animation } from '../../types/animation'; import { Annotation } from '../../types/annotation'; import { Interaction } from '../../types/interaction'; +import { Slider } from '../../types/slider'; import { IPlotTypes } from './utils'; /** @@ -67,11 +68,22 @@ export type IView = { * @title 子 plot 的配置 */ export type IPlot = IPlotTypes & { + /** + * @title 数据 + * @description 设置画布具体的数据. 默认基础顶层 data + */ + readonly data?: Record[]; /** * @title plot view 的布局范围 * @default "占满全部" */ readonly region?: Region; + /** + * @title 是否为顶层 + * @description 设置为 true 时,几何图形挂在顶层 chart 对象上,而不是子 view 下。此时 region 设置无效,data 继承顶层 data 配置。 + * @default false + */ + readonly top?: boolean; }; /** @@ -79,6 +91,10 @@ export type IPlot = IPlotTypes & { */ export interface MixOptions extends Omit { + /** + * @title 顶层数据配置 + */ + readonly data?: Options['data']; /** * @title 是否同步子 view 的配置 * @description 目前仅仅支持 true / false,后续需要增加 function 的方式进行自定义 view 之前的布局同步 @@ -96,12 +112,18 @@ export interface MixOptions readonly plots?: IPlot[]; /** * @title tooltip 配置 - * @description tooltip 配置在 chart 层配置 + * @description Mix tooltip 组件配置,统一顶层配置 + * TODO 字段映射 */ readonly tooltip?: Tooltip; /** * @title legend 配置 - * @description 统一顶层配置 + * @description Mix 图例配置,统一顶层配置 */ readonly legend?: false | Record; + /** + * @title slider 配置 + * @description Mix 缩略轴配置,统一顶层配置 + */ + readonly slider?: Slider; } diff --git a/src/plots/mix/utils.ts b/src/plots/mix/utils.ts index 54a13abc25..13b5f011bf 100644 --- a/src/plots/mix/utils.ts +++ b/src/plots/mix/utils.ts @@ -33,9 +33,9 @@ import { Funnel, FunnelOptions } from '../funnel'; import { Stock, StockOptions } from '../stock'; /** - * 移除 options 中的 width、height 设置 + * 移除 options 中的 width、height、data 设置 */ -type OmitSize = Omit; +type OmitPlotOptions = Omit; /** * multi-view 中的支持的 plots 类型(带 options 定义) @@ -49,63 +49,63 @@ export type IPlotTypes = /** * plot 配置 */ - readonly options: OmitSize; + readonly options: OmitPlotOptions; } | { readonly type: 'pie'; - readonly options: OmitSize; + readonly options: OmitPlotOptions; } | { readonly type: 'bar'; - readonly options: OmitSize; + readonly options: OmitPlotOptions; } | { readonly type: 'column'; - readonly options: OmitSize; + readonly options: OmitPlotOptions; } | { readonly type: 'area'; - readonly options: OmitSize; + readonly options: OmitPlotOptions; } | { readonly type: 'gauge'; - readonly options: OmitSize; + readonly options: OmitPlotOptions; } | { readonly type: 'tiny-line'; - readonly options: OmitSize; + readonly options: OmitPlotOptions; } | { readonly type: 'tiny-area'; - readonly options: OmitSize; + readonly options: OmitPlotOptions; } | { readonly type: 'tiny-column'; - readonly options: OmitSize; + readonly options: OmitPlotOptions; } | { readonly type: 'ring-progress'; - readonly options: OmitSize; + readonly options: OmitPlotOptions; } | { readonly type: 'progress'; - readonly options: OmitSize; + readonly options: OmitPlotOptions; } | { readonly type: 'histogram'; - readonly options: OmitSize; + readonly options: OmitPlotOptions; } | { readonly type: 'scatter'; - readonly options: OmitSize; + readonly options: OmitPlotOptions; } | { readonly type: 'funnel'; - readonly options: OmitSize; + readonly options: OmitPlotOptions; } | { readonly type: 'stock'; - readonly options: OmitSize; + readonly options: OmitPlotOptions; }; /**