@fc-plot/ts-graph 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,192 @@
1
+ export type TsGraphType = {
2
+ [index: string]: any,
3
+ }
4
+
5
+ export type Timestamp = 'x' | 'X';
6
+
7
+ export type BaseOptions = {
8
+ xkey: number | string,
9
+ ykey: number | string,
10
+ timestamp: Timestamp,
11
+ }
12
+
13
+ export type Chart = {
14
+ id: string,
15
+ colors: string[],
16
+ width: number,
17
+ height: number,
18
+ marginTop: number,
19
+ marginRight: number,
20
+ marginBottom: number,
21
+ marginLeft: number,
22
+ renderTo: HTMLElement,
23
+ containerWidth: number,
24
+ containerHeight: number,
25
+ }
26
+
27
+ export type PlotLine = {
28
+ value: number,
29
+ color: string,
30
+ }
31
+
32
+ export type XAxis = {
33
+ visible: boolean,
34
+ lineColor: string,
35
+ lineWidth: number,
36
+ tickLength: number,
37
+ tickpadding: number,
38
+ tickColor: string,
39
+ labels: {
40
+ color: string,
41
+ fontSize: number,
42
+ timeFormat?: string, // %Y:%m:%d %H:%M:%S
43
+ },
44
+ plotLines: PlotLine[],
45
+ ticks?: Date[],
46
+ }
47
+
48
+ export type YAxis = {
49
+ visible: boolean,
50
+ min: number,
51
+ max: number,
52
+ lineColor: string,
53
+ lineWidth: number,
54
+ tickLength: number,
55
+ tickpadding: number,
56
+ tickColor: string,
57
+ gridLineColor: string,
58
+ labels: {
59
+ color: string,
60
+ shadowColor: string,
61
+ fontSize: number,
62
+ style: {
63
+ fontSize: number,
64
+ color: string,
65
+ },
66
+ },
67
+ plotLines: PlotLine[],
68
+ }
69
+
70
+ export type SerieDataItemMarker = {
71
+ enabled: boolean,
72
+ radius: number,
73
+ }
74
+
75
+ export type SerieDataItem = {
76
+ [index: string]: any,
77
+ marker?: SerieDataItemMarker,
78
+ timestamp: number,
79
+ value: number,
80
+ }
81
+
82
+ export interface Serie {
83
+ name: string,
84
+ color: string,
85
+ visible: boolean,
86
+ data: SerieDataItem[],
87
+ lineDash?: number[],
88
+ }
89
+
90
+ export type Series = Serie[]
91
+
92
+ export type Point = {
93
+ name: string,
94
+ color: string,
95
+ x: number,
96
+ y: number,
97
+ timestamp: number,
98
+ value: number,
99
+ serieIndex: number,
100
+ filledNull?: number,
101
+ serieOptions: Serie,
102
+ }
103
+
104
+ export type Tooltip = {
105
+ precision: number | 'origin' | 'short';
106
+ shared: boolean,
107
+ sharedSortDirection: 'asc' | 'desc',
108
+ formatter: (points: Point[], originalPoints: Point[]) => string;
109
+ }
110
+
111
+ export type Legend = {
112
+ align: string,
113
+ verticalAlign: string,
114
+ enabled: boolean,
115
+ }
116
+
117
+ export type Time = {
118
+ timezoneOffset: number,
119
+ }
120
+
121
+ export type NearestPoint = {
122
+ name: string,
123
+ color: string,
124
+ timestamp: number,
125
+ value: number,
126
+ serieIndex: number,
127
+ filledNull?: number,
128
+ serieOptions: Serie,
129
+ }
130
+
131
+ export interface GetNearestPointsFnParam extends BaseOptions {
132
+ series: Series,
133
+ x: Date,
134
+ fillNull: undefined | number,
135
+ }
136
+
137
+ export interface Options extends BaseOptions {
138
+ type: 'line' | 'area' | 'lineAndArea',
139
+ ratio: number,
140
+ chart: Chart,
141
+ xAxis: XAxis,
142
+ yAxis: YAxis,
143
+ tooltip: Tooltip,
144
+ line: {
145
+ width: number,
146
+ },
147
+ series: Serie[],
148
+ notDisplayedSeries: string[],
149
+ legend: Legend,
150
+ time: Time,
151
+ fillNull: undefined | number,
152
+ xmin: number,
153
+ xmax: number,
154
+ ymin: number,
155
+ ymax: number,
156
+ onClick: (d3Event: any) => any,
157
+ onZoom: (getZoomedSeries: () => Series) => any,
158
+ }
159
+
160
+ export type EventPosition = {
161
+ clientX: number;
162
+ clientY: number;
163
+ offsetX: number;
164
+ offsetY: number;
165
+ layerX: number;
166
+ layerY: number;
167
+ }
168
+
169
+ export type Transform = {
170
+ rescaleX: (xScales: any) => any,
171
+ } | undefined
172
+
173
+ export interface WorkerPostMessage extends BaseOptions {
174
+ id?: string,
175
+ str?: string,
176
+ flag?: boolean,
177
+ x: Date,
178
+ fillNull: undefined | number,
179
+ }
180
+
181
+ export interface XScales {
182
+ (a: number | Date): number,
183
+ invert(value: number | { valueOf(): number }): Date,
184
+ ticks(value: number): Date[],
185
+ }
186
+
187
+ export interface YScales {
188
+ (a: number): number,
189
+ domain(value: number[]): any,
190
+ nice(value: number): any,
191
+ ticks(value: number): number[],
192
+ }
package/src/legend.ts ADDED
@@ -0,0 +1,97 @@
1
+ import { Options } from './interface';
2
+
3
+ type LegendItemClickFuncType = (name: string) => any;
4
+
5
+ function findIndex(elem: HTMLElement) {
6
+ let k = -1;
7
+ let e = elem;
8
+ while (e) {
9
+ if ('previousSibling' in e) {
10
+ e = e.previousSibling as HTMLElement;
11
+ k += 1;
12
+ } else {
13
+ k = -1;
14
+ break;
15
+ }
16
+ }
17
+ return k;
18
+ }
19
+
20
+ export default class Legend {
21
+ options: Options;
22
+ onLegendItemClick: LegendItemClickFuncType;
23
+ constructor(userOptions: Options, onLegendItemClick: LegendItemClickFuncType, container: HTMLElement, canvas: HTMLCanvasElement) {
24
+ this.options = userOptions;
25
+ this.init(container, canvas);
26
+ this.onLegendItemClick = onLegendItemClick;
27
+ }
28
+
29
+ init(container: HTMLElement, canvas: HTMLCanvasElement) {
30
+ const { chart: { id } } = this.options;
31
+ const legendNode = document.createElement('div');
32
+
33
+ legendNode.id = `${id}-legend`;
34
+ legendNode.className = 'ts-graph-legend';
35
+ if (!document.getElementById(legendNode.id)) {
36
+ container.insertBefore(legendNode, canvas);
37
+ }
38
+ this.initEvent();
39
+ }
40
+
41
+ initEvent() {
42
+ const { chart: { id } } = this.options;
43
+ const legendNode = document.getElementById(`${id}-legend`) as HTMLElement;
44
+ legendNode.removeEventListener('click', this.handelLegendItemClick);
45
+ legendNode.addEventListener('click', this.handelLegendItemClick);
46
+ }
47
+
48
+ updateOptions(newOptions: Options) {
49
+ this.options = newOptions;
50
+ }
51
+
52
+ handelLegendItemClick = (e: MouseEvent) => {
53
+ const currentTarget = e.currentTarget as HTMLElement;
54
+ let target = e.target as HTMLElement;
55
+ while (target !== currentTarget) {
56
+ if (target && target.className === 'ts-graph-legend-item') {
57
+ const { series } = this.options;
58
+ const index = findIndex(target);
59
+ this.onLegendItemClick(series[index].name);
60
+ }
61
+ if (target.parentNode) {
62
+ target = target.parentNode as HTMLElement;
63
+ }
64
+ }
65
+ }
66
+
67
+ draw() {
68
+ const { chart: { id }, series, legend } = this.options;
69
+ const legendNode = document.getElementById(`${id}-legend`)!;
70
+
71
+ if (!legend.enabled) return;
72
+ if (Object.prototype.toString.call(series) === '[object Array]') {
73
+ legendNode.innerHTML = '';
74
+ const frag = document.createDocumentFragment();
75
+ series.forEach((serie) => {
76
+ const legendItemNode = document.createElement('span');
77
+ const symbolNode = document.createElement('span');
78
+ const symbolLineNode = document.createElement('span');
79
+ const symbolPointNode = document.createElement('span');
80
+ const legendTextNode = document.createTextNode(serie.name);
81
+
82
+ legendItemNode.className = 'ts-graph-legend-item';
83
+ symbolNode.className = 'ts-graph-legend-item-symbol';
84
+ symbolLineNode.className = 'ts-graph-legend-item-symbol-line';
85
+ symbolLineNode.style.borderColor = serie.color;
86
+ symbolPointNode.className = 'ts-graph-legend-item-symbol-point';
87
+ symbolPointNode.style.backgroundColor = serie.color;
88
+ symbolNode.appendChild(symbolLineNode);
89
+ symbolNode.appendChild(symbolPointNode);
90
+ legendItemNode.appendChild(symbolNode);
91
+ legendItemNode.appendChild(legendTextNode);
92
+ frag.appendChild(legendItemNode);
93
+ });
94
+ legendNode.appendChild(frag);
95
+ }
96
+ }
97
+ }
package/src/line.ts ADDED
@@ -0,0 +1,92 @@
1
+ import * as d3 from 'd3';
2
+ import { sortBy } from 'lodash';
3
+ import { getColor } from './utils';
4
+ import { Options, Serie, SerieDataItem, XScales, YScales } from './interface';
5
+
6
+ export default class Line {
7
+ options: Options;
8
+ ctx: CanvasRenderingContext2D;
9
+ constructor(userOptions: Options, ctx: CanvasRenderingContext2D) {
10
+ this.options = userOptions;
11
+ this.ctx = ctx;
12
+ }
13
+
14
+ draw(xScales: XScales, yScales: YScales) {
15
+ const { type, series, chart: { colors, width, height }, xAxis, xkey, ykey, timestamp, fillNull, notDisplayedSeries } = this.options;
16
+ const { ctx } = this;
17
+
18
+ sortBy(series, 'zIndex').forEach((serie: Serie, i: number) => {
19
+ if (serie.visible === false) return;
20
+ if (notDisplayedSeries.indexOf(serie.name) > -1) return;
21
+
22
+ const color = serie.color || getColor(colors, i);
23
+
24
+ serie.color = color;
25
+
26
+ if (type === 'area' || type === 'lineAndArea') {
27
+ ctx.beginPath();
28
+ const area = d3.area().x((d: SerieDataItem) => {
29
+ const xVal = timestamp === 'X' ? d[xkey] * 1000 : d[xkey];
30
+ const x = xScales(new Date(xVal));
31
+ return x;
32
+ }).y0(height - xAxis.tickpadding - xAxis.labels.fontSize - 5)
33
+ .y1((d: SerieDataItem) => {
34
+ const val = d[ykey];
35
+ if (typeof val === 'number') {
36
+ return yScales(val);
37
+ } else if (typeof fillNull === 'number') {
38
+ return yScales(fillNull);
39
+ }
40
+ return undefined;
41
+ }).defined((d: SerieDataItem) => {
42
+ const val = d[ykey];
43
+ return typeof val === 'number' || typeof fillNull === 'number';
44
+ }).context(ctx);
45
+
46
+ area(serie.data || [])
47
+
48
+
49
+ const grd = ctx.createLinearGradient(0, 0, 0, height);
50
+ grd.addColorStop(0, color);
51
+ grd.addColorStop(0.2, color);
52
+ grd.addColorStop(1, 'rgb(255,255,255, 0)');
53
+ ctx.fillStyle = grd;
54
+ ctx.fill();
55
+ ctx.closePath();
56
+ }
57
+
58
+ if (type === 'line' || type === 'lineAndArea') {
59
+ ctx.beginPath();
60
+ const line = d3.line().x((d: SerieDataItem) => {
61
+ const xVal = timestamp === 'X' ? d[xkey] * 1000 : d[xkey];
62
+ const x = xScales(new Date(xVal));
63
+
64
+ return x;
65
+ }).y((d: SerieDataItem) => {
66
+ const val = d[ykey];
67
+ if (typeof val === 'number') {
68
+ return yScales(val);
69
+ } else if (typeof fillNull === 'number') {
70
+ return yScales(fillNull);
71
+ }
72
+ return undefined;
73
+ }).defined((d: SerieDataItem) => {
74
+ const val = d[ykey];
75
+ return typeof val === 'number' || typeof fillNull === 'number';
76
+ }).context(ctx);
77
+
78
+ ctx.lineTo(0, 0); // TODO: fix don't draw the first single point
79
+
80
+ line(serie.data || []);
81
+
82
+ ctx.lineJoin = 'round';
83
+ ctx.lineWidth = this.options.line.width;
84
+ ctx.strokeStyle = color;
85
+ ctx.lineCap = 'round';
86
+ ctx.setLineDash(serie.lineDash || []);
87
+ ctx.stroke();
88
+ ctx.closePath();
89
+ }
90
+ });
91
+ }
92
+ }
package/src/tooltip.ts ADDED
@@ -0,0 +1,318 @@
1
+ import * as d3 from 'd3';
2
+ import { groupBy, keys, orderBy, isNumber } from 'lodash';
3
+ import { distanceBetweenPointsX, distanceBetweenPoints, getColor, formatTrim } from './utils';
4
+ import { Options, EventPosition, Point, WorkerPostMessage, XScales, YScales } from './interface';
5
+ import Worker from "worker-loader?inline=true!./worker.ts";
6
+
7
+ export default class Tooltip {
8
+ options: Options;
9
+ ctx: CanvasRenderingContext2D;
10
+ flag: boolean;
11
+ container: any;
12
+ isMouserover: boolean;
13
+ worker: any;
14
+ constructor(userOptions: Options, ctx: CanvasRenderingContext2D, container: HTMLElement, canvas: HTMLCanvasElement) {
15
+ this.options = userOptions;
16
+ this.ctx = ctx;
17
+ this.flag = false;
18
+ this.container = container;
19
+ this.init(container, canvas);
20
+ this.isMouserover = false;
21
+ this.worker = new Worker();
22
+ }
23
+
24
+ init(container: HTMLElement, canvas: HTMLCanvasElement) {
25
+ const { chart: { id } } = this.options;
26
+ const tooltipNode = document.createElement('div');
27
+
28
+ tooltipNode.id = `${id}-tooltip`;
29
+ tooltipNode.className = 'ts-graph-tooltip';
30
+ // 插到 eventCanvas 前,事件是绑定在 eventCanvas 层,防止 tooltip 遮挡
31
+ if (!document.getElementById(tooltipNode.id)) {
32
+ // container.insertBefore(tooltipNode, canvas);
33
+ document.body.append(tooltipNode);
34
+ }
35
+ }
36
+
37
+ clear() {
38
+ const { ratio, chart: { id, containerWidth, containerHeight } } = this.options;
39
+ const tooltipNode = document.getElementById(`${id}-tooltip`) as HTMLElement;
40
+
41
+ this.isMouserover = false;
42
+ this.ctx.save();
43
+ this.ctx.setTransform(1 * ratio, 0, 0, 1 * ratio, 0, 0);
44
+ this.ctx.clearRect(0, 0, containerWidth, containerHeight);
45
+ this.ctx.restore();
46
+ tooltipNode.style.top = '-99999px';
47
+ // tooltipNode.style.visibility = 'hidden';
48
+ if (tooltipNode.lastChild) {
49
+ tooltipNode.removeChild(tooltipNode.lastChild);
50
+ }
51
+ }
52
+
53
+ getNearestPoints(eventPosition: EventPosition, xScales: XScales, yScales: YScales, cbk: (nearestPoints: Point[]) => void) {
54
+ const { series = [], chart: { id, colors }, tooltip: { shared }, xkey, ykey, timestamp, fillNull } = this.options;
55
+ const x = xScales.invert(eventPosition.offsetX);
56
+ let nearestPoints: Point[] = [];
57
+ const message: WorkerPostMessage = {
58
+ x,
59
+ xkey,
60
+ ykey,
61
+ timestamp,
62
+ fillNull,
63
+ };
64
+
65
+ if (!this.flag) {
66
+ message.id = id;
67
+ message.str = JSON.stringify(series);
68
+ message.flag = this.flag;
69
+ this.worker.postMessage(message);
70
+ this.flag = true;
71
+ } else {
72
+ message.id = id;
73
+ message.flag = this.flag;
74
+ this.worker.postMessage(message);
75
+ }
76
+
77
+ this.worker.onmessage = (event: any) => {
78
+ if (this.isMouserover === false) return;
79
+
80
+ if (Object.prototype.toString.call(event.data) === '[object Array]') {
81
+ nearestPoints = event.data.map((item: Point) => {
82
+ return {
83
+ ...item,
84
+ x: xScales(item.timestamp),
85
+ y: yScales(item.value),
86
+ color: item.color || getColor(colors, item.serieIndex),
87
+ };
88
+ });
89
+ }
90
+ // 不同时间点数据,还需要再处理一遍
91
+ const nearestPointsGroup = groupBy(nearestPoints, 'x');
92
+
93
+ if (keys(nearestPointsGroup).length > 1) {
94
+ let minDistance = Number.POSITIVE_INFINITY;
95
+
96
+ // eslint-disable-next-line no-restricted-syntax
97
+ for (const key in nearestPointsGroup) {
98
+ if (Object.prototype.hasOwnProperty.call(nearestPointsGroup, key)) {
99
+ const d = distanceBetweenPointsX(eventPosition.offsetX, Number(key));
100
+
101
+ if (d < minDistance) {
102
+ minDistance = d;
103
+ nearestPoints = nearestPointsGroup[key];
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ if (!shared) {
110
+ let minDistance = Number.POSITIVE_INFINITY;
111
+
112
+ nearestPoints.forEach((point) => {
113
+ const d = distanceBetweenPoints({
114
+ x: eventPosition.offsetX,
115
+ y: eventPosition.offsetY,
116
+ }, point);
117
+
118
+ if (d < minDistance) {
119
+ minDistance = d;
120
+ nearestPoints = [point];
121
+ }
122
+ });
123
+ }
124
+
125
+ cbk(nearestPoints);
126
+ };
127
+ }
128
+
129
+ draw(eventPosition: EventPosition, xScales: XScales, yScales: YScales, cbk?: (nearestPoints: Point[]) => void) {
130
+ this.isMouserover = true;
131
+ this.getNearestPoints(eventPosition, xScales, yScales, (nearestPoints) => {
132
+ this.clear();
133
+ if (nearestPoints.length) {
134
+ this.drawCrosshair(nearestPoints[0].x);
135
+ this.drawSymbol(nearestPoints);
136
+ this.drawModal(nearestPoints, eventPosition);
137
+ }
138
+ if (cbk && Object.prototype.toString.call(cbk) === '[object Function]') {
139
+ cbk(nearestPoints);
140
+ }
141
+ });
142
+ }
143
+
144
+ drawModal(nearestPoints: Point[], eventPosition: EventPosition) {
145
+ const { chart: { id, renderTo }, tooltip, time } = this.options;
146
+ const renderToWidth = renderTo.offsetWidth;
147
+ let renderToHeight = renderTo.offsetHeight;
148
+ const tooltipNode = document.getElementById(`${id}-tooltip`) as HTMLElement;
149
+ const wrapEle = document.createElement('div');
150
+ const originalNearestPoints = nearestPoints;
151
+
152
+ renderToHeight = window.innerHeight / 1.5;
153
+
154
+ // 最大可显示的行数,超出最大行数就在中位隐藏超出数量的行
155
+ // 18: padding + border
156
+ // 100: 缓冲区域
157
+ // 15: 每行高度
158
+ // + 1: 标题栏高度
159
+ const maxLength = (renderToHeight - 18 - 100) / 15;
160
+ let overflow = false;
161
+
162
+ if (tooltip.sharedSortDirection) {
163
+ nearestPoints = orderBy(nearestPoints, (point: Point) => {
164
+ return point.value;
165
+ }, tooltip.sharedSortDirection);
166
+ }
167
+
168
+ if (nearestPoints.length > maxLength) {
169
+ nearestPoints = nearestPoints.slice(0, maxLength);
170
+ overflow = true;
171
+ }
172
+
173
+ wrapEle.className = 'ts-graph-tooltip-content';
174
+ tooltipNode.appendChild(wrapEle);
175
+
176
+ if (Object.prototype.toString.call(tooltip.formatter) === '[object Function]') {
177
+ wrapEle.innerHTML = tooltip.formatter([...nearestPoints], [...originalNearestPoints]);
178
+ } else {
179
+ const firstNearestPoint = nearestPoints[0];
180
+ const frag = document.createDocumentFragment();
181
+ const ulNode = document.createElement('ul');
182
+ const headerNode = document.createElement('li');
183
+
184
+ let tzD = new Date(firstNearestPoint.timestamp);
185
+
186
+ if (time && time.timezoneOffset) {
187
+ const timezoneOffset = tzD.getTimezoneOffset();
188
+ const ts = tzD.getTime();
189
+ const destTs = ts + (timezoneOffset * 60 * 1000) + (time.timezoneOffset * 60 * 1000);
190
+ tzD = new Date(destTs);
191
+ }
192
+ const headerTextNode = document.createTextNode(d3.timeFormat('%Y-%m-%d %H:%M:%S')(tzD));
193
+
194
+ headerNode.appendChild(headerTextNode);
195
+ headerNode.style.color = '#666';
196
+ ulNode.style.maxWidth = `${window.innerWidth / 1.5}px`; // 宽度最大值
197
+ ulNode.appendChild(headerNode);
198
+ frag.appendChild(ulNode);
199
+
200
+ nearestPoints.forEach(({ color, name, value, filledNull }) => {
201
+ const liNode = document.createElement('li');
202
+
203
+ if (color) {
204
+ const symbolNode = document.createElement('span');
205
+ const symbolTextNode = document.createTextNode('● ');
206
+
207
+ symbolNode.style.color = color;
208
+ symbolNode.appendChild(symbolTextNode);
209
+ liNode.appendChild(symbolNode);
210
+ }
211
+
212
+ if (name) {
213
+ const nameTextNode = document.createTextNode(`${name}: `);
214
+
215
+ liNode.appendChild(nameTextNode);
216
+ }
217
+
218
+ if (isNumber(value)) {
219
+ const valueNode = document.createElement('strong');
220
+ let formatValue
221
+ if(tooltip.precision === 'origin'){
222
+ formatValue = value
223
+ }else if (tooltip.precision === 'short'){
224
+ let text = d3.format('.5s')(value);
225
+ formatValue = formatTrim(text)
226
+ }else if (tooltip.precision > 0){
227
+ formatValue = d3.format(',.'+tooltip.precision+'f')(value)
228
+ }else{
229
+ formatValue = d3.format(',.3f')(value)
230
+ }
231
+ formatValue += filledNull ? '(空值填补,仅限看图使用)' : ''
232
+
233
+ const valueTextNode = document.createTextNode(formatValue);
234
+
235
+ valueNode.appendChild(valueTextNode);
236
+ liNode.appendChild(valueNode);
237
+ }
238
+ ulNode.appendChild(liNode);
239
+ });
240
+
241
+ if (overflow) {
242
+ const overflowLiNode = document.createElement('li');
243
+ const overflowLiTextNode = document.createTextNode('......');
244
+
245
+ overflowLiNode.appendChild(overflowLiTextNode);
246
+ ulNode.appendChild(overflowLiNode);
247
+ }
248
+
249
+ wrapEle.appendChild(frag);
250
+ }
251
+
252
+ const tooltipWidth = wrapEle.offsetWidth;
253
+ const tooltipHeight = wrapEle.offsetHeight;
254
+ const containerRect = this.container.getBoundingClientRect();
255
+ const clientX = eventPosition.layerX + containerRect.left;
256
+ const clientY = eventPosition.layerY + containerRect.top;
257
+
258
+ tooltipNode.style.left = `${clientX - tooltipWidth - 20}px`;
259
+ tooltipNode.style.top = `${clientY + 20}px`;
260
+
261
+ if (clientX - tooltipWidth - 20 < 0) {
262
+ tooltipNode.style.left = `${clientX + 20}px`;
263
+ if ((clientX + 20 + tooltipWidth) > renderToWidth) {
264
+ tooltipNode.style.left = '0px';
265
+ }
266
+ }
267
+
268
+ if ((clientY + 20 + tooltipHeight) > renderToHeight) {
269
+ if (clientY - tooltipHeight - 20 < 0) {
270
+ tooltipNode.style.top = '0px';
271
+ } else {
272
+ tooltipNode.style.top = `${clientY - tooltipHeight - 20}px`;
273
+ }
274
+ }
275
+
276
+ tooltipNode.style.zIndex = '9999';
277
+ tooltipNode.style.visibility = 'visible';
278
+ }
279
+
280
+ drawSymbol(points: Point[] = []) {
281
+ const { ctx } = this;
282
+
283
+ points.forEach((point) => {
284
+ const { x, y, color } = point;
285
+ ctx.beginPath();
286
+ ctx.arc(x, y, 5, 0, 360, false);
287
+ ctx.lineWidth = 1;
288
+ ctx.fillStyle = color;
289
+ ctx.fill();
290
+ ctx.beginPath();
291
+ ctx.arc(x, y, 3, 0, 360, false);
292
+ ctx.lineWidth = 1;
293
+ ctx.fillStyle = 'white';
294
+ ctx.fill();
295
+ });
296
+ }
297
+
298
+ drawCrosshair(x: number) {
299
+ const { ctx, options } = this;
300
+ const { chart, xAxis } = options;
301
+ const tickBottom = chart.height - xAxis.tickpadding - xAxis.labels.fontSize - xAxis.tickLength;
302
+
303
+ ctx.beginPath();
304
+ ctx.moveTo(x, 0);
305
+ ctx.lineTo(x, tickBottom);
306
+ ctx.lineWidth = 1;
307
+ ctx.strokeStyle = xAxis.lineColor;
308
+ ctx.stroke();
309
+ }
310
+
311
+ destroy() {
312
+ const { chart: { id } } = this.options;
313
+ const tooltipNode = document.getElementById(`${id}-tooltip`) as HTMLElement;
314
+ if (tooltipNode && tooltipNode.parentNode) {
315
+ tooltipNode.parentNode.removeChild(tooltipNode);
316
+ }
317
+ }
318
+ }