import { Event, Scale, Types } from '@antv/g2'; import { each, every, find, findIndex, get, isBoolean, isEqual, isObject } from '@antv/util'; import { animation as commonAnimation, annotation as commonAnnotation, interaction as commonInteraction, limitInPlot as commonLimitInPlot, scale, theme as commonTheme, } from '../../adaptor/common'; import { Params } from '../../core/adaptor'; import { Datum } from '../../types'; import { deepAssign, flow } from '../../utils'; import { percent } from '../../utils/transform/percent'; import { findViewById } from '../../utils/view'; import { LEFT_AXES_VIEW, RIGHT_AXES_VIEW } from './constant'; import { AxisType, DualAxesGeometry, DualAxesOptions } from './types'; import { drawSingleGeometry } from './util/geometry'; import { getViewLegendItems } from './util/legend'; import { getGeometryOption, getYAxisWithDefault, isColumn, transformObjectToArray } from './util/option'; import { doSliderFilter } from './util/render-sider'; /** * transformOptions,双轴图整体的取参逻辑如下 * 1. get index getOptions: 对应的是默认的图表参数,如 appendPadding,syncView 等 * 2. get adpator transformOption: 对应的是双轴图的默认参数,deepAssign 优先级从低到高如下 * 2.1 defaultoption,如 tooltip,legend * 2.2 用户填写 options * 2.3 根据用户填写的 options 补充的数组型 options,如 yaxis,GeometryOption,因为 deepAssign 无法 assign 数组 * * @param params */ export function transformOptions(params: Params): Params { const { options } = params; const { geometryOptions = [], xField, yField } = options; const allLine = every( geometryOptions, ({ geometry }) => geometry === DualAxesGeometry.Line || geometry === undefined ); return deepAssign( {}, { options: { geometryOptions: [], meta: { [xField]: { // 默认为 cat 类型 type: 'cat', // x 轴一定是同步 scale 的 sync: true, // 如果有没有柱子,则 range: allLine ? [0, 1] : undefined, }, }, tooltip: { showMarkers: allLine, // 存在柱状图,不显示 crosshairs showCrosshairs: allLine, shared: true, crosshairs: { type: 'x', }, }, interactions: !allLine ? [{ type: 'legend-visible-filter' }, { type: 'active-region' }] : [{ type: 'legend-visible-filter' }], legend: { position: 'top-left', }, }, }, params, { options: { // yAxis yAxis: transformObjectToArray(yField, options.yAxis), // geometryOptions geometryOptions: [ getGeometryOption(xField, yField[0], geometryOptions[0]), getGeometryOption(xField, yField[1], geometryOptions[1]), ], // annotations annotations: transformObjectToArray(yField, options.annotations), }, } ); } /** * 创建 双轴图 中绘制图形的 view,提前创建是因为 theme 适配器的需要 * @param params */ function createViews(params: Params): Params { const { chart, options } = params; const { geometryOptions } = options; const SORT_MAP = { line: 0, column: 1 }; // 包含配置,id,数据的结构 const geometries = [ { type: geometryOptions[0]?.geometry, id: LEFT_AXES_VIEW }, { type: geometryOptions[1]?.geometry, id: RIGHT_AXES_VIEW }, ]; // 将线的 view 放置在更上一层,防止线柱遮挡。先柱后先 geometries.sort((a, b) => -SORT_MAP[a.type] + SORT_MAP[b.type]).forEach((g) => chart.createView({ id: g.id })); return params; } /** * 绘制图形 * @param params */ function geometry(params: Params): Params { const { chart, options } = params; const { xField, yField, geometryOptions, data, tooltip } = options; // 包含配置,id,数据的结构 const geometries = [ { ...geometryOptions[0], id: LEFT_AXES_VIEW, data: data[0], yField: yField[0] }, { ...geometryOptions[1], id: RIGHT_AXES_VIEW, data: data[1], yField: yField[1] }, ]; geometries.forEach((geometry) => { const { id, data, yField } = geometry; // 百分比柱状图需要额外处理一次数据 const isPercent = isColumn(geometry) && geometry.isPercent; const formatData = isPercent ? percent(data, yField, xField, yField) : data; const view = findViewById(chart, id).data(formatData); const tooltipOptions = isPercent ? { formatter: (datum: Datum) => ({ name: datum[geometry.seriesField] || yField, value: (Number(datum[yField]) * 100).toFixed(2) + '%', }), ...tooltip, } : tooltip; // 绘制图形 drawSingleGeometry({ chart: view, options: { xField, yField, tooltip: tooltipOptions, geometryOption: geometry, }, }); }); return params; } export function color(params: Params): Params { const { chart, options } = params; const { geometryOptions } = options; const themeColor = chart.getTheme()?.colors10 || []; let start = 0; /* 为 geometry 添加默认 color。 * 1. 若 geometryOptions 存在 color,则在 drawGeometry 时已处理 * 2. 若 不存在 color,获取 Geometry group scales个数,在 theme color 10 中提取 * 3. 为防止 group 过多导致右色板无值或值很少,右 view 面板在依次提取剩下的 N 个 后再 concat 一次 themeColor * 4. 为简便获取 Geometry group scales个数,在绘制完后再执行 color * 5. 考虑之后将不同 view 使用同一个色板的需求沉淀到 g2 */ chart.once('beforepaint', () => { each(geometryOptions, (geometryOption, index) => { const view = findViewById(chart, index === 0 ? LEFT_AXES_VIEW : RIGHT_AXES_VIEW); if (geometryOption.color) return; const groupScale = view.getGroupScales(); const count = get(groupScale, [0, 'values', 'length'], 1); const color = themeColor.slice(start, start + count).concat(index === 0 ? [] : themeColor); view.geometries.forEach((geometry) => { if (geometryOption.seriesField) { geometry.color(geometryOption.seriesField, color); } else { geometry.color(color[0]); } }); start += count; }); chart.render(true); }); return params; } /** * meta 配置 * @param params */ export function meta(params: Params): Params { const { chart, options } = params; const { xAxis, yAxis, xField, yField } = options; scale({ [xField]: xAxis, [yField[0]]: yAxis[0], })(deepAssign({}, params, { chart: findViewById(chart, LEFT_AXES_VIEW) })); scale({ [xField]: xAxis, [yField[1]]: yAxis[1], })(deepAssign({}, params, { chart: findViewById(chart, RIGHT_AXES_VIEW) })); return params; } /** * axis 配置 * @param params */ export function axis(params: Params): Params { const { chart, options } = params; const leftView = findViewById(chart, LEFT_AXES_VIEW); const rightView = findViewById(chart, RIGHT_AXES_VIEW); const { xField, yField, xAxis, yAxis } = options; chart.axis(xField, false); chart.axis(yField[0], false); chart.axis(yField[1], false); // 左 View leftView.axis(xField, xAxis); leftView.axis(yField[0], getYAxisWithDefault(yAxis[0], AxisType.Left)); // 右 Y 轴 rightView.axis(xField, false); rightView.axis(yField[1], getYAxisWithDefault(yAxis[1], AxisType.Right)); return params; } /** * tooltip 配置 * @param params */ export function tooltip(params: Params): Params { const { chart, options } = params; const { tooltip } = options; const leftView = findViewById(chart, LEFT_AXES_VIEW); const rightView = findViewById(chart, RIGHT_AXES_VIEW); // tooltip 经过 getDefaultOption 处理后,一定不为 undefined chart.tooltip(tooltip); // 在 view 上添加 tooltip,使得 shared 和 interaction active-region 起作用 // view 应该继承 chart 里的 shared,但是从表现看来,继承有点问题 leftView.tooltip({ shared: true, }); rightView.tooltip({ shared: true, }); return params; } /** * interaction 配置 * @param params */ export function interaction(params: Params): Params { const { chart } = params; commonInteraction(deepAssign({}, params, { chart: findViewById(chart, LEFT_AXES_VIEW) })); commonInteraction(deepAssign({}, params, { chart: findViewById(chart, RIGHT_AXES_VIEW) })); return params; } /** * annotation 配置 * @param params */ export function annotation(params: Params): Params { const { chart, options } = params; const { annotations } = options; const a1 = get(annotations, [0]); const a2 = get(annotations, [1]); commonAnnotation(a1)( deepAssign({}, params, { chart: findViewById(chart, LEFT_AXES_VIEW), options: { annotations: a1, }, }) ); commonAnnotation(a2)( deepAssign({}, params, { chart: findViewById(chart, RIGHT_AXES_VIEW), options: { annotations: a2, }, }) ); return params; } export function theme(params: Params): Params { const { chart } = params; /* * 双轴图中,部分组件是绘制在子 view 层(例如 axis,line),部分组件是绘制在 chart (例如 legend) * 为 chart 和 子 view 均注册 theme,使其自行遵循 G2 theme geometry > view > chart 进行渲染。 */ commonTheme(deepAssign({}, params, { chart: findViewById(chart, LEFT_AXES_VIEW) })); commonTheme(deepAssign({}, params, { chart: findViewById(chart, RIGHT_AXES_VIEW) })); commonTheme(params); return params; } export function animation(params: Params): Params { const { chart } = params; commonAnimation(deepAssign({}, params, { chart: findViewById(chart, LEFT_AXES_VIEW) })); commonAnimation(deepAssign({}, params, { chart: findViewById(chart, RIGHT_AXES_VIEW) })); return params; } /** * 双轴图 limitInPlot * @param params */ export function limitInPlot(params: Params): Params { const { chart, options } = params; const { yAxis } = options; commonLimitInPlot( deepAssign({}, params, { chart: findViewById(chart, LEFT_AXES_VIEW), options: { yAxis: yAxis[0], }, }) ); commonLimitInPlot( deepAssign({}, params, { chart: findViewById(chart, RIGHT_AXES_VIEW), options: { yAxis: yAxis[1], }, }) ); return params; } /** * legend 配置 * 使用 custom,便于和类似于分组柱状图-单折线图的逻辑统一 * @param params */ export function legend(params: Params): Params { const { chart, options } = params; const { legend, geometryOptions, yField, data } = options; const leftView = findViewById(chart, LEFT_AXES_VIEW); const rightView = findViewById(chart, RIGHT_AXES_VIEW); if (legend === false) { chart.legend(false); } else if (isObject(legend) && legend.custom === true) { chart.legend(legend); } else { const leftLegend = get(geometryOptions, [0, 'legend'], legend); const rightLegend = get(geometryOptions, [1, 'legend'], legend); // 均使用自定义图例 chart.once('beforepaint', () => { const leftItems = data[0].length ? getViewLegendItems({ view: leftView, geometryOption: geometryOptions[0], yField: yField[0], legend: leftLegend, }) : []; const rightItems = data[1].length ? getViewLegendItems({ view: rightView, geometryOption: geometryOptions[1], yField: yField[1], legend: rightLegend, }) : []; chart.legend( deepAssign({}, legend, { custom: true, // todo 修改类型定义 // @ts-ignore items: leftItems.concat(rightItems), }) ); }); if (geometryOptions[0].seriesField) { leftView.legend(geometryOptions[0].seriesField, leftLegend); } if (geometryOptions[1].seriesField) { rightView.legend(geometryOptions[1].seriesField, rightLegend); } // 自定义图例交互 chart.on('legend-item:click', (evt) => { const delegateObject = get(evt, 'gEvent.delegateObject', {}); if (delegateObject && delegateObject.item) { const { value: field, isGeometry, viewId } = delegateObject.item; // geometry 的时候,直接使用 view.changeVisible if (isGeometry) { const idx = findIndex(yField, (yF: string) => yF === field); if (idx > -1) { const geometries = get(findViewById(chart, viewId), 'geometries'); each(geometries, (g) => { g.changeVisible(!delegateObject.item.unchecked); }); } } else { const legendItem = get(chart.getController('legend'), 'option.items', []); // 分组柱线图 each(chart.views, (view) => { // 单折柱图 const groupScale = view.getGroupScales(); each(groupScale, (scale: Scale) => { if (scale.values && scale.values.indexOf(field) > -1) { view.filter(scale.field, (value) => { const curLegendItem: Types.LegendItem = find( legendItem, (item: Types.LegendItem) => item.value === value ); // 使用 legend 中的 unchecked 来判断,使得支持关闭多个图例 return !curLegendItem.unchecked; }); } }); chart.render(true); }); } } }); } return params; } /** * 双轴图 slider 适配器 * @param params */ export function slider(params: Params): Params { const { chart, options } = params; const { slider } = options; const leftView = findViewById(chart, LEFT_AXES_VIEW); const rightView = findViewById(chart, RIGHT_AXES_VIEW); if (slider) { // 左 View leftView.option('slider', slider); // 监听左侧 slider 改变事件, 同步右侧 View 视图 leftView.on('slider:valuechanged', (evt: Event) => { const { event: { value, originValue }, } = evt; if (isEqual(value, originValue)) { return; } doSliderFilter(rightView, value); }); chart.once('afterpaint', () => { // 初始化数据,配置默认值时需要同步 if (!isBoolean(slider)) { const { start, end } = slider; if (start || end) { doSliderFilter(rightView, [start, end]); } } }); } return params; } /** * 双折线图适配器 * @param chart * @param options */ export function adaptor(params: Params): Params { // transformOptions 一定在最前面处理;color legend 使用了 beforepaint,为便于理解放在最后面 return flow( transformOptions, createViews, // 主题靠前设置,作为最低优先级 theme, geometry, meta, axis, limitInPlot, tooltip, interaction, annotation, animation, color, legend, slider )(params); }