From 64137e1a770af0d7f7d28455861171b34b6fe801 Mon Sep 17 00:00:00 2001 From: hustcc Date: Fri, 28 Jul 2023 21:43:03 +0800 Subject: [PATCH] feat: html label supported (#5354) * feat: html label supported * chore: fix ci * chore: update screenshot * docs: add 4 examples for label * docs: update label document --- .../static/alphabetIntervalHtmlLabel.png | Bin 0 -> 11383 bytes .../integration/utils/createNodeGCanvas.ts | 3 + .../static/alphabet-interval-html-label.ts | 41 ++++++++++++++ __tests__/plots/static/index.ts | 1 + site/docs/spec/label/overview.zh.md | 16 +++++- .../component/label/demo/contrastReverse.ts | 32 +++++++++++ .../component/label/demo/htmlLabel.ts | 39 +++++++++++++ site/examples/component/label/demo/meta.json | 40 +++++++++++++ .../component/label/demo/overflowHide.ts | 27 +++++++++ .../component/label/demo/overlapHide.ts | 26 +++++++++ site/examples/component/label/index.en.md | 4 ++ site/examples/component/label/index.zh.md | 4 ++ src/runtime/plot.ts | 9 ++- src/runtime/types/theme.ts | 1 + src/shape/label/label.ts | 23 +++++++- src/shape/text/advance.ts | 53 +++++++++++++----- src/theme/academy.ts | 6 ++ src/theme/classic.ts | 6 ++ src/theme/classicDark.ts | 6 ++ src/utils/selection.ts | 7 ++- 20 files changed, 324 insertions(+), 20 deletions(-) create mode 100644 __tests__/integration/snapshots/static/alphabetIntervalHtmlLabel.png create mode 100644 __tests__/plots/static/alphabet-interval-html-label.ts create mode 100644 site/examples/component/label/demo/contrastReverse.ts create mode 100644 site/examples/component/label/demo/htmlLabel.ts create mode 100644 site/examples/component/label/demo/meta.json create mode 100644 site/examples/component/label/demo/overflowHide.ts create mode 100644 site/examples/component/label/demo/overlapHide.ts create mode 100644 site/examples/component/label/index.en.md create mode 100644 site/examples/component/label/index.zh.md diff --git a/__tests__/integration/snapshots/static/alphabetIntervalHtmlLabel.png b/__tests__/integration/snapshots/static/alphabetIntervalHtmlLabel.png new file mode 100644 index 0000000000000000000000000000000000000000..260d76f862de68d6c15dd9347e9fc6f59d5da77e GIT binary patch literal 11383 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 O0Sglw;}>T=Z~hC+R*s(l literal 0 HcmV?d00001 diff --git a/__tests__/integration/utils/createNodeGCanvas.ts b/__tests__/integration/utils/createNodeGCanvas.ts index df394cc1a9..b6d5c4d1db 100644 --- a/__tests__/integration/utils/createNodeGCanvas.ts +++ b/__tests__/integration/utils/createNodeGCanvas.ts @@ -11,6 +11,9 @@ export function createNodeGCanvas(width: number, height: number): Canvas { // Create a renderer, unregister plugin relative to DOM. const renderer = new Renderer(); + // Remove html plugin to ssr. + const htmlRendererPlugin = renderer.getPlugin('html-renderer'); + renderer.unregisterPlugin(htmlRendererPlugin); const domInteractionPlugin = renderer.getPlugin('dom-interaction'); renderer.unregisterPlugin(domInteractionPlugin); renderer.registerPlugin( diff --git a/__tests__/plots/static/alphabet-interval-html-label.ts b/__tests__/plots/static/alphabet-interval-html-label.ts new file mode 100644 index 0000000000..aac93fedff --- /dev/null +++ b/__tests__/plots/static/alphabet-interval-html-label.ts @@ -0,0 +1,41 @@ +import { G2Spec } from '../../../src'; + +export function alphabetIntervalHtmlLabel(): G2Spec { + return { + type: 'interval', + transform: [{ type: 'sortX', by: 'y', reverse: true }], + data: { + type: 'fetch', + value: 'data/alphabet.csv', + }, + axis: { + y: { labelFormatter: '.0%' }, + }, + encode: { + x: 'letter', + y: 'frequency', + color: 'steelblue', + }, + labels: [ + { + text: 'frequency', + transform: [ + { + type: 'overlapHide', + }, + ], + className: 'alphabet-labels', + render: ( + _, + datum, + ) => `
+ ${ + datum.letter + }:${datum.frequency.toFixed( + 2, + )} +
`, + }, + ], + }; +} diff --git a/__tests__/plots/static/index.ts b/__tests__/plots/static/index.ts index 2147b44188..4f782989a5 100644 --- a/__tests__/plots/static/index.ts +++ b/__tests__/plots/static/index.ts @@ -1,4 +1,5 @@ export { alphabetInterval } from './alphabet-interval'; +export { alphabetIntervalHtmlLabel } from './alphabet-interval-html-label'; export { alphabetIntervalMaxWidth } from './alphabet-interval-max-width'; export { alphabetIntervalMinWidth } from './alphabet-interval-min-width'; export { alphabetIntervalMaxWidthTransposed } from './alphabet-interval-max-width-transposed'; diff --git a/site/docs/spec/label/overview.zh.md b/site/docs/spec/label/overview.zh.md index 94fbfe43a0..dd60af3968 100644 --- a/site/docs/spec/label/overview.zh.md +++ b/site/docs/spec/label/overview.zh.md @@ -97,6 +97,16 @@ Label 继承 G Text 所有属性样式配置,此外还有 `position`, `selecto | backgroundPadding | 背景框内间距 | `number[]` | - | | `background${style}` | 更多背景框样式配置,参考 `RectStyleProps` 属性值 | - | - | +数据标签支持使用 HTML 自定义标签,具体配置为: + +| 参数 | 说明 | 类型 | 默认值 | +| -------------------- | ------------------------------------------------ | ---------- | ------ | +| render | 返回 HTML string 或者 HTMElement,使用 HTML 自定义复杂标签 | `RenderFunc` | - | + +```ts +type RenderFunc = (text: string, datum: object, index: number) => String | HTMLElement; +``` + ## FAQ ### 支持哪些 position? @@ -131,6 +141,10 @@ selector 选择器可以对系列数据进行过滤索引。 } ``` -### 支持哪些 transform ? +### 支持哪些 transform? 所有的 transform 有单独具体的文档,具体参考 [Label.transform](/spec/overview#label)。 + +### 怎么使用 HTML 自定义数据标签? + +使用 label 配置手册中的 `render` 即可,具体使用可以参考 [DEMO](/examples/component/label/#htmlLabel)。 diff --git a/site/examples/component/label/demo/contrastReverse.ts b/site/examples/component/label/demo/contrastReverse.ts new file mode 100644 index 0000000000..8c4cc227e8 --- /dev/null +++ b/site/examples/component/label/demo/contrastReverse.ts @@ -0,0 +1,32 @@ +import { Chart } from '@antv/g2'; + +const chart = new Chart({ + container: 'container', + theme: 'classic', + autoFit: true, +}); + +chart + .interval() + .data({ + type: 'fetch', + value: 'https://assets.antv.antgroup.com/g2/alphabet.json', + }) + .encode('x', 'letter') + .encode('y', 'frequency') + .encode('color', 'letter') + .label({ + text: 'frequency', + position: 'inside', + formatter: '.0%', + fill: '#000', + transform: [ + { + type: 'contrastReverse', + threshold: 21, + palette: ['#000', '#fff'], // Use full color string to avoid screenshot error. + }, + ], + }); + +chart.render(); diff --git a/site/examples/component/label/demo/htmlLabel.ts b/site/examples/component/label/demo/htmlLabel.ts new file mode 100644 index 0000000000..0d11fcb6c4 --- /dev/null +++ b/site/examples/component/label/demo/htmlLabel.ts @@ -0,0 +1,39 @@ +import { Chart } from '@antv/g2'; + +const chart = new Chart({ + container: 'container', + theme: 'classic', + autoFit: true, +}); + +const data = [ + { repo: 'G', star: 918 }, + { repo: 'G2', star: 11688 }, + { repo: 'G6', star: 10045 }, + { repo: 'L7', star: 3125 }, + { repo: 'F2', star: 7820 }, + { repo: 'S2', star: 1231 }, + { repo: 'X6', star: 4755 }, +]; + +chart + .interval() + .data(data) + .encode('x', 'repo') + .encode('y', 'star') + .encode('color', 'repo') + .label({ + text: 'star', + render: (text, datum) => { + return ` +
+ ${datum.repo} + : + ${datum.star} +
+ `; + }, + }) + .legend(false); + +chart.render(); diff --git a/site/examples/component/label/demo/meta.json b/site/examples/component/label/demo/meta.json new file mode 100644 index 0000000000..edd82d64fe --- /dev/null +++ b/site/examples/component/label/demo/meta.json @@ -0,0 +1,40 @@ +{ + "title": { + "zh": "中文分类", + "en": "Category" + }, + "demos": [ + { + "filename": "overlapHide.ts", + "title": { + "zh": "数据标签碰撞隐藏", + "en": "Overlap Hide" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*clZ2SbxuHloAAAAAAAAAAAAADmJ7AQ/original" + }, + { + "filename": "overflowHide.ts", + "title": { + "zh": "数据标签超出隐藏", + "en": "Overflow Hide" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*EMy1RqEBmm4AAAAAAAAAAAAADmJ7AQ/original" + }, + { + "filename": "contrastReverse.ts", + "title": { + "zh": "数据标签颜色反转", + "en": "Contrast Reverse" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*CUyIT4Bxx9wAAAAAAAAAAAAADmJ7AQ/original" + }, + { + "filename": "htmlLabel.ts", + "title": { + "zh": "HTML 标签", + "en": "HTML Label" + }, + "screenshot": "https://mdn.alipayobjects.com/huamei_qa8qxu/afts/img/A*rUexRKH7czsAAAAAAAAAAAAADmJ7AQ/original" + } + ] +} diff --git a/site/examples/component/label/demo/overflowHide.ts b/site/examples/component/label/demo/overflowHide.ts new file mode 100644 index 0000000000..42e3de658e --- /dev/null +++ b/site/examples/component/label/demo/overflowHide.ts @@ -0,0 +1,27 @@ +import { Chart } from '@antv/g2'; + +const chart = new Chart({ + container: 'container', + theme: 'classic', + width: 800, + autoFit: false, +}); + +chart + .interval() + .data({ + type: 'fetch', + value: 'https://assets.antv.antgroup.com/g2/alphabet.json', + }) + .encode('x', 'letter') + .encode('y', 'frequency') + .encode('color', 'steelblue') + .axis('y', { labelFormatter: '.0%' }) + .label({ + text: 'frequency', + position: 'inside', + formatter: '.0%', + transform: [{ type: 'overflowHide' }], + }); + +chart.render(); diff --git a/site/examples/component/label/demo/overlapHide.ts b/site/examples/component/label/demo/overlapHide.ts new file mode 100644 index 0000000000..e4cf259f0c --- /dev/null +++ b/site/examples/component/label/demo/overlapHide.ts @@ -0,0 +1,26 @@ +import { Chart } from '@antv/g2'; + +const chart = new Chart({ + container: 'container', + theme: 'classic', + autoFit: true, +}); + +chart + .line() + .data({ + type: 'fetch', + value: 'https://assets.antv.antgroup.com/g2/aapl.json', + }) + .encode('x', (d) => new Date(d.date)) + .encode('y', 'close') + .label({ + text: 'close', + transform: [ + { + type: 'overlapHide', + }, + ], + }); + +chart.render(); diff --git a/site/examples/component/label/index.en.md b/site/examples/component/label/index.en.md new file mode 100644 index 0000000000..26b5633b5f --- /dev/null +++ b/site/examples/component/label/index.en.md @@ -0,0 +1,4 @@ +--- +title: Label +order: 3 +--- diff --git a/site/examples/component/label/index.zh.md b/site/examples/component/label/index.zh.md new file mode 100644 index 0000000000..a8333aab08 --- /dev/null +++ b/site/examples/component/label/index.zh.md @@ -0,0 +1,4 @@ +--- +title: 数据标签 +order: 3 +--- \ No newline at end of file diff --git a/src/runtime/plot.ts b/src/runtime/plot.ts index 681883a747..d7ca601536 100644 --- a/src/runtime/plot.ts +++ b/src/runtime/plot.ts @@ -1189,6 +1189,7 @@ function createLabelShapeFunction( formatter = (d) => `${d}`, transform, style: abstractStyle, + render, ...abstractOptions } = options; const visualOptions = mapObject( @@ -1197,10 +1198,14 @@ function createLabelShapeFunction( ); const { shape = defaultLabelShape, text, ...style } = visualOptions; const f = typeof formatter === 'string' ? format(formatter) : formatter; - const value = { ...style, text: f(text, datum, index, abstractData) }; + const value = { + ...style, + text: f(text, datum, index, abstractData), + datum, + }; // Params for create shape. - const shapeOptions = { type: `label.${shape}`, ...style }; + const shapeOptions = { type: `label.${shape}`, render, ...style }; const shapeFunction = useShape(shapeOptions, shapeContext); const defaults = getDefaultsStyle(theme, 'label', shape, 'label'); diff --git a/src/runtime/types/theme.ts b/src/runtime/types/theme.ts index e4e7a14e96..68732b71e6 100644 --- a/src/runtime/types/theme.ts +++ b/src/runtime/types/theme.ts @@ -79,6 +79,7 @@ type ComponentTheme = { legendContinuous?: any; label?: LabelStyleProps; innerLabel?: LabelStyleProps; + htmlLabel?: any; slider?: any; scrollbar?: any; title?: any; // @todo diff --git a/src/shape/label/label.ts b/src/shape/label/label.ts index 51fa54c7e8..861105d5aa 100644 --- a/src/shape/label/label.ts +++ b/src/shape/label/label.ts @@ -8,7 +8,13 @@ import { Advance } from '../text/advance'; import { LabelPosition } from './position/default'; import * as PositionProcessor from './position'; -export type LabelOptions = Record; +export type LabelOptions = { + /** + * Customize label with html string or element. + */ + render: (text: string, datum: any, index: number) => string | HTMLElement; + [key: string]: any; +}; function inferPosition(position: LabelPosition, coordinate: Coordinate) { if (position !== undefined) return position; @@ -27,8 +33,14 @@ function getDefaultStyle( // For non-series mark, calc position for label based on // position and the bounds of shape. const { position } = value; + const { render } = options; const p = inferPosition(position, coordinate); - const t = theme[p === 'inside' ? 'innerLabel' : 'label']; + const labelType = render + ? 'htmlLabel' + : p === 'inside' + ? 'innerLabel' + : 'label'; + const t = theme[labelType]; const v = Object.assign({}, t, value); const processor = PositionProcessor[camelCase(p)]; if (!processor) { @@ -46,6 +58,7 @@ function getDefaultStyle( */ export const Label: SC = (options, context) => { const { coordinate, theme } = context; + const { render } = options; return (points, value) => { const { text, @@ -53,6 +66,7 @@ export const Label: SC = (options, context) => { y, transform: specifiedTS = '', transformOrigin, + className = '', ...overrideStyle } = value; const { @@ -64,6 +78,11 @@ export const Label: SC = (options, context) => { return select(new Advance()) .call(applyStyle, defaultStyle) .style('text', `${text}`) + .style('className', `${className} g2-label`) + .style( + 'innerHTML', + render ? render(text, value.datum, value.index) : undefined, + ) .style( 'labelTransform', `${transform} rotate(${+rotate}) ${specifiedTS}`.trim(), diff --git a/src/shape/text/advance.ts b/src/shape/text/advance.ts index 500516af91..4c3b51f9a6 100644 --- a/src/shape/text/advance.ts +++ b/src/shape/text/advance.ts @@ -34,6 +34,8 @@ type TextShapeStyleProps = Omit & BackgroundStyleProps & MarkerStyleProps<'startMarker'> & MarkerStyleProps<'endMarker'> & { + id: string; + className?: string; x0?: number; // x0 represents the x position of relative point, default is equal to x y0?: number; coordCenter?: Vector2; // center of coordinate @@ -44,6 +46,8 @@ type TextShapeStyleProps = Omit & labelTransform?: string; labelTransformOrigin?: string; rotate?: number; + innerHTML?: string | HTMLElement; + text?: string; }; function getConnectorPoint(shape: GText | Rect) { @@ -126,8 +130,10 @@ function inferConnectorPath( export const Advance = createElement((g) => { const { + id, + className, // Do not pass className - class: className, + class: _c, transform, rotate, labelTransform, @@ -136,11 +142,13 @@ export const Advance = createElement((g) => { y, x0 = x, y0 = y, + text, background, connector, startMarker, endMarker, coordCenter, + innerHTML, ...rest } = g.attributes as TextShapeStyleProps; @@ -157,26 +165,43 @@ export const Advance = createElement((g) => { [+x0, +y0], [+x, +y], ]; - const shape1 = select(g) - .maybeAppend('text', 'text') - .style('zIndex', 0) - .call(applyStyle, { - textBaseline: 'middle', - transform: labelTransform, - transformOrigin: labelTransformOrigin, - ...rest, - }) - .node(); - const shape2 = select(g) + let textShape; + // Use html to customize advance text. + if (innerHTML) { + textShape = select(g) + .maybeAppend(id, 'html', className) + .style('zIndex', 0) + .style('innerHTML', innerHTML) + .call(applyStyle, { + transform: labelTransform, + transformOrigin: labelTransformOrigin, + ...rest, + }) + .node(); + } else { + textShape = select(g) + .maybeAppend('text', 'text') + .style('zIndex', 0) + .style('text', text) + .call(applyStyle, { + textBaseline: 'middle', + transform: labelTransform, + transformOrigin: labelTransformOrigin, + ...rest, + }) + .node(); + } + + const rect = select(g) .maybeAppend('background', 'rect') .style('zIndex', -1) - .call(applyStyle, inferBackgroundBounds(shape1, padding)) + .call(applyStyle, inferBackgroundBounds(textShape, padding)) .call(applyStyle, background ? backgroundStyle : {}) .node(); const connectorPath = inferConnectorPath( - shape2, + rect, endPoints, points, coordCenter, diff --git a/src/theme/academy.ts b/src/theme/academy.ts index 667147417e..93b2295e3a 100644 --- a/src/theme/academy.ts +++ b/src/theme/academy.ts @@ -370,6 +370,12 @@ export const Academy: TC = (options) => { stroke: undefined, offset: 0, }, + htmlLabel: { + fontSize: 12, + opacity: 0.65, + color: COLORS.BLACK, + fontWeight: 'normal', + }, slider: { trackSize: 16, trackFill: COLORS.STROKE, diff --git a/src/theme/classic.ts b/src/theme/classic.ts index ed69c41e23..51785f2e29 100644 --- a/src/theme/classic.ts +++ b/src/theme/classic.ts @@ -377,6 +377,12 @@ export const Classic: TC = (options) => { stroke: undefined, offset: 0, }, + htmlLabel: { + fontSize: 12, + opacity: 0.65, + color: COLORS.BLACK, + fontWeight: 'normal', + }, slider: { trackSize: 16, trackFill: COLORS.STROKE, diff --git a/src/theme/classicDark.ts b/src/theme/classicDark.ts index 21428cb791..edc38262cc 100644 --- a/src/theme/classicDark.ts +++ b/src/theme/classicDark.ts @@ -376,6 +376,12 @@ export const ClassicDark: TC = (options) => { stroke: undefined, offset: 0, }, + htmlLabel: { + fontSize: 12, + opacity: 0.65, + color: COLORS.BLACK, + fontWeight: 'normal', + }, slider: { trackSize: 16, trackFill: COLORS.STROKE, diff --git a/src/utils/selection.ts b/src/utils/selection.ts index aabd6e9d1d..59bfde4bf2 100644 --- a/src/utils/selection.ts +++ b/src/utils/selection.ts @@ -166,7 +166,11 @@ export class Selection { } } - maybeAppend(id: string, node: string | (() => G2Element)) { + maybeAppend( + id: string, + node: string | (() => G2Element), + className?: string, + ) { const element = this._elements[0]; const child = element.getElementById(id) as G2Element; if (child) { @@ -175,6 +179,7 @@ export class Selection { const newChild = typeof node === 'string' ? this.createElement(node) : node(); newChild.id = id; + if (className) newChild.className = className; element.appendChild(newChild); return new Selection([newChild], null, this._parent, this._document); }