@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.
package/src/utils.ts ADDED
@@ -0,0 +1,74 @@
1
+ import { Timestamp } from './interface';
2
+
3
+ type Point = {
4
+ x: number,
5
+ y: number,
6
+ };
7
+
8
+ export function distanceBetweenPointsX(x1: number, x2: number): number {
9
+ return Math.sqrt(Math.pow(x2 - x1, 2));
10
+ }
11
+
12
+ export function distanceBetweenPoints(pt1: Point, pt2: Point) {
13
+ return Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2));
14
+ }
15
+
16
+ export function getColor(colors: string[], i: number): string {
17
+ const len = colors.length;
18
+ if (i < len) {
19
+ return colors[i];
20
+ }
21
+ return getColor(colors, i - len);
22
+ }
23
+
24
+ export function dateFormat(d: Date) {
25
+ return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}
26
+ ${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}`;
27
+ }
28
+
29
+ export function getRatio() {
30
+ return window.devicePixelRatio ? window.devicePixelRatio : 2;
31
+ }
32
+
33
+ export function dateStrToTs(date: string) {
34
+ return (new Date(date)).getTime();
35
+ }
36
+
37
+ export function getMsTs(ts: number, type: Timestamp) {
38
+ return type === 'X' ? ts * 1000 : ts;
39
+ }
40
+
41
+ // 获取在移动端的偏移位置
42
+ export function getTouchPosition(el: HTMLCanvasElement, d3Event: any, idx = 0) {
43
+ const vertex = el.getBoundingClientRect();
44
+ const clientX = d3Event.touches[idx].clientX;
45
+ const clientY = d3Event.touches[idx].clientY;
46
+ const x = clientX - vertex.left;
47
+ const y = clientY - vertex.top;
48
+ return {
49
+ offsetX: x,
50
+ offsetY: y,
51
+ clientX,
52
+ clientY,
53
+ };
54
+ }
55
+
56
+ export function formatTrim(s: string) {
57
+
58
+ out: for (var n = s.length, i = 1, i0 = -1, i1; i < n; ++i) {
59
+
60
+ switch (s[i]) {
61
+
62
+ case ".": i0 = i1 = i; break;
63
+
64
+ case "0": if (i0 === 0) i0 = i; i1 = i; break;
65
+
66
+ default: if (i0 > 0) { if (!+s[i]) break out; i0 = 0; } break;
67
+
68
+ }
69
+
70
+ }
71
+
72
+ return i0 > 0 ? s.slice(0, i0) + s.slice(i1 + 1) : s;
73
+
74
+ }
package/src/worker.ts ADDED
@@ -0,0 +1,37 @@
1
+ import getNearestPoints from './getNearestPoints';
2
+ import { Serie } from './interface';
3
+
4
+ type CacheType = {
5
+ [index: string]: string;
6
+ };
7
+ type SeriesType = {
8
+ [index: string]: Serie[];
9
+ };
10
+
11
+ const cache: CacheType = {};
12
+ const series: SeriesType = {};
13
+ const ctx: Worker = self as any;
14
+
15
+ ctx.addEventListener('message', (event) => {
16
+ const { data } = event;
17
+
18
+ if (data.id) {
19
+ if (!cache[data.id] || !data.flag) {
20
+ cache[data.id] = data.str;
21
+ series[data.id] = JSON.parse(data.str);
22
+ }
23
+ }
24
+
25
+ const nearestPoints = getNearestPoints({
26
+ x: data.x,
27
+ xkey: data.xkey,
28
+ ykey: data.ykey,
29
+ timestamp: data.timestamp,
30
+ series: series[data.id],
31
+ fillNull: data.fillNull,
32
+ });
33
+
34
+ ctx.postMessage(nearestPoints); // TODO: targetOrigin
35
+ });
36
+
37
+ export default ctx;
package/src/xAxis.ts ADDED
@@ -0,0 +1,119 @@
1
+ import * as d3 from 'd3';
2
+ import { getMsTs } from './utils';
3
+ import { Options, XScales } from './interface';
4
+
5
+ export default class XAxis {
6
+ options: Options;
7
+
8
+ ctx: CanvasRenderingContext2D;
9
+
10
+ constructor(userOptions: Options, ctx: CanvasRenderingContext2D) {
11
+ this.options = userOptions;
12
+ this.ctx = ctx;
13
+ }
14
+
15
+ init() {
16
+ const {
17
+ chart: { width }, timestamp, xmin, xmax,
18
+ } = this.options;
19
+ const xScales = d3.scaleTime().range([0, width]);
20
+
21
+ xScales.domain([
22
+ getMsTs(xmax, timestamp) + (xmin === xmax ? 1000 : 0), // 只有一个点的时候居中显示
23
+ getMsTs(xmin, timestamp) - (xmin === xmax ? 1000 : 0),
24
+ ]);
25
+ return xScales;
26
+ }
27
+
28
+ draw(xScales: XScales) {
29
+ const { ctx, options } = this;
30
+ const { chart, xAxis, time } = options;
31
+ let ticks = xScales.ticks(Math.floor(chart.width / 90));
32
+ if (xAxis.ticks) {
33
+ // eslint-disable-next-line prefer-destructuring
34
+ ticks = xAxis.ticks;
35
+ }
36
+ const tickBottom = chart.height - xAxis.tickpadding - xAxis.labels.fontSize;
37
+
38
+ // draw tick
39
+ ctx.beginPath();
40
+ ticks.forEach((d) => {
41
+ const xScalesedVal = xScales(d);
42
+ if (xScalesedVal) {
43
+ ctx.moveTo(xScalesedVal, tickBottom);
44
+ ctx.lineTo(xScalesedVal, tickBottom - xAxis.tickLength);
45
+ }
46
+ });
47
+ ctx.lineWidth = 1;
48
+ ctx.strokeStyle = xAxis.tickColor;
49
+ ctx.stroke();
50
+
51
+ // draw line
52
+ ctx.beginPath();
53
+ ctx.moveTo(0, tickBottom - xAxis.tickLength);
54
+ ctx.lineTo(chart.width, tickBottom - xAxis.tickLength);
55
+ ctx.lineWidth = 1;
56
+ ctx.strokeStyle = xAxis.lineColor;
57
+ ctx.stroke();
58
+
59
+ // draw labels
60
+ ctx.textAlign = 'center';
61
+ ctx.textBaseline = 'bottom';
62
+ ctx.fillStyle = xAxis.labels.color;
63
+ ctx.font = `${xAxis.labels.fontSize}px Palantino`;
64
+ ticks.forEach((d, idx) => {
65
+ let timeFormat = '%H:%M';
66
+ if (d.getSeconds() !== 0) {
67
+ timeFormat = '%H:%M:%S';
68
+ }
69
+ if (d.getHours() === 0 && d.getMinutes() === 0 && d.getSeconds() === 0) {
70
+ timeFormat = '%m-%d';
71
+ }
72
+
73
+ if (xAxis.labels.timeFormat) {
74
+ // eslint-disable-next-line prefer-destructuring
75
+ timeFormat = xAxis.labels.timeFormat;
76
+ }
77
+
78
+ let tzD = d;
79
+
80
+ if (time && time.timezoneOffset) {
81
+ const timezoneOffset = d.getTimezoneOffset();
82
+ const ts = d.getTime();
83
+ const destTs = ts + (timezoneOffset * 60 * 1000) + (time.timezoneOffset * 60 * 1000);
84
+ tzD = new Date(destTs);
85
+ }
86
+ // TODO: 临时支持 ticks
87
+ if (xAxis.ticks && xAxis.ticks.length > 1) {
88
+ let x = xScales(d);
89
+ if (idx === 0) {
90
+ x += (timeFormat.length * xAxis.labels.fontSize) / 4 + 1;
91
+ } else if (idx === ticks.length - 1) {
92
+ x -= (timeFormat.length * xAxis.labels.fontSize) / 4 - 1;
93
+ }
94
+ ctx.fillText(d3.timeFormat(timeFormat)(tzD), x, chart.height);
95
+ } else if (xScales(d)) {
96
+ ctx.fillText(d3.timeFormat(timeFormat)(tzD), xScales(d), chart.height);
97
+ }
98
+ });
99
+ }
100
+
101
+ drawPlotLines(xScales: XScales) {
102
+ const { ctx, options } = this;
103
+ const { chart, xAxis, xAxis: { plotLines } } = options;
104
+ const tickBottom = chart.height - xAxis.tickpadding - xAxis.labels.fontSize;
105
+
106
+ if (Array.isArray(plotLines)) {
107
+ plotLines.forEach((plotLine) => {
108
+ const xValue = xScales(plotLine.value);
109
+ ctx.setLineDash([5, 3]);
110
+ ctx.beginPath();
111
+ ctx.moveTo(xValue, 0);
112
+ ctx.lineTo(xValue, tickBottom - xAxis.tickLength);
113
+ ctx.strokeStyle = plotLine.color;
114
+ ctx.stroke();
115
+ ctx.setLineDash([]);
116
+ });
117
+ }
118
+ }
119
+ }
package/src/yAxis.ts ADDED
@@ -0,0 +1,178 @@
1
+ import * as d3 from 'd3';
2
+ import { Options, YScales } from './interface';
3
+
4
+ interface RealData {
5
+ [index: string]: any;
6
+ }
7
+
8
+ export default class YAxis {
9
+ options: Options;
10
+
11
+ ctx: CanvasRenderingContext2D;
12
+
13
+ tickLength: number;
14
+
15
+ ticks: number[];
16
+
17
+ constructor(userOptions: Options, ctx: CanvasRenderingContext2D) {
18
+ this.options = userOptions;
19
+ this.ctx = ctx;
20
+ this.tickLength = Math.floor(userOptions.chart.height / 50);
21
+ this.ticks = [];
22
+ }
23
+
24
+ init() {
25
+ const { chart, xAxis } = this.options;
26
+ const yScales = d3.scaleLinear().range([chart.height - (xAxis.labels.fontSize + xAxis.tickpadding + xAxis.tickLength), 20]);
27
+
28
+ this.setDomain(yScales, this.options);
29
+ return yScales;
30
+ }
31
+
32
+ /**
33
+ * 返回真实的数据,其中包含 plotLines
34
+ * @param {Array} data [description]
35
+ * @return {Array} [description]
36
+ */
37
+ getRealData(data: RealData) {
38
+ const { yAxis: { plotLines }, ykey, fillNull } = this.options;
39
+
40
+ if (Array.isArray(data)) {
41
+ if (Array.isArray(plotLines)) {
42
+ plotLines.forEach((plotLine) => {
43
+ if (typeof plotLine.value === 'number') {
44
+ data.push({
45
+ [ykey]: plotLine.value,
46
+ });
47
+ }
48
+ });
49
+ }
50
+ if (typeof fillNull === 'number') {
51
+ data.push({
52
+ [ykey]: fillNull,
53
+ });
54
+ }
55
+ return data;
56
+ }
57
+ return [];
58
+ }
59
+
60
+ getPlotLinesMaxAbs() {
61
+ const { yAxis: { plotLines } } = this.options;
62
+ let maxAbs: undefined | number;
63
+ if (Array.isArray(plotLines)) {
64
+ plotLines.forEach((plotLine) => {
65
+ if (!maxAbs || Math.abs(maxAbs) < Math.abs(plotLine.value)) {
66
+ maxAbs = plotLine.value;
67
+ }
68
+ });
69
+ }
70
+ return maxAbs;
71
+ }
72
+
73
+ setDomain(yScales: YScales, { ymin, ymax }: { ymin: number, ymax: number}) {
74
+ const plotLinesMaxAbs = this.getPlotLinesMaxAbs();
75
+ let realTickLength = this.tickLength;
76
+
77
+ // TODO: Optimize the tick
78
+ if (ymin === ymax) {
79
+ const increment = (ymax - ymin) / (this.tickLength - 1);
80
+ const halfIncrement = increment / 2 || ymax || 1;
81
+ realTickLength = increment === 0 ? 1 : this.tickLength;
82
+
83
+ ymin -= halfIncrement;
84
+ ymax += halfIncrement;
85
+ }
86
+
87
+ if (ymin > ymax) {
88
+ const cachemin = ymin;
89
+ ymin = ymax;
90
+ ymax = cachemin;
91
+ }
92
+
93
+ if (plotLinesMaxAbs) {
94
+ if (plotLinesMaxAbs < ymin) {
95
+ ymin = plotLinesMaxAbs;
96
+ }
97
+
98
+ if (plotLinesMaxAbs > ymax) {
99
+ ymax = plotLinesMaxAbs;
100
+ }
101
+ }
102
+
103
+ yScales.domain([ymin, ymax]);
104
+ yScales.nice(realTickLength);
105
+ this.ticks = yScales.ticks(realTickLength);
106
+ }
107
+
108
+ draw(yScales: YScales) {
109
+ const { ctx, options } = this;
110
+ const { yAxis } = options;
111
+
112
+ ctx.textAlign = 'left';
113
+ ctx.textBaseline = 'bottom';
114
+ ctx.shadowColor = yAxis.labels.shadowColor;
115
+ ctx.shadowBlur = 1;
116
+ ctx.lineWidth = 1;
117
+ ctx.fillStyle = yAxis.labels.color;
118
+ ctx.font = `${yAxis.labels.fontSize}px Palantino`;
119
+ this.ticks.forEach((d) => {
120
+ let text: string = String(d);
121
+ if (d >= 1000) {
122
+ text = d3.format('.5s')(d);
123
+ // trim insignificant zeros
124
+ const siPrefixReg = /[kMGTPEZY]$/;
125
+ if (siPrefixReg.test(text)) {
126
+ const siPrefix = text[text.length - 1];
127
+ text = text.replace(siPrefixReg, '');
128
+ text = parseFloat(text) + siPrefix;
129
+ }
130
+ }
131
+ const y = yScales(d);
132
+ ctx.strokeText(text, 0, y);
133
+ ctx.fillText(text, 0, y);
134
+ });
135
+ }
136
+
137
+ drawGridLine(yScales: YScales) {
138
+ const { ctx, options } = this;
139
+ const { chart, yAxis } = options;
140
+
141
+ ctx.beginPath();
142
+ this.ticks.forEach((d) => {
143
+ const yValue = yScales(d);
144
+ ctx.moveTo(0, yValue);
145
+ ctx.lineTo(chart.width, yValue);
146
+ });
147
+ ctx.strokeStyle = yAxis.gridLineColor;
148
+ ctx.stroke();
149
+ }
150
+
151
+ drawPlotLines(yScales: YScales) {
152
+ const { ctx, options } = this;
153
+ const { chart, yAxis, yAxis: { plotLines } } = options;
154
+
155
+ if (Array.isArray(plotLines)) {
156
+ plotLines.forEach((plotLine) => {
157
+ const yValue = yScales(plotLine.value);
158
+ ctx.setLineDash([5, 3]);
159
+ ctx.beginPath();
160
+ ctx.moveTo(0, yValue);
161
+ ctx.lineTo(chart.width, yValue);
162
+ ctx.strokeStyle = plotLine.color;
163
+ ctx.stroke();
164
+ ctx.setLineDash([]);
165
+
166
+ ctx.textAlign = 'right';
167
+ ctx.textBaseline = 'top';
168
+ ctx.shadowColor = '#fff';
169
+ ctx.shadowBlur = 1;
170
+ ctx.lineWidth = 1;
171
+ ctx.fillStyle = plotLine.color;
172
+ ctx.font = `${yAxis.labels.fontSize}px Palantino`;
173
+ ctx.strokeText(String(plotLine.value), chart.width - 10, yValue);
174
+ ctx.fillText(String(plotLine.value), chart.width - 10, yValue);
175
+ });
176
+ }
177
+ }
178
+ }
package/src/zoom.ts ADDED
@@ -0,0 +1,105 @@
1
+ import * as d3 from 'd3';
2
+ import { Options, EventPosition, Transform } from './interface';
3
+
4
+ type ResetFuncType = (transform?: Transform) => void;
5
+
6
+ export default class Zoom {
7
+ options: Options;
8
+
9
+ reset: ResetFuncType;
10
+
11
+ constructor(userOptions: Options, reset: ResetFuncType, container: HTMLElement, canvas: HTMLCanvasElement) {
12
+ this.options = userOptions;
13
+ this.init(container, canvas);
14
+ this.reset = reset;
15
+ }
16
+
17
+ init(container: HTMLElement, canvas: HTMLCanvasElement) {
18
+ const { chart: { id, containerHeight } } = this.options;
19
+ const zoomEle = document.createElement('div');
20
+ const markerEle = document.createElement('div');
21
+ const resetEle = document.createElement('a');
22
+ const resetNode = document.createTextNode('Reset zoom');
23
+
24
+ zoomEle.id = `${id}-zoom`;
25
+ zoomEle.className = 'ts-graph-zoom';
26
+ markerEle.id = `${id}-zoom-marker`;
27
+ markerEle.className = 'ts-graph-zoom-marker';
28
+ markerEle.style.height = `${containerHeight}px`;
29
+ resetEle.id = `${id}-zoom-resetBtn`;
30
+ resetEle.className = 'ts-graph-zoom-resetBtn';
31
+ resetEle.appendChild(resetNode);
32
+ zoomEle.appendChild(markerEle);
33
+ zoomEle.appendChild(resetEle);
34
+ container.insertBefore(zoomEle, canvas);
35
+ this.onReset();
36
+ }
37
+
38
+ onReset() {
39
+ const { chart: { id } } = this.options;
40
+ const resetEle = document.getElementById(`${id}-zoom-resetBtn`) as HTMLElement;
41
+
42
+ resetEle.addEventListener('click', (e) => {
43
+ e.stopPropagation();
44
+ this.reset();
45
+ this.clearResetBtn();
46
+ });
47
+ }
48
+
49
+ onZoom(mousedownPos: EventPosition, mouseupPos: EventPosition, cbk: (transform: Transform) => void) {
50
+ const { chart: { width } } = this.options;
51
+ const range = mouseupPos.offsetX - mousedownPos.offsetX;
52
+
53
+ if (range) {
54
+ const scale = width / Math.abs(range);
55
+ const startPoint = mouseupPos.offsetX > mousedownPos.offsetX ? mousedownPos.offsetX : mouseupPos.offsetX;
56
+ const transform = d3.zoomIdentity.translate(-startPoint * scale, 0).scale(scale);
57
+ this.drawResetBtn();
58
+ cbk(transform);
59
+ }
60
+ }
61
+
62
+ clearMarker() {
63
+ const { chart: { id } } = this.options;
64
+ const markerEle = document.getElementById(`${id}-zoom-marker`) as HTMLElement;
65
+
66
+ markerEle.style.display = 'none';
67
+ markerEle.style.width = '0px';
68
+ markerEle.style.left = 'unset';
69
+ markerEle.style.right = 'unset';
70
+ }
71
+
72
+ drawMarker(mousedownPos: EventPosition, eventPosition: EventPosition) {
73
+ const { chart: { marginLeft } } = this.options;
74
+ const x1 = mousedownPos.offsetX;
75
+ const x2 = eventPosition.offsetX;
76
+ const { chart: { id, containerWidth } } = this.options;
77
+ const markerEle = document.getElementById(`${id}-zoom-marker`) as HTMLElement;
78
+
79
+ markerEle.style.display = 'block';
80
+ markerEle.style.top = '0px';
81
+ if (x1 < x2) {
82
+ markerEle.style.right = 'unset';
83
+ markerEle.style.left = `${x1 + marginLeft}px`;
84
+ markerEle.style.width = `${x2 - x1}px`;
85
+ } else {
86
+ markerEle.style.left = 'unset';
87
+ markerEle.style.right = `${containerWidth - x1 - marginLeft}px`;
88
+ markerEle.style.width = `${x1 - x2}px`;
89
+ }
90
+ }
91
+
92
+ clearResetBtn() {
93
+ const { chart: { id } } = this.options;
94
+ const resetEle = document.getElementById(`${id}-zoom-resetBtn`) as HTMLElement;
95
+
96
+ resetEle.style.display = 'none';
97
+ }
98
+
99
+ drawResetBtn() {
100
+ const { chart: { id } } = this.options;
101
+ const resetEle = document.getElementById(`${id}-zoom-resetBtn`) as HTMLElement;
102
+
103
+ resetEle.style.display = 'block';
104
+ }
105
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowSyntheticDefaultImports": true,
4
+ "noFallthroughCasesInSwitch": true,
5
+ "noUnusedParameters": true,
6
+ "noImplicitReturns": true,
7
+ "moduleResolution": "node",
8
+ "esModuleInterop": true,
9
+ "noUnusedLocals": true,
10
+ "noImplicitAny": true,
11
+ "target": "es2015",
12
+ "module": "es2015",
13
+ "strict": true,
14
+ "jsx": "react",
15
+ "allowJs": true,
16
+ "noEmit": true,
17
+ },
18
+ "include": [
19
+ "src/**/*"
20
+ ],
21
+ "exclude": [
22
+ "node_modules/*",
23
+ "dist/*",
24
+ "examples/*"
25
+ ]
26
+ }
@@ -0,0 +1,15 @@
1
+ module.exports = function(webpackConfig) {
2
+ webpackConfig.externals = {
3
+ 'd3': {
4
+ root: 'd3',
5
+ commonjs2: 'd3',
6
+ commonjs: 'd3',
7
+ },
8
+ 'lodash': {
9
+ root: '_',
10
+ commonjs2: 'lodash',
11
+ commonjs: 'lodash',
12
+ },
13
+ };
14
+ return webpackConfig;
15
+ }