diff --git a/__tests__/unit/components/marker-point-spec.ts b/__tests__/unit/components/marker-point-spec.ts new file mode 100644 index 0000000000..9945258084 --- /dev/null +++ b/__tests__/unit/components/marker-point-spec.ts @@ -0,0 +1,150 @@ +import { createDiv } from '../../utils/dom'; +import { Line } from '../../../src'; +import MarkerPoint from '../../../src/components/marker-point'; + +const data = [ + { + date: '2018/8/12', + value: 5, + }, + { + date: '2018/8/12', + description: 'register', + value: 5, + }, + { + date: '2018/8/12', + value: 5, + }, + { + date: '2018/8/13', + value: 5, + }, +]; + +describe('Marker Point', () => { + const div = createDiv('canvas'); + + const line = new Line(div, { + width: 400, + height: 400, + xField: 'date', + yField: 'value', + padding: [0, 0, 0, 0], + data, + point: { + visible: true, + }, + }); + + line.render(); + + it('normal', () => { + const markerPoint = new MarkerPoint({ + view: line.getLayer().view, + data: [data[0], data[1]], + }); + + // @ts-ignore + const points = markerPoint.points; + expect(markerPoint.container.getChildren().length).toBe(2); + // @ts-ignore + expect(points.length).toBe(2); + // @ts-ignore + expect(markerPoint.labels.length).toBe(0); + + const shapes = line.getLayer().view.geometries[1].getShapes(); + expect(shapes[0].getBBox().minX + shapes[0].getBBox().width / 2).toBe( + points[0].getBBox().minX + points[0].getBBox().width / 2 + ); + }); + + it('marker label & label style', () => { + const markerPoint = new MarkerPoint({ + view: line.getLayer().view, + data: [data[0], data[1]], + field: 'description', + label: { + visible: true, + style: { + fill: 'red', + }, + }, + }); + + // @ts-ignore + const points = markerPoint.points; + // @ts-ignore + const labels = markerPoint.labels; + expect(labels.length).toBe(2); + expect(labels[1].attr('text')).toBe(data[1]['description']); + expect(labels[1].attr('fill')).toBe('red'); + expect(points[0].getBBox().y).toBeGreaterThan(labels[0].getBBox().y); + }); + + it('label position & offsetX & offsetY', () => { + const markerPoint = new MarkerPoint({ + view: line.getLayer().view, + data: [data[0], data[1]], + field: 'description', + label: { + visible: true, + position: 'bottom', + offsetX: 10, + offsetY: 5, + }, + }); + // @ts-ignore + const points = markerPoint.points; + // @ts-ignore + const labels = markerPoint.labels; + const labelBBox = labels[0].getBBox(); + expect(points[0].getBBox().y).toBeLessThan(labelBBox.y); + expect(points[0].attr('x') + 10).toBe(labelBBox.minX + labelBBox.width / 2); + expect(points[0].attr('y') + 5).toBe(labelBBox.minY); + }); + + it('interaction & events', (done) => { + let clicked = false; + const markerPoint = new MarkerPoint({ + view: line.getLayer().view, + data: [data[0], data[1]], + field: 'description', + events: { + click: () => (clicked = true), + }, + style: { + normal: { + stroke: 'transparent', + }, + selected: { + stroke: 'blue', + fill: 'red', + }, + active: { + stroke: 'yellow', + fill: 'red', + }, + }, + }); + // @ts-ignore + const points = markerPoint.points; + setTimeout(() => { + // @ts-ignore + markerPoint.container.emit(`${markerPoint.name}:mouseenter`, { + target: points[1], + }); + expect(points[1].attr('stroke')).toBe('yellow'); + expect(points[1].attr('fill')).toBe('red'); + // @ts-ignore + markerPoint.container.emit(`${markerPoint.name}:click`, { + target: points[0], + }); + expect(clicked).toBe(true); + expect(points[0].attr('stroke')).toBe('blue'); + expect(points[0].attr('fill')).toBe('red'); + expect(points[1].attr('stroke')).toBe('transparent'); + done(); + }); + }); +}); diff --git a/__tests__/unit/plots/line/line-with-markerPoint-spec.ts b/__tests__/unit/plots/line/line-with-markerPoint-spec.ts new file mode 100644 index 0000000000..2bbe595718 --- /dev/null +++ b/__tests__/unit/plots/line/line-with-markerPoint-spec.ts @@ -0,0 +1,111 @@ +import { Line } from '../../../../src'; +import { createDiv } from '../../../utils/dom'; +import { income as data } from '../../../data/income'; +import MarkerPoint from '../../../../src/components/marker-point'; +import { IShape } from '@antv/g2/lib/dependents'; + +describe('Line plot with marker-point', () => { + const div = createDiv(); + const linePlot = new Line(div, { + width: 600, + height: 600, + data, + xField: 'time', + yField: 'rate', + markerPoints: [ + { + visible: true, + data: [{ time: '2013-06-13' }], + }, + ], + }); + linePlot.render(); + + it('normal', () => { + const layer = linePlot.getLayer(); + // @ts-ignore + const markerPoints: MarkerPoint[] = layer.markerPoints; + expect(markerPoints.length).toBe(1); + // @ts-ignore + expect(markerPoints[0].points.length).toBe(1); + // @ts-ignore + expect(markerPoints[0].labels.length).toBe(0); + }); + + it('with 2 markerPoints component', () => { + linePlot.updateConfig({ + markerPoints: [ + { + visible: true, + data: [{ time: '2013-06-13' }], + }, + { + visible: true, + data: [{ time: '2013-06-16' }], + }, + ], + }); + linePlot.render(); + const layer = linePlot.getLayer(); + // @ts-ignore + const markerPoints: MarkerPoint[] = layer.markerPoints; + expect(markerPoints.length).toBe(2); + }); + + it('custom markerPoints style', () => { + linePlot.updateConfig({ + markerPoints: [ + { + visible: true, + data: [{ time: '2013-06-13' }], + style: { + normal: { + fill: 'red', + stroke: '#000', + lineWidth: 1, + }, + }, + }, + ], + }); + linePlot.render(); + const layer = linePlot.getLayer(); + // @ts-ignore + const pointShapes: IShape[] = layer.markerPoints[0].points; + expect(pointShapes[0].attr('fill')).toBe('red'); + expect(pointShapes[0].attr('stroke')).toBe('#000'); + expect(pointShapes[0].attr('lineWidth')).toBe(1); + }); + + it('markerPoints with label', () => { + linePlot.updateConfig({ + markerPoints: [ + { + visible: true, + data: [{ time: '2013-06-13' }, { time: '2013-06-18' }], + style: { + normal: { + fill: 'red', + stroke: '#000', + lineWidth: 1, + }, + }, + label: { + visible: true, + formatter: () => 'hello', + style: { + fill: 'red', + }, + }, + }, + ], + }); + linePlot.render(); + const layer = linePlot.getLayer(); + // @ts-ignore + const labelShapes: IShape[] = layer.markerPoints[0].labels; + expect(labelShapes.length).toBe(2); + expect(labelShapes[0].attr('fill')).toBe('red'); + expect(labelShapes[0].attr('text')).toBe('hello'); + }); +}); diff --git a/examples/general/markerPoint/API.en.md b/examples/general/markerPoint/API.en.md new file mode 100644 index 0000000000..caf3059d9d --- /dev/null +++ b/examples/general/markerPoint/API.en.md @@ -0,0 +1,138 @@ +--- +title: API +--- + +--- + +## title: API + +说明: **required** 标签代表组件的必选配置项,**optional** 标签代表组件的可选配置项。 + +- `style: object`    标注点样式。
+ + - `fill: string`    标注点颜色
+ - `opacity: number`  标注点颜色透明度
+ - `stroke: string`    标注点描边色
+ - `lineWidth: number`    标注点描边粗细 + +## 快速开始 + +[DEMOS](https://g2plot.antv.vision/zh/examples/general/markerPoint) + +配置标注点示例代码: + +```js +{ + markerPoints: [ + { + visible: true, + shape: 'circle', + data: [], + style: { + /** 正常样式 **/ + normal: {}, + /** 激活样式 **/ + active: {}, + /** 选中样式 **/ + selected: {}, + }, + label: { + visible: true, + position: 'top', + style: {}, + }, + }, + ], +} +``` + +## symbol + +**optional** string | Function, 默认: `circle` + +标注点图形类型 + +1. string 类型。可参见 G2 支持的`symbol`类型,包括: `hexagon`, `bowtie`, `cross`, `tick`, `plus`, `hyphen`, `line` + +2. Function 类型,可以自定义 symbol 绘制,如下: + +```typwscript +sumbol: (x: number, y: number, r: number) => { + return [ + ['M', x - r, y - r], + ['L', x + r, y + r], + ['L', x + r, y - r], + ['L', x - r, y + r], + ['L', x - r, y - r], + ['Z'], + ]; +} +``` + +## size + +**optional** number, 默认: 6 + +symbol 的大小 + +## data + +**required** array + +标注点的数据数组,每个数据项是一个对象 + +> 注意,标注点的数据数组是图表 data 的子集 + +示例: + +```typescript +data: [ + // 匹配所有数据项为 3 的数据点 + { value: 3 }, + // 匹配 日期为 2019-10-01,且数值为 3 的数据点 + { date: '2019-10-01', value: 3 }, +]; +``` + +## field + +**optional** string + +标注点映射的数据字段,用于标注点标签 + +## label + +**optional** object + +- `visible` 标注点标签是否可见 +- `formatter` 标签格式化 +- `position` 标注点标签位置,`top` | `bottom` +- `offsetX` x 方向偏移 +- `offsetY` y 方向偏移 +- `style` 样式 + +## style + +**optional** object + +- `normal` 正常样式 +- `active` 激活样式 +- `selected` 选中样式 + +## events + +**optional** object + +标注点事件 + +- `mouseenter` 鼠标移入事件 +- `mouseleave` 鼠标移出事件 +- `click` 鼠标 click 事件 + +* Modern browsers and Internet Explorer 9+ (with [polyfills](https:// ant.design/docs/react/getting-started#Compatibility)) +* Server-side Rendering +* [Electron](http:// electron.atom.io/) + +| [IE / Edge](http:// godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | [Electron](http://godban.github.io/browsers-support-badges/)
Electron | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| IE9, IE10, IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions | last 2 versions | diff --git a/examples/general/markerPoint/API.zh.md b/examples/general/markerPoint/API.zh.md new file mode 100644 index 0000000000..4a7d64fc25 --- /dev/null +++ b/examples/general/markerPoint/API.zh.md @@ -0,0 +1,134 @@ +--- +title: API +--- + +说明: **required** 标签代表组件的必选配置项,**optional** 标签代表组件的可选配置项。 + +- `style: object`    标注点样式。
+ + - `fill: string`    标注点颜色
+ - `opacity: number`  标注点颜色透明度
+ - `stroke: string`    标注点描边色
+ - `lineWidth: number`    标注点描边粗细 + +## 快速开始 + +[DEMOS](https://g2plot.antv.vision/zh/examples/general/markerPoint) + +配置标注点示例代码: + +```js +{ + markerPoints: [ + { + visible: true, + shape: 'circle', + data: [], + style: { + /** 正常样式 **/ + normal: {}, + /** 激活样式 **/ + active: {}, + /** 选中样式 **/ + selected: {}, + }, + label: { + visible: true, + position: 'top', + style: {}, + }, + }, + ], +} +``` + +## symbol + +**optional** string | Function, 默认: `circle` + +标注点图形类型 + +1. string 类型。可参见 G2 支持的`symbol`类型,包括: `hexagon`, `bowtie`, `cross`, `tick`, `plus`, `hyphen`, `line` + +2. Function 类型,可以自定义 symbol 绘制,如下: + +```typwscript +sumbol: (x: number, y: number, r: number) => { + return [ + ['M', x - r, y - r], + ['L', x + r, y + r], + ['L', x + r, y - r], + ['L', x - r, y + r], + ['L', x - r, y - r], + ['Z'], + ]; +} +``` + +## size + +**optional** number, 默认: 6 + +symbol 的大小 + +## data + +**required** array + +标注点的数据数组,每个数据项是一个对象 + +> 注意,标注点的数据数组是图表 data 的子集 + +示例: + +```typescript +data: [ + // 匹配所有数据项为 3 的数据点 + { value: 3 }, + // 匹配 日期为 2019-10-01,且数值为 3 的数据点 + { date: '2019-10-01', value: 3 }, +]; +``` + +## field + +**optional** string + +标注点映射的数据字段,用于标注点标签 + +## label + +**optional** object + +- `visible` 标注点标签是否可见 +- `formatter` 标签格式化 +- `position` 标注点标签位置,`top` | `bottom` +- `offsetX` x 方向偏移 +- `offsetY` y 方向偏移 +- `style` 样式 + +## style + +**optional** object + +- `normal` 正常样式 +- `active` 激活样式 +- `selected` 选中样式 + +## events + +**optional** object + +标注点事件 + +- `mouseenter` 鼠标移入事件 +- `mouseleave` 鼠标移出事件 +- `click` 鼠标 click 事件 + +- Modern browsers and Internet Explorer 9+ (with [polyfills](https:// ant.design/docs/react/getting-started#Compatibility)) +- Server-side Rendering +- [Electron](http:// electron.atom.io/) + +| [IE / Edge](http:// godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | [Opera](http://godban.github.io/browsers-support-badges/)
Opera | [Electron](http://godban.github.io/browsers-support-badges/)
Electron | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| IE9, IE10, IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions | last 2 versions | diff --git a/examples/general/markerPoint/demo/basic.js b/examples/general/markerPoint/demo/basic.js new file mode 100644 index 0000000000..7ffea3f870 --- /dev/null +++ b/examples/general/markerPoint/demo/basic.js @@ -0,0 +1,50 @@ +import { Line } from '@antv/g2plot'; + +const data = [ + { date: '2019-01-01', value: 3 }, + { date: '2019-02-01', value: 4 }, + { date: '2019-03-01', value: 3.5 }, + { date: '2019-04-01', value: 5 }, + { date: '2019-05-01', value: 4.9, festival: '劳动节' }, + { date: '2019-06-01', value: 6 }, + { date: '2019-07-01', value: 7 }, + { date: '2019-08-01', value: 9 }, + { date: '2019-09-01', value: 3 }, + { date: '2019-10-01', value: 13, festival: '国庆节' }, + { date: '2019-11-01', value: 6 }, + { date: '2019-12-01', value: 23 }, +]; + +const linePlot = new Line(document.getElementById('container'), { + title: { + visible: true, + text: '带标注点的折线图', + }, + description: { + visible: true, + text: '在折线图上标注重点的数据,如节假日等', + }, + forceFit: true, + padding: 'auto', + data, + xField: 'date', + yField: 'value', + yAxis: { + nice: true, + }, + label: { + visible: false, + }, + markerPoints: [ + { + visible: true, + data: [{ date: '2019-05-01', value: 4.9 }, { date: '2019-10-01' }], + field: 'festival', + label: { + visible: true, + }, + }, + ], +}); + +linePlot.render(); diff --git a/examples/general/markerPoint/demo/meta.json b/examples/general/markerPoint/demo/meta.json new file mode 100644 index 0000000000..1b17a7c7b2 --- /dev/null +++ b/examples/general/markerPoint/demo/meta.json @@ -0,0 +1,18 @@ +{ + "title": { + "zh": "中文分类", + "en": "Category" + }, + "demos": [ + { + "filename": "basic.js", + "title": "带标注点的折线图", + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*b9T_Q5xpZgYAAAAAAAAAAABkARQnAQ" + }, + { + "filename": "multi-markerpoint.js", + "title": "多种类型标注点", + "screenshot": "https://gw.alipayobjects.com/mdn/rms_d314dd/afts/img/A*-WEbS6l8Xk0AAAAAAAAAAABkARQnAQ" + } + ] +} diff --git a/examples/general/markerPoint/demo/multi-markerpoint.js b/examples/general/markerPoint/demo/multi-markerpoint.js new file mode 100644 index 0000000000..39b256c5d0 --- /dev/null +++ b/examples/general/markerPoint/demo/multi-markerpoint.js @@ -0,0 +1,64 @@ +import { Line } from '@antv/g2plot'; + +const data = [ + { date: '2019-01-01', value: 3 }, + { date: '2019-02-01', value: 4 }, + { date: '2019-03-01', value: 3.5 }, + { date: '2019-04-01', value: 5 }, + { date: '2019-05-01', value: 4.9, festival: '劳动节' }, + { date: '2019-06-01', value: 6 }, + { date: '2019-07-01', value: 7 }, + { date: '2019-08-01', value: 9 }, + { date: '2019-09-01', value: -7, error: '异常' }, + { date: '2019-10-01', value: 13, festival: '国庆节' }, + { date: '2019-11-01', value: 13 }, + { date: '2019-12-01', value: 13 }, +]; + +const linePlot = new Line(document.getElementById('container'), { + title: { + visible: true, + text: '多种类型标注点', + }, + description: { + visible: true, + text: '在折线图上标注重点的数据,如节假日、异常点等', + }, + forceFit: true, + padding: 'auto', + data, + xField: 'date', + yField: 'value', + yAxis: { + nice: true, + }, + label: { + visible: false, + }, + markerPoints: [ + { + visible: true, + data: [{ date: '2019-05-01', value: 4.9 }, { date: '2019-10-01' }], + field: 'festival', + label: { + visible: true, + }, + }, + { + visible: true, + data: [{ date: '2019-09-01' }], + field: 'error', + symbol: 'cross', + label: { + visible: true, + position: 'bottom', + offsetY: 8, + }, + style: { + normal: { stroke: 'rgba(255, 0, 0, 0.65)', lineWidth: 2 }, + }, + }, + ], +}); + +linePlot.render(); diff --git a/examples/general/markerPoint/design.en.md b/examples/general/markerPoint/design.en.md new file mode 100644 index 0000000000..6d7c247589 --- /dev/null +++ b/examples/general/markerPoint/design.en.md @@ -0,0 +1,5 @@ +--- +title: 设计规范 +--- + +设计规范 diff --git a/examples/general/markerPoint/design.zh.md b/examples/general/markerPoint/design.zh.md new file mode 100644 index 0000000000..02c31c8d1e --- /dev/null +++ b/examples/general/markerPoint/design.zh.md @@ -0,0 +1,3 @@ +--- +title: 设计规范 +--- diff --git a/examples/general/markerPoint/index.en.md b/examples/general/markerPoint/index.en.md new file mode 100644 index 0000000000..74812addd3 --- /dev/null +++ b/examples/general/markerPoint/index.en.md @@ -0,0 +1,6 @@ +--- +title: MarkerPoint +order: 8 +--- + +Marker point in a chart, the items always are tied to points diff --git a/examples/general/markerPoint/index.zh.md b/examples/general/markerPoint/index.zh.md new file mode 100644 index 0000000000..12f700b2d3 --- /dev/null +++ b/examples/general/markerPoint/index.zh.md @@ -0,0 +1,6 @@ +--- +title: 图表标注 +order: 8 +--- + +图表标注, 标注点与数据点绑定 diff --git a/src/components/marker-point.ts b/src/components/marker-point.ts new file mode 100644 index 0000000000..8f5647daab --- /dev/null +++ b/src/components/marker-point.ts @@ -0,0 +1,286 @@ +import { View, IGroup, IShape, MarkerSymbols } from '../dependents'; +import { deepMix, isMatch, isString, isArray, each, find } from '@antv/util'; +import { MappingDatum } from '@antv/g2/lib/interface'; +import { Event } from '@antv/g2/lib/dependents'; + +interface PointStyle { + size?: number; + fill?: string; + stroke?: string; + lineWidth?: number; +} + +interface MarkerItem { + _origin: object; +} + +interface Cfg { + view: View; + data: any[]; + /** marker point 映射的字段 */ + field?: string; + symbol?: string | ((x: number, y: number, r: number) => any[][]); + size?: number; + label?: { + visible: boolean; + /** _origin: 原始数据 */ + formatter?: (text: string, item: MarkerItem, index: number) => string; + position?: 'top' | 'bottom'; + offsetX?: number; + offsetY?: number; + style?: object; + }; + style?: { + normal?: PointStyle; + active?: PointStyle; + selected?: PointStyle; + }; + events?: { + mouseenter?: (e: Event) => void; + mouseleave?: (e: Event) => void; + click?: (e: Event) => void; + }; +} + +const DEFAULT_STYLE = { + stroke: 'transparent', + fill: '#FCC509', + lineWidth: 0, +}; + +const ACTIVE_STYLE = { + stroke: '#FFF', + fill: '#FCC509', + lineWidth: 1, +}; + +const SELECTED_STYLE = { + stroke: 'rgba(0,0,0,0.85)', + fill: '#FCC509', + lineWidth: 1, +}; + +interface StateCondition { + id: string; + data: {}; +} +type State = 'active' | 'inactive' | 'selected'; + +export { Cfg as MarkerPointCfg }; + +/** + * 标注点 绘制在最顶层 + */ +export default class MarkerPoint { + public view: View; + public container: IGroup; + public config: Cfg; + private points: IShape[] = []; + private labels: IShape[] = []; + private size: number; + private name = 'markerPoints'; + private selectedPoint: IShape; + + protected defaultCfg = { + style: { normal: DEFAULT_STYLE, selected: SELECTED_STYLE, active: ACTIVE_STYLE }, + events: { + mouseenter: () => {}, + mouseleave: () => {}, + click: () => {}, + }, + label: { + visible: false, + offsetY: -8, + position: 'top', + style: { + fill: 'rgba(0, 0, 0, 0.85)', + }, + }, + }; + + constructor(cfg: Cfg) { + this.view = cfg.view; + this.size = cfg.size || 6; + this.config = deepMix({}, this.defaultCfg, cfg); + this._init(); + } + + public render() { + const dataArray = this.getDataArray(); + this._renderPoints(dataArray); + this.view.canvas.draw(); + this._addInteraction(); + } + + public clear() { + if (this.container) { + this.container.clear(); + } + } + + public destroy() { + if (this.container) { + this.container.remove(); + } + this.points = []; + this.labels = []; + } + + protected getDataArray(): MappingDatum[][] { + const geometry = this.view.geometries[0]; + return geometry.dataArray; + } + + private _init() { + const layer = this.view.foregroundGroup; + this.container = layer.addGroup(); + this.render(); + this.view.on('beforerender', () => { + this.clear(); + }); + } + + private _renderPoints(dataArray: MappingDatum[][]) { + each(this.config.data, (dataItem, dataItemIdx) => { + const origin = find(dataArray[0], (d) => isMatch(d._origin, dataItem)); + if (origin) { + const pointAttrs = this.config.style.normal; + const group = this.container.addGroup({ name: this.name }); + let { x, y } = origin; + if (isArray(x)) { + x = x[0]; + } + if (isArray(y)) { + y = y[0]; + } + const symbol = this.config.symbol; + const point = group.addShape({ + type: 'marker', + name: 'marker-point', + id: `point-${dataItemIdx}`, + attrs: { + x, + y, + r: this.size / 2, + ...pointAttrs, + symbol: isString(symbol) ? MarkerSymbols[symbol] : symbol, + }, + }); + this.points.push(point); + this._renderLabel(group, origin, dataItemIdx); + group.set('data', dataItem); + group.set('origin', origin); + } + }); + } + + private _renderLabel(container: IGroup, origin: MappingDatum, index) { + const { label: labelCfg, field } = this.config; + if (labelCfg && labelCfg.visible) { + const { offsetX = 0, offsetY = 0, formatter, position } = labelCfg; + let text = origin._origin[field]; + if (formatter) { + text = formatter(text, { _origin: origin._origin }, index); + } + const x = isArray(origin.x) ? origin.x[0] : origin.x; + const y = isArray(origin.y) ? origin.y[0] : origin.y; + const label = container.addShape('text', { + name: 'marker-label', + id: `label-${index}`, + attrs: { + x: x + offsetX, + y: y + offsetY, + text: text || '', + ...labelCfg.style, + textAlign: 'center', + textBaseline: position === 'top' ? 'bottom' : 'top', + }, + }); + this.labels.push(label); + } + } + + private _addInteraction() { + const { events } = this.config; + each(events, (cb, eventName) => { + this.container.on(`${this.name}:${eventName}`, (e) => { + cb(e); + const target = e.target.get('parent'); + const pointShape = target.get('children')[0]; + if (pointShape) { + const data = pointShape.get('data'); + const id = pointShape.get('id'); + const condition = { id, data }; + if (eventName === 'click') { + if (this.selectedPoint && this.selectedPoint.get('id') === id) { + this.selectedPoint = null; + this.setState('inactive', condition); + } else { + this.selectedPoint = pointShape; + this.setState('selected', condition); + } + } else if (eventName === 'mouseenter') { + this.setState('active', condition); + } else if (eventName === 'mouseleave') { + this.setState('inactive', condition); + } + } + this.view.canvas.draw(); + }); + this.view.on('click', (e) => { + const target = e.target.get('parent'); + if (!target || (target.get('name') !== this.name && this.selectedPoint)) { + this.selectedPoint = null; + this.setState('inactive'); + } + }); + }); + } + + private setState(state: State, condition?: StateCondition) { + if (state === 'active') { + if (!this.selectedPoint || condition.id !== this.selectedPoint.get('id')) { + this._onActive(condition); + } + } else if (state === 'inactive') { + this.points.forEach((p) => this._onInactive(p)); + } else if (state === 'selected') { + this._onSelected(condition); + } + } + + private _onActive(condition?: StateCondition) { + const { active } = this.config.style; + each(this.points, (point) => { + if (point.get('id') === condition.id) { + each(active, (v, k) => { + point.attr(k, v); + }); + } else { + this._onInactive(point); + } + }); + } + + private _onInactive(point: IShape) { + const { normal } = this.config.style; + if (!this.selectedPoint || point.get('id') !== this.selectedPoint.get('id')) { + each(normal, (v, k) => { + point.attr(k, v); + }); + } + } + + private _onSelected(condition: StateCondition) { + const { selected } = this.config.style; + each(this.points, (point) => { + if (point.get('id') === condition.id) { + each(selected, (v, k) => { + point.attr(k, v); + }); + } else { + this._onInactive(point); + } + }); + } +} diff --git a/src/dependents.ts b/src/dependents.ts index 62a84c46ef..a190347402 100644 --- a/src/dependents.ts +++ b/src/dependents.ts @@ -17,6 +17,7 @@ export { getShapeFactory, } from '@antv/g2'; export { VIEW_LIFE_CIRCLE } from '@antv/g2/lib/constant'; +export { MarkerSymbols } from '@antv/g2/lib/util/marker'; export { Datum, Data, diff --git a/src/plots/line/layer.ts b/src/plots/line/layer.ts index 2627b40423..d518f216c2 100644 --- a/src/plots/line/layer.ts +++ b/src/plots/line/layer.ts @@ -1,4 +1,4 @@ -import { deepMix, has, map, each } from '@antv/util'; +import { deepMix, has, map, each, get, some } from '@antv/util'; import { registerPlotType } from '../../base/global'; import { LayerConfig } from '../../base/layer'; import ViewLayer, { ViewConfig } from '../../base/view-layer'; @@ -10,6 +10,7 @@ import { getPlotOption } from './animation/clipIn-with-data'; import responsiveMethods from './apply-responsive'; import LineLabel from './component/label/line-label'; import * as EventParser from './event'; +import MarkerPoint, { MarkerPointCfg } from '../../components/marker-point'; import './theme'; import './apply-responsive/theme'; import { LooseMap } from '../../interface/types'; @@ -55,6 +56,9 @@ export interface LineViewConfig extends ViewConfig { color?: string; style?: PointStyle; }; + markerPoints?: (Omit & { + visible?: boolean; + })[]; xAxis?: IValueAxis | ICatAxis | ITimeAxis; yAxis?: IValueAxis; } @@ -88,16 +92,27 @@ export default class LineLayer exte position: 'top-left', wordSpacing: 4, }, + tooltip: { + crosshairs: { + line: { + style: { + stroke: 'rgba(0,0,0,0.45)', + }, + }, + }, + }, + markerPoints: [], }); } public line: any; // 保存line和point的配置项,用于后续的label、tooltip public point: any; public type: string = 'line'; + protected markerPoints: MarkerPoint[] = []; public afterRender() { - const props = this.options; - if (this.options.label && this.options.label.visible && this.options.label.type === 'line') { + const options = this.options; + if (options.label && options.label.visible && options.label.type === 'line') { const label = new LineLabel({ view: this.view, plot: this, @@ -105,8 +120,22 @@ export default class LineLayer exte }); label.render(); } + if (options.markerPoints) { + // 清空 + each(this.markerPoints, (markerPoint: MarkerPoint) => markerPoint.destroy()); + this.markerPoints = []; + options.markerPoints.forEach((markerPointOpt) => { + if (markerPointOpt.visible) { + const markerPoint = new MarkerPoint({ + ...markerPointOpt, + view: this.view, + }); + this.markerPoints.push(markerPoint); + } + }); + } // 响应式 - if (props.responsive && props.padding !== 'auto') { + if (options.responsive && options.padding !== 'auto') { this.applyResponsive('afterRender'); } super.afterRender();