@antv/g2plot
Version:
An interactive and responsive charting library
254 lines (236 loc) • 8.21 kB
text/typescript
import { Coordinate, Element, Geometry, getTheme, IGroup, ShapeAttrs, View } from '@antv/g2';
import { each, find, isObject, map } from '@antv/util';
import { Params } from '../core/adaptor';
import { Options } from '../types';
import { deepAssign } from '../utils';
import { conversionTagFormatter } from '../utils/conversion';
/** 转化率组件配置选项 */
export interface ConversionTagOptions {
/** tag 高度 */
size?: number;
/** tag 对柱子间距 */
spacing?: number;
/** tag 距离轴线距离 */
offset?: number;
/** 箭头形状配置 */
arrow?:
| {
/** 箭头宽度 */
headSize?: number;
/** 箭头样式 */
style?: ShapeAttrs;
}
| false;
/** 文本配置 */
text?:
| {
/** 文字大小 */
size?: number;
/** 文字样式 */
style?: ShapeAttrs;
/** 文本格式化 */
formatter?: (prev: number, next: number) => string;
}
| false;
}
export interface OptionWithConversionTag {
conversionTag?: ConversionTagOptions | false;
}
/** 控制转化率组件绘制的选项 */
type TagRenderConfig = {
/** 所在的 View */
view: View;
/** 所属的 geometry */
geometry: Geometry;
/** 转化率组件的 group */
group: IGroup;
/** 转化率配置 */
options: ConversionTagOptions;
/** 用于转化率计算的字段 */
field: string;
/** 水平/垂直 */
horizontal: boolean;
};
function getConversionTagOptionsWithDefaults(options: ConversionTagOptions, horizontal: boolean): ConversionTagOptions {
return deepAssign(
{
size: horizontal ? 32 : 80,
spacing: horizontal ? 8 : 12,
offset: horizontal ? 32 : 0,
arrow: options.arrow !== false && {
headSize: 12,
style: {
fill: 'rgba(0, 0, 0, 0.05)',
},
},
text: options.text !== false && {
style: {
fontSize: 12,
fill: 'rgba(0, 0, 0, 0.85)',
textAlign: 'center',
textBaseline: 'middle',
},
formatter: conversionTagFormatter,
},
},
options
);
}
function parsePoints(coordinate: Coordinate, element: Element): { x: number; y: number }[] {
// @ts-ignore
return map(element.getModel().points, (point) => coordinate.convertPoint(point));
}
function renderArrowTag(config: TagRenderConfig, elemPrev: Element, elemNext: Element): void {
const { view, geometry, group, options, horizontal } = config;
const { offset, size, arrow } = options;
const coordinate = view.getCoordinate();
const pointPrev = parsePoints(coordinate, elemPrev)[3];
const pointNext = parsePoints(coordinate, elemNext)[0];
const totalHeight = pointNext.y - pointPrev.y;
const totalWidth = pointNext.x - pointPrev.x;
if (typeof arrow === 'boolean') {
return;
}
const { headSize } = arrow;
let spacing = options.spacing;
let points;
if (horizontal) {
if ((totalWidth - headSize) / 2 < spacing) {
// 当柱间距不足容纳箭头尖与间隔时,画三角并挤占间隔
spacing = Math.max(1, (totalWidth - headSize) / 2);
points = [
[pointPrev.x + spacing, pointPrev.y - offset],
[pointPrev.x + spacing, pointPrev.y - offset - size],
[pointNext.x - spacing, pointNext.y - offset - size / 2],
];
} else {
// 当柱间距足够时,画完整图形并留出间隔。
points = [
[pointPrev.x + spacing, pointPrev.y - offset],
[pointPrev.x + spacing, pointPrev.y - offset - size],
[pointNext.x - spacing - headSize, pointNext.y - offset - size],
[pointNext.x - spacing, pointNext.y - offset - size / 2],
[pointNext.x - spacing - headSize, pointNext.y - offset],
];
}
} else {
if ((totalHeight - headSize) / 2 < spacing) {
// 当柱间距不足容纳箭头尖与间隔时,画三角并挤占间隔
spacing = Math.max(1, (totalHeight - headSize) / 2);
points = [
[pointPrev.x + offset, pointPrev.y + spacing],
[pointPrev.x + offset + size, pointPrev.y + spacing],
[pointNext.x + offset + size / 2, pointNext.y - spacing],
];
} else {
// 当柱间距足够时,画完整图形并留出间隔。
points = [
[pointPrev.x + offset, pointPrev.y + spacing],
[pointPrev.x + offset + size, pointPrev.y + spacing],
[pointNext.x + offset + size, pointNext.y - spacing - headSize],
[pointNext.x + offset + size / 2, pointNext.y - spacing],
[pointNext.x + offset, pointNext.y - spacing - headSize],
];
}
}
group.addShape('polygon', {
id: `${view.id}-conversion-tag-arrow-${geometry.getElementId(elemPrev.getModel().mappingData)}`,
name: 'conversion-tag-arrow',
origin: {
element: elemPrev,
nextElement: elemNext,
},
attrs: {
...(arrow.style || {}),
points,
},
});
}
function renderTextTag(config: TagRenderConfig, elemPrev: Element, elemNext: Element): void {
const { view, geometry, group, options, field, horizontal } = config;
const { offset, size } = options;
if (typeof options.text === 'boolean') {
return;
}
const coordinate = view.getCoordinate();
const text = options.text?.formatter && options.text?.formatter(elemPrev.getData()[field], elemNext.getData()[field]);
const pointPrev = parsePoints(coordinate, elemPrev)[horizontal ? 3 : 0];
const pointNext = parsePoints(coordinate, elemNext)[horizontal ? 0 : 3];
const textShape = group.addShape('text', {
id: `${view.id}-conversion-tag-text-${geometry.getElementId(elemPrev.getModel().mappingData)}`,
name: 'conversion-tag-text',
origin: {
element: elemPrev,
nextElement: elemNext,
},
attrs: {
...(options.text?.style || {}),
text,
x: horizontal ? (pointPrev.x + pointNext.x) / 2 : pointPrev.x + offset + size / 2,
y: horizontal ? pointPrev.y - offset - size / 2 : (pointPrev.y + pointNext.y) / 2,
},
});
if (horizontal) {
const totalWidth = pointNext.x - pointPrev.x;
const { width: textWidth } = textShape.getBBox();
if (textWidth > totalWidth) {
const cWidth = textWidth / text.length;
const cEnd = Math.max(1, Math.ceil(totalWidth / cWidth) - 1);
const textAdjusted = `${text.slice(0, cEnd)}...`;
textShape.attr('text', textAdjusted);
}
}
}
function renderTag(options: TagRenderConfig, elemPrev: Element, elemNext: Element): void {
renderArrowTag(options, elemPrev, elemNext);
renderTextTag(options, elemPrev, elemNext);
}
/**
* 返回支持转化率组件的 adaptor,适用于柱形图/条形图
* @param field 用户转化率计算的字段
* @param horizontal 是否水平方向的转化率
* @param disabled 是否禁用
*/
export function conversionTag<O extends OptionWithConversionTag & Options>(
field: string,
horizontal = true,
disabled = false
) {
return function (params: Params<O>): Params<O> {
const { options, chart } = params;
const { conversionTag, theme } = options;
if (conversionTag && !disabled) {
// 有转化率组件时,柱子宽度占比自动为 1/3
chart.theme(
deepAssign({}, isObject(theme) ? theme : getTheme(theme), {
columnWidthRatio: 1 / 3,
})
);
// 使用 shape annotation 绘制转化率组件
chart.annotation().shape({
render: (container, view) => {
const group = container.addGroup({
id: `${chart.id}-conversion-tag-group`,
name: 'conversion-tag-group',
});
const interval = find(chart.geometries, (geom: Geometry) => geom.type === 'interval');
const config: TagRenderConfig = {
view,
geometry: interval,
group,
field,
horizontal,
options: getConversionTagOptionsWithDefaults(conversionTag, horizontal),
};
const elements = interval.elements;
each(elements, (elem: Element, idx: number) => {
if (idx > 0) {
renderTag(config, elements[idx - 1], elem);
}
});
},
});
}
return params;
};
}