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/)
+
+| [
](http:// godban.github.io/browsers-support-badges/)IE / Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](http://godban.github.io/browsers-support-badges/)Safari | [
](http://godban.github.io/browsers-support-badges/)Opera | [
](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/)
+
+| [
](http:// godban.github.io/browsers-support-badges/)IE / Edge | [
](http://godban.github.io/browsers-support-badges/)Firefox | [
](http://godban.github.io/browsers-support-badges/)Chrome | [
](http://godban.github.io/browsers-support-badges/)Safari | [
](http://godban.github.io/browsers-support-badges/)Opera | [
](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();