@tradingaction/axes 2.0.13

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/Axis.tsx ADDED
@@ -0,0 +1,407 @@
1
+ import {
2
+ first,
3
+ GenericChartComponent,
4
+ getAxisCanvas,
5
+ getStrokeDasharrayCanvas,
6
+ last,
7
+ strokeDashTypes,
8
+ } from "@tradingaction/core";
9
+ import { range as d3Range, zip } from "d3-array";
10
+ import { forceCollide, forceSimulation, forceX } from "d3-force";
11
+ import { ScaleContinuousNumeric } from "d3-scale";
12
+ import * as React from "react";
13
+ import { AxisZoomCapture } from "./AxisZoomCapture";
14
+
15
+ interface AxisProps {
16
+ readonly axisZoomCallback?: (domain: number[]) => void;
17
+ readonly bg: {
18
+ h: number;
19
+ x: number;
20
+ w: number;
21
+ y: number;
22
+ };
23
+ readonly className?: string;
24
+ readonly domainClassName?: string;
25
+ readonly edgeClip: boolean;
26
+ readonly fontFamily?: string;
27
+ readonly fontSize?: number;
28
+ readonly fontWeight?: number;
29
+ readonly getMouseDelta: (startXY: [number, number], mouseXY: [number, number]) => number;
30
+ readonly getScale: (moreProps: any) => ScaleContinuousNumeric<number, number>;
31
+ readonly innerTickSize?: number;
32
+ readonly inverted?: boolean;
33
+ readonly onContextMenu?: (e: React.MouseEvent, mousePosition: [number, number]) => void;
34
+ readonly onDoubleClick?: (e: React.MouseEvent, mousePosition: [number, number]) => void;
35
+ readonly orient?: "top" | "left" | "right" | "bottom";
36
+ readonly outerTickSize: number;
37
+ readonly range: number[];
38
+ readonly showDomain?: boolean;
39
+ readonly showGridLines?: boolean;
40
+ readonly showTicks?: boolean;
41
+ readonly showTickLabel?: boolean;
42
+ readonly strokeStyle: string;
43
+ readonly strokeWidth: number;
44
+ readonly tickFormat?: (data: any) => string;
45
+ readonly tickPadding?: number;
46
+ readonly tickSize?: number;
47
+ readonly ticks?: number;
48
+ readonly tickLabelFill?: string;
49
+ readonly tickStrokeStyle?: string;
50
+ readonly tickStrokeWidth?: number;
51
+ readonly tickStrokeDasharray?: strokeDashTypes;
52
+ readonly tickValues?: number[] | ((domain: number[]) => number[]);
53
+ readonly tickInterval?: number;
54
+ readonly tickIntervalFunction?: (min: number, max: number, tickInterval: number) => number[];
55
+ readonly transform: number[];
56
+ readonly zoomEnabled?: boolean;
57
+ readonly zoomCursorClassName?: string;
58
+ }
59
+
60
+ interface Tick {
61
+ value: number;
62
+ x1: number;
63
+ y1: number;
64
+ x2: number;
65
+ y2: number;
66
+ labelX: number;
67
+ labelY: number;
68
+ }
69
+
70
+ export class Axis extends React.Component<AxisProps> {
71
+ public static defaultProps = {
72
+ edgeClip: false,
73
+ zoomEnabled: false,
74
+ zoomCursorClassName: "",
75
+ };
76
+
77
+ private readonly chartRef = React.createRef<GenericChartComponent>();
78
+
79
+ public render() {
80
+ const {
81
+ bg,
82
+ axisZoomCallback,
83
+ className,
84
+ zoomCursorClassName,
85
+ zoomEnabled,
86
+ getScale,
87
+ inverted,
88
+ transform,
89
+ getMouseDelta,
90
+ edgeClip,
91
+ onContextMenu,
92
+ onDoubleClick,
93
+ } = this.props;
94
+
95
+ const zoomCapture = zoomEnabled ? (
96
+ <AxisZoomCapture
97
+ bg={bg}
98
+ getScale={getScale}
99
+ getMoreProps={this.getMoreProps}
100
+ getMouseDelta={getMouseDelta}
101
+ axisZoomCallback={axisZoomCallback}
102
+ className={className}
103
+ zoomCursorClassName={zoomCursorClassName}
104
+ inverted={inverted}
105
+ onContextMenu={onContextMenu}
106
+ onDoubleClick={onDoubleClick}
107
+ />
108
+ ) : null;
109
+
110
+ return (
111
+ <g transform={`translate(${transform[0]}, ${transform[1]})`}>
112
+ {zoomCapture}
113
+ <GenericChartComponent
114
+ ref={this.chartRef}
115
+ canvasToDraw={getAxisCanvas}
116
+ clip={false}
117
+ edgeClip={edgeClip}
118
+ canvasDraw={this.drawOnCanvas}
119
+ drawOn={["pan"]}
120
+ />
121
+ </g>
122
+ );
123
+ }
124
+
125
+ private readonly getMoreProps = () => {
126
+ return this.chartRef.current!.getMoreProps();
127
+ };
128
+
129
+ private readonly drawOnCanvas = (ctx: CanvasRenderingContext2D, moreProps: any) => {
130
+ const { showDomain, showGridLines, showTickLabel, showTicks, transform, range, getScale, tickLabelFill } =
131
+ this.props;
132
+
133
+ ctx.save();
134
+ ctx.translate(transform[0], transform[1]);
135
+
136
+ const scale = getScale(moreProps);
137
+ const tickProps = tickHelper(this.props, scale);
138
+ if (showTicks) {
139
+ drawTicks(ctx, tickProps);
140
+ }
141
+
142
+ if (showGridLines) {
143
+ tickProps.ticks.forEach((tick) => {
144
+ drawGridLine(ctx, tick, tickProps, moreProps);
145
+ });
146
+ }
147
+
148
+ if (showTickLabel) {
149
+ const { fontFamily, fontSize, fontWeight, textAnchor } = tickProps;
150
+
151
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
152
+ if (tickLabelFill !== undefined) {
153
+ ctx.fillStyle = tickLabelFill;
154
+ }
155
+ ctx.textAlign = textAnchor === "middle" ? "center" : (textAnchor as CanvasTextAlign);
156
+ tickProps.ticks.forEach((tick: any) => {
157
+ drawEachTickLabel(ctx, tick, tickProps);
158
+ });
159
+ }
160
+
161
+ if (showDomain) {
162
+ drawAxisLine(ctx, this.props, range);
163
+ }
164
+
165
+ ctx.restore();
166
+ };
167
+ }
168
+
169
+ const tickHelper = (props: AxisProps, scale: ScaleContinuousNumeric<number, number>) => {
170
+ const {
171
+ orient,
172
+ innerTickSize = 4,
173
+ tickFormat,
174
+ tickPadding = 4,
175
+ tickLabelFill,
176
+ tickStrokeWidth,
177
+ tickStrokeDasharray,
178
+ fontSize = 12,
179
+ fontFamily,
180
+ fontWeight,
181
+ showTicks,
182
+ showTickLabel,
183
+ ticks: tickArguments,
184
+ tickValues: tickValuesProp,
185
+ tickStrokeStyle,
186
+ tickInterval,
187
+ tickIntervalFunction,
188
+ ...rest
189
+ } = props;
190
+
191
+ let tickValues: number[];
192
+ if (tickValuesProp !== undefined) {
193
+ if (typeof tickValuesProp === "function") {
194
+ tickValues = tickValuesProp(scale.domain());
195
+ } else {
196
+ tickValues = tickValuesProp;
197
+ }
198
+ } else if (tickInterval !== undefined) {
199
+ const [min, max] = scale.domain();
200
+ const baseTickValues = d3Range(min, max, (max - min) / tickInterval);
201
+
202
+ tickValues = tickIntervalFunction ? tickIntervalFunction(min, max, tickInterval) : baseTickValues;
203
+ } else if (scale.ticks !== undefined) {
204
+ tickValues = scale.ticks(tickArguments);
205
+ } else {
206
+ tickValues = scale.domain();
207
+ }
208
+
209
+ const format = tickFormat === undefined ? scale.tickFormat(tickArguments) : (d: any) => tickFormat(d) || "";
210
+
211
+ const sign = orient === "top" || orient === "left" ? -1 : 1;
212
+ const tickSpacing = Math.max(innerTickSize, 0) + tickPadding;
213
+
214
+ let ticks: Tick[];
215
+ let dy;
216
+ // tslint:disable-next-line: variable-name
217
+ let canvas_dy;
218
+ let textAnchor;
219
+
220
+ if (orient === "bottom" || orient === "top") {
221
+ dy = sign < 0 ? "0em" : ".71em";
222
+ canvas_dy = sign < 0 ? 0 : fontSize * 0.71;
223
+ textAnchor = "middle";
224
+
225
+ const y2 = sign * innerTickSize;
226
+ const labelY = sign * tickSpacing;
227
+
228
+ ticks = tickValues.map((d) => {
229
+ const x = Math.round(scale(d));
230
+ return {
231
+ value: d,
232
+ x1: x,
233
+ y1: 0,
234
+ x2: x,
235
+ y2,
236
+ labelX: x,
237
+ labelY,
238
+ };
239
+ });
240
+
241
+ if (showTicks) {
242
+ const nodes = ticks.map((d) => ({ id: d.value, value: d.value, fy: d.y2, origX: d.x1 }));
243
+
244
+ const simulation = forceSimulation(nodes)
245
+ .force("x", forceX<any>((d) => d.origX).strength(1))
246
+ .force("collide", forceCollide(22))
247
+ .stop();
248
+
249
+ for (let i = 0; i < 100; ++i) {
250
+ simulation.tick();
251
+ }
252
+
253
+ // @ts-ignore
254
+ ticks = zip(ticks, nodes).map((d) => {
255
+ const a: any = d[0];
256
+ const b: any = d[1];
257
+
258
+ if (Math.abs(b.x - b.origX) > 0.01) {
259
+ return {
260
+ ...a,
261
+ x2: b.x,
262
+ labelX: b.x,
263
+ };
264
+ }
265
+ return a;
266
+ });
267
+ }
268
+ } else {
269
+ ticks = tickValues.map((d) => {
270
+ const y = Math.round(scale(d));
271
+ const x2 = sign * innerTickSize;
272
+ const labelX = sign * tickSpacing;
273
+ return {
274
+ value: d,
275
+ x1: 0,
276
+ y1: y,
277
+ x2,
278
+ y2: y,
279
+ labelX,
280
+ labelY: y,
281
+ };
282
+ });
283
+
284
+ dy = ".32em";
285
+ canvas_dy = fontSize * 0.32;
286
+ textAnchor = sign < 0 ? "end" : "start";
287
+ }
288
+
289
+ return {
290
+ orient,
291
+ ticks,
292
+ scale,
293
+ tickStrokeStyle,
294
+ tickLabelFill: tickLabelFill || tickStrokeStyle,
295
+ tickStrokeWidth,
296
+ tickStrokeDasharray,
297
+ dy,
298
+ canvas_dy,
299
+ textAnchor,
300
+ fontSize,
301
+ fontFamily,
302
+ fontWeight,
303
+ format,
304
+ showTickLabel,
305
+ ...rest,
306
+ };
307
+ };
308
+
309
+ const drawAxisLine = (ctx: CanvasRenderingContext2D, props: AxisProps, range: any) => {
310
+ const { orient, outerTickSize, strokeStyle, strokeWidth } = props;
311
+
312
+ const sign = orient === "top" || orient === "left" ? -1 : 1;
313
+ const xAxis = orient === "bottom" || orient === "top";
314
+
315
+ ctx.lineWidth = strokeWidth;
316
+ ctx.strokeStyle = strokeStyle;
317
+
318
+ ctx.beginPath();
319
+
320
+ const firstPoint = first(range);
321
+ const lastPoint = last(range);
322
+ const tickSize = sign * outerTickSize;
323
+ if (xAxis) {
324
+ ctx.moveTo(firstPoint, tickSize);
325
+ ctx.lineTo(firstPoint, 0);
326
+ ctx.lineTo(lastPoint, 0);
327
+ ctx.lineTo(lastPoint, tickSize);
328
+ } else {
329
+ ctx.moveTo(tickSize, firstPoint);
330
+ ctx.lineTo(0, firstPoint);
331
+ ctx.lineTo(0, lastPoint);
332
+ ctx.lineTo(tickSize, lastPoint);
333
+ }
334
+
335
+ ctx.stroke();
336
+ };
337
+
338
+ const drawTicks = (ctx: CanvasRenderingContext2D, result: any) => {
339
+ const { ticks, tickStrokeStyle } = result;
340
+
341
+ if (tickStrokeStyle !== undefined) {
342
+ ctx.strokeStyle = tickStrokeStyle;
343
+ ctx.fillStyle = tickStrokeStyle;
344
+ }
345
+
346
+ ticks.forEach((tick: any) => {
347
+ drawEachTick(ctx, tick, result);
348
+ });
349
+ };
350
+
351
+ const drawGridLine = (ctx: CanvasRenderingContext2D, tick: Tick, result: any, moreProps: any) => {
352
+ const { orient, gridLinesStrokeWidth, gridLinesStrokeStyle, gridLinesStrokeDasharray } = result;
353
+
354
+ const { chartConfig } = moreProps;
355
+
356
+ const { height, width } = chartConfig;
357
+
358
+ if (gridLinesStrokeStyle !== undefined) {
359
+ ctx.strokeStyle = gridLinesStrokeStyle;
360
+ }
361
+ ctx.beginPath();
362
+
363
+ const sign = orient === "top" || orient === "left" ? 1 : -1;
364
+
365
+ switch (orient) {
366
+ case "top":
367
+ case "bottom":
368
+ ctx.moveTo(tick.x1, 0);
369
+ ctx.lineTo(tick.x2, sign * height);
370
+ break;
371
+ default:
372
+ ctx.moveTo(0, tick.y1);
373
+ ctx.lineTo(sign * width, tick.y2);
374
+ break;
375
+ }
376
+ ctx.lineWidth = gridLinesStrokeWidth;
377
+
378
+ const lineDash = getStrokeDasharrayCanvas(gridLinesStrokeDasharray);
379
+
380
+ ctx.setLineDash(lineDash);
381
+ ctx.stroke();
382
+ };
383
+
384
+ const drawEachTick = (ctx: CanvasRenderingContext2D, tick: any, result: any) => {
385
+ const { tickStrokeWidth, tickStrokeDasharray } = result;
386
+
387
+ ctx.beginPath();
388
+
389
+ ctx.moveTo(tick.x1, tick.y1);
390
+ ctx.lineTo(tick.x2, tick.y2);
391
+ ctx.lineWidth = tickStrokeWidth;
392
+
393
+ const lineDash = getStrokeDasharrayCanvas(tickStrokeDasharray);
394
+
395
+ ctx.setLineDash(lineDash);
396
+ ctx.stroke();
397
+ };
398
+
399
+ const drawEachTickLabel = (ctx: CanvasRenderingContext2D, tick: any, result: any) => {
400
+ const { canvas_dy, format } = result;
401
+
402
+ const text = format(tick.value);
403
+
404
+ ctx.beginPath();
405
+
406
+ ctx.fillText(text, tick.labelX, tick.labelY + canvas_dy);
407
+ };
@@ -0,0 +1,232 @@
1
+ import {
2
+ d3Window,
3
+ first,
4
+ getTouchProps,
5
+ last,
6
+ MOUSEMOVE,
7
+ mousePosition,
8
+ MOUSEUP,
9
+ sign,
10
+ TOUCHEND,
11
+ TOUCHMOVE,
12
+ touchPosition,
13
+ } from "@tradingaction/core";
14
+ import { mean } from "d3-array";
15
+ import { ScaleContinuousNumeric } from "d3-scale";
16
+ import { select, pointer } from "d3-selection";
17
+ import * as React from "react";
18
+
19
+ export interface AxisZoomCaptureProps {
20
+ readonly axisZoomCallback?: (domain: number[]) => void;
21
+ readonly bg: {
22
+ h: number;
23
+ x: number;
24
+ w: number;
25
+ y: number;
26
+ };
27
+ readonly className?: string;
28
+ readonly getMoreProps: () => any;
29
+ readonly getScale: (moreProps: any) => ScaleContinuousNumeric<number, number>;
30
+ readonly getMouseDelta: (startXY: [number, number], mouseXY: [number, number]) => number;
31
+ readonly innerTickSize?: number;
32
+ readonly inverted?: boolean;
33
+ readonly onDoubleClick?: (e: React.MouseEvent, mousePosition: [number, number]) => void;
34
+ readonly onContextMenu?: (e: React.MouseEvent, mousePosition: [number, number]) => void;
35
+ readonly outerTickSize?: number;
36
+ readonly showDomain?: boolean;
37
+ readonly showTicks?: boolean;
38
+ readonly tickFormat?: (datum: number) => string;
39
+ readonly tickPadding?: number;
40
+ readonly tickSize?: number;
41
+ readonly ticks?: number;
42
+ readonly tickValues?: number[];
43
+ readonly zoomCursorClassName?: string;
44
+ }
45
+
46
+ interface AxisZoomCaptureState {
47
+ startPosition: { startScale: ScaleContinuousNumeric<number, number>; startXY: [number, number] } | null;
48
+ }
49
+
50
+ export class AxisZoomCapture extends React.Component<AxisZoomCaptureProps, AxisZoomCaptureState> {
51
+ private readonly ref = React.createRef<SVGRectElement>();
52
+ private clicked = false;
53
+ private dragHappened = false;
54
+
55
+ public constructor(props: AxisZoomCaptureProps) {
56
+ super(props);
57
+
58
+ this.state = {
59
+ startPosition: null,
60
+ };
61
+ }
62
+
63
+ public render() {
64
+ const { bg, className, zoomCursorClassName } = this.props;
65
+
66
+ const cursor =
67
+ this.state.startPosition !== null ? zoomCursorClassName : "react-financial-charts-default-cursor";
68
+
69
+ return (
70
+ <rect
71
+ className={`react-financial-charts-enable-interaction ${cursor} ${className}`}
72
+ ref={this.ref}
73
+ x={bg.x}
74
+ y={bg.y}
75
+ opacity={0}
76
+ height={bg.h}
77
+ width={bg.w}
78
+ onContextMenu={this.handleRightClick}
79
+ onMouseDown={this.handleDragStartMouse}
80
+ onTouchStart={this.handleDragStartTouch}
81
+ />
82
+ );
83
+ }
84
+
85
+ private readonly handleDragEnd = (e: any) => {
86
+ const container = this.ref.current;
87
+ if (container === null) {
88
+ return;
89
+ }
90
+
91
+ if (!this.dragHappened) {
92
+ if (this.clicked) {
93
+ const mouseXY = pointer(e, container);
94
+ const { onDoubleClick } = this.props;
95
+ if (onDoubleClick !== undefined) {
96
+ onDoubleClick(e, mouseXY);
97
+ }
98
+ } else {
99
+ this.clicked = true;
100
+ setTimeout(() => {
101
+ this.clicked = false;
102
+ }, 300);
103
+ }
104
+ }
105
+
106
+ select(d3Window(container)).on(MOUSEMOVE, null).on(MOUSEUP, null).on(TOUCHMOVE, null).on(TOUCHEND, null);
107
+
108
+ this.setState({
109
+ startPosition: null,
110
+ });
111
+ };
112
+
113
+ private readonly handleDrag = (e: any) => {
114
+ const container = this.ref.current;
115
+ if (container === null) {
116
+ return;
117
+ }
118
+ this.dragHappened = true;
119
+
120
+ const { getMouseDelta, inverted = true } = this.props;
121
+
122
+ const { startPosition } = this.state;
123
+ if (startPosition !== null) {
124
+ const { startScale } = startPosition;
125
+ const { startXY } = startPosition;
126
+
127
+ const mouseXY = pointer(e, container);
128
+
129
+ const diff = getMouseDelta(startXY, mouseXY);
130
+
131
+ const center = mean(startScale.range());
132
+ if (center === undefined) {
133
+ return;
134
+ }
135
+
136
+ const tempRange = startScale
137
+ .range()
138
+ .map((d) => (inverted ? d - sign(d - center) * diff : d + sign(d - center) * diff));
139
+
140
+ const newDomain = tempRange.map(startScale.invert);
141
+
142
+ if (
143
+ sign(last(startScale.range()) - first(startScale.range())) === sign(last(tempRange) - first(tempRange))
144
+ ) {
145
+ const { axisZoomCallback } = this.props;
146
+ if (axisZoomCallback !== undefined) {
147
+ axisZoomCallback(newDomain);
148
+ }
149
+ }
150
+ }
151
+ };
152
+
153
+ private readonly handleDragStartTouch = (event: React.TouchEvent<SVGRectElement>) => {
154
+ const container = this.ref.current;
155
+ if (container === null) {
156
+ return;
157
+ }
158
+
159
+ this.dragHappened = false;
160
+
161
+ const { getScale, getMoreProps } = this.props;
162
+ const allProps = getMoreProps();
163
+ const startScale = getScale(allProps);
164
+
165
+ if (event.touches.length === 1 && startScale.invert !== undefined) {
166
+ select(d3Window(container)).on(TOUCHMOVE, this.handleDrag).on(TOUCHEND, this.handleDragEnd);
167
+
168
+ const startXY = touchPosition(getTouchProps(event.touches[0]), event);
169
+
170
+ this.setState({
171
+ startPosition: {
172
+ startScale,
173
+ startXY,
174
+ },
175
+ });
176
+ }
177
+ };
178
+
179
+ private readonly handleDragStartMouse = (event: React.MouseEvent<SVGRectElement, MouseEvent>) => {
180
+ event.preventDefault();
181
+
182
+ const container = this.ref.current;
183
+ if (container === null) {
184
+ return;
185
+ }
186
+
187
+ this.dragHappened = false;
188
+
189
+ const { getScale, getMoreProps } = this.props;
190
+ const allProps = getMoreProps();
191
+ const startScale = getScale(allProps);
192
+
193
+ if (startScale.invert !== undefined) {
194
+ select(d3Window(container)).on(MOUSEMOVE, this.handleDrag, false).on(MOUSEUP, this.handleDragEnd, false);
195
+
196
+ const startXY = mousePosition(event);
197
+
198
+ this.setState({
199
+ startPosition: {
200
+ startXY,
201
+ startScale,
202
+ },
203
+ });
204
+ }
205
+ };
206
+
207
+ private readonly handleRightClick = (event: React.MouseEvent<SVGRectElement, MouseEvent>) => {
208
+ event.stopPropagation();
209
+ event.preventDefault();
210
+
211
+ const container = this.ref.current;
212
+ if (container === null) {
213
+ return;
214
+ }
215
+
216
+ const { onContextMenu } = this.props;
217
+ if (onContextMenu === undefined) {
218
+ return;
219
+ }
220
+
221
+ const defaultRect = container.getBoundingClientRect();
222
+ const mouseXY = mousePosition(event, defaultRect);
223
+
224
+ select(d3Window(container)).on(MOUSEMOVE, null).on(MOUSEUP, null);
225
+
226
+ this.setState({
227
+ startPosition: null,
228
+ });
229
+
230
+ onContextMenu(event, mouseXY);
231
+ };
232
+ }