UNPKG

canvas-trading

Version:
463 lines (462 loc) 25.7 kB
var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); }; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useEventListener } from 'usehooks-ts'; import { AoCanvas, CursorCanvas, MainCanvas, PriceLabel, DateLabel, Wrap, OclhLabel, AssetLabel, AOCanvasNameLabel, StdevCanvasNameLabel, } from './canvas.styled'; import { CandleCanvas } from '../classes/CandleCanvas'; import { candleColors, canvasSettings } from '../config'; import { displayTrade, drawAo, drawCursor, drawFibonacciRetracements, drawFunction, drawSimpleLine, drawStdev, } from '../draw/draw'; import scrollZoom from '../scrollZoom'; import { findCandleByDate, findCandleWithTrade } from '../draw/drawFunctions'; import { touchEventHasScale } from '../utils/scalingMobileLike'; function usePropState(prop) { var _a = useState(prop), state = _a[0], setState = _a[1]; useEffect(function () { setState(prop); }, [prop]); return [state, setState]; } var Canvas = function (_a) { var style = _a.style, candleArray = _a.candleArray, lastCandle = _a.lastCandle, candlesShownProp = _a.candlesShown, shiftProp = _a.shift, initialFibonacciRetracement = _a.initialFibonacciRetracement, _b = _a.shownFibonacci, shownFibonacci = _b === void 0 ? 0 : _b, drawnLines = _a.drawnLines, shownTrade = _a.shownTrade, widthProp = _a.width, heightProp = _a.height, props = __rest(_a, ["style", "candleArray", "lastCandle", "candlesShown", "shift", "initialFibonacciRetracement", "shownFibonacci", "drawnLines", "shownTrade", "width", "height"]); var canvasRef = useRef(null); var cursorRef = useRef(null); var aoCanvasRef = useRef(null); var stdevCanvasRef = useRef(null); var otherSettings = useMemo(function () { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11; return ({ allTradesShown: (_b = (_a = props.otherSettings) === null || _a === void 0 ? void 0 : _a.allTradesShown) !== null && _b !== void 0 ? _b : false, alligator: (_d = (_c = props.otherSettings) === null || _c === void 0 ? void 0 : _c.alligator) !== null && _d !== void 0 ? _d : true, ao: (_f = (_e = props.otherSettings) === null || _e === void 0 ? void 0 : _e.ao) !== null && _f !== void 0 ? _f : true, stdev: (_h = (_g = props.otherSettings) === null || _g === void 0 ? void 0 : _g.stdev) !== null && _h !== void 0 ? _h : false, mountedIndicators: (_k = (_j = props.otherSettings) === null || _j === void 0 ? void 0 : _j.mountedIndicators) !== null && _k !== void 0 ? _k : true, zoom: (_m = (_l = props.otherSettings) === null || _l === void 0 ? void 0 : _l.zoom) !== null && _m !== void 0 ? _m : true, scroll: (_p = (_o = props.otherSettings) === null || _o === void 0 ? void 0 : _o.scroll) !== null && _p !== void 0 ? _p : true, showAsset: (_r = (_q = props.otherSettings) === null || _q === void 0 ? void 0 : _q.showAsset) !== null && _r !== void 0 ? _r : false, showLastCandlePrice: (_t = (_s = props.otherSettings) === null || _s === void 0 ? void 0 : _s.showLastCandlePrice) !== null && _t !== void 0 ? _t : false, cursor: (_v = (_u = props.otherSettings) === null || _u === void 0 ? void 0 : _u.cursor) !== null && _v !== void 0 ? _v : true, resizable: (_x = (_w = props.otherSettings) === null || _w === void 0 ? void 0 : _w.resizable) !== null && _x !== void 0 ? _x : false, fullscreen: (_z = (_y = props.otherSettings) === null || _y === void 0 ? void 0 : _y.fullscreen) !== null && _z !== void 0 ? _z : false, aoCanvasStyle: (_1 = (_0 = props.otherSettings) === null || _0 === void 0 ? void 0 : _0.aoCanvasStyle) !== null && _1 !== void 0 ? _1 : {}, onlyShowSelectedFibonacci: (_3 = (_2 = props.otherSettings) === null || _2 === void 0 ? void 0 : _2.onlyShowSelectedFibonacci) !== null && _3 !== void 0 ? _3 : false, autoFocusOnSelectedFibonacci: (_5 = (_4 = props.otherSettings) === null || _4 === void 0 ? void 0 : _4.autoFocusOnSelectedFibonacci) !== null && _5 !== void 0 ? _5 : false, drawRevBar: (_7 = (_6 = props.otherSettings) === null || _6 === void 0 ? void 0 : _6.drawRevBar) !== null && _7 !== void 0 ? _7 : true, drawFractal: (_9 = (_8 = props.otherSettings) === null || _8 === void 0 ? void 0 : _8.drawFractal) !== null && _9 !== void 0 ? _9 : true, dateTimeZone: (_11 = (_10 = props.otherSettings) === null || _10 === void 0 ? void 0 : _10.dateTimeZone) !== null && _11 !== void 0 ? _11 : 'UTC', }); }, [props.otherSettings]); // canvas settings var _c = usePropState(widthProp), width = _c[0], setWidth = _c[1]; var _d = usePropState(heightProp), height = _d[0], setHeight = _d[1]; var _e = usePropState(shiftProp !== null && shiftProp !== void 0 ? shiftProp : 0), shift = _e[0], setShift = _e[1]; var _f = usePropState(candlesShownProp !== null && candlesShownProp !== void 0 ? candlesShownProp : 100), candlesShown = _f[0], setCandlesShown = _f[1]; var _g = usePropState(otherSettings.fullscreen), fullscreen = _g[0], setFullscreen = _g[1]; // canvas elements var _h = useState(), displayedPrice = _h[0], setDisplayedPrice = _h[1]; var _j = useState(), displayedDate = _j[0], setDisplayedDate = _j[1]; var _k = useState(), displayedOclh = _k[0], setDisplayedOclh = _k[1]; var _l = useState({ x: -5, y: -5 }), cursor = _l[0], setCursor = _l[1]; var _m = useState(false), isDragging = _m[0], setIsDragging = _m[1]; var fibonacciRetracement = useState(initialFibonacciRetracement !== null && initialFibonacciRetracement !== void 0 ? initialFibonacciRetracement : [])[0]; var propsCanvas = useMemo(function () { try { return new CandleCanvas(Number(width), Number(height), candlesShown, shift, candleArray, lastCandle); } catch (error) { console.error('Could not create new CandleCanvas()', error); return null; } }, [width, height, candlesShown, shift, candleArray, lastCandle]); var cursorFunction = useCallback(function (position, onlyLabels) { if (onlyLabels === void 0) { onlyLabels = false; } if (!canvasRef.current || !propsCanvas) return; if (position.x < 0 || position.y < 0) { // No need to update cursor that is not present. // Also, resetting cursor labels. setDisplayedPrice(undefined); setDisplayedDate(undefined); setDisplayedOclh(undefined); return; } var rect = canvasRef.current.getBoundingClientRect(); var x = onlyLabels ? position.x : position.x - rect.left; var y = onlyLabels ? position.y : position.y - rect.top; setDisplayedPrice(propsCanvas.getDisplayedPrice(y)); if (!onlyLabels) setCursor({ x: x, y: y }); //#region finding date for displayed date var zoomedAndShifted = candleArray.slice(candleArray.length - candlesShown - shift, candleArray.length - shift); var xPosInPercent = x / rect.width; var index = Math.floor(xPosInPercent * zoomedAndShifted.length); var candle = zoomedAndShifted[index]; if (candle) { setDisplayedDate(otherSettings.dateTimeZone === 'UTC' ? new Date(candle.openTime).toLocaleString('en-US', { timeZone: 'UTC', }) : new Date(candle.openTime).toLocaleString()); if (!candle.open || !candle.close || !candle.low || !candle.high) return; setDisplayedOclh({ o: candle.open, h: candle.high, l: candle.low, c: candle.close, }); } //#endregion }, [candleArray, candlesShown, propsCanvas, shift, otherSettings.dateTimeZone]); var initialCandlesShown = useRef(candlesShown); var maxTradeId = useMemo(function () { return Math.max.apply(Math, __spreadArray(__spreadArray([], candleArray .map(function (candle) { return candle.trades; }) .flat() .map(function (trade) { return trade === null || trade === void 0 ? void 0 : trade.tradeID; }) .filter(function (tradeID) { return tradeID !== undefined; }), false), [0], false)); }, [candleArray]); var candlesForAllTrades = useMemo(function () { if (!candleArray) return; if (maxTradeId === undefined) return; var result = {}; for (var tradeIDIndex = 0; tradeIDIndex <= maxTradeId; tradeIDIndex++) { var startCandle = findCandleWithTrade(candleArray, tradeIDIndex); var endCandle = findCandleWithTrade(candleArray, tradeIDIndex, true); result[tradeIDIndex] = { startCandle: startCandle, endCandle: endCandle, }; } return result; }, [maxTradeId, candleArray]); var linesToDraw = useMemo(function () { if (!drawnLines) return; var result = []; drawnLines.forEach(function (line) { var startCandle = findCandleByDate(candleArray, new Date(line.start.openTime)); var endCandle = findCandleByDate(candleArray, new Date(line.end.openTime)); if (!startCandle.candle || !endCandle.candle) return; result.push({ start: { candle: startCandle, param: line.start.param, }, end: { candle: endCandle, param: line.end.param, }, color: line.color, opacity: line.opacity, dash: line.dash, }); }); return result; }, [drawnLines, candleArray]); var candlesForAllFibonacci = useMemo(function () { if (!candleArray) return; if (fibonacciRetracement.length === 0) return; var result = []; for (var fibonacciIndex = 0; fibonacciIndex < fibonacciRetracement.length; fibonacciIndex++) { var startCandle = findCandleByDate(candleArray, new Date(fibonacciRetracement[fibonacciIndex].startDate)); var endCandle = findCandleByDate(candleArray, new Date(fibonacciRetracement[fibonacciIndex].endDate)); result.push({ startPrice: fibonacciRetracement[fibonacciIndex].priceA, endPrice: fibonacciRetracement[fibonacciIndex].priceB, startCandle: startCandle, endCandle: endCandle, }); } return result; }, [fibonacciRetracement, candleArray]); // main useEffect useEffect(function () { var canvas = canvasRef.current; if (!canvas || !propsCanvas) return; var ctx = canvas.getContext('2d'); if (!ctx) return; drawFunction(ctx, propsCanvas, otherSettings); if (otherSettings.allTradesShown && maxTradeId !== undefined && candlesForAllTrades) { for (var i = 0; i <= maxTradeId; i++) { displayTrade(ctx, propsCanvas, candlesForAllTrades[i]); } } else if (shownTrade !== undefined && candlesForAllTrades) { displayTrade(ctx, propsCanvas, candlesForAllTrades[shownTrade]); } // draw ao if (otherSettings.ao) { var aoCanvas = aoCanvasRef.current; if (!aoCanvas) return; var aoCtx = aoCanvas.getContext('2d'); if (!aoCtx) return; drawAo(aoCtx, propsCanvas); } //draw standard deviation if (otherSettings.stdev) { var stdevCanvas = stdevCanvasRef.current; if (!stdevCanvas) return; var stdevCtx = stdevCanvas.getContext('2d'); if (!stdevCtx) return; drawStdev(stdevCtx, propsCanvas.standardDeviationArray); } // draw simple lines from drawnLines linesToDraw === null || linesToDraw === void 0 ? void 0 : linesToDraw.forEach(function (line) { var startCandle = line.start.candle; var endCandle = line.end.candle; if (!startCandle.candle || !endCandle.candle) return; drawSimpleLine(ctx, propsCanvas, startCandle.candle[line.start.param], endCandle.candle[line.end.param], startCandle.index, endCandle.index, line.color, line.opacity, line.dash); }); if (!candlesForAllFibonacci) return; //? if onlyShowSelectedFibonacci is true, we only draw the selected fibonacci, not all of them (cuz they might overlap and look bad) var fiboArray = otherSettings.onlyShowSelectedFibonacci ? [candlesForAllFibonacci[shownFibonacci]] : candlesForAllFibonacci; fiboArray.forEach(function (fibo) { if (!fibo) return; drawFibonacciRetracements(ctx, propsCanvas, fibo.startPrice, fibo.endPrice, fibo.startCandle.index, fibo.endCandle.index); }); }, [ candlesForAllTrades, candlesForAllFibonacci, maxTradeId, otherSettings, propsCanvas, linesToDraw, shownTrade, shownFibonacci, candleArray, ]); var _o = useState(false), isPinching = _o[0], setIsPinching = _o[1]; useEventListener('wheel', function (e) { if (isPinching) return; e.preventDefault(); // prevent other events happening on the parent page e.stopPropagation(); if (!(otherSettings.scroll || otherSettings.zoom)) return; scrollZoom({ x: otherSettings.scroll ? e.deltaX : 0, y: otherSettings.zoom ? e.deltaY : 0, }, shift, candlesShown, candleArray.length, setShift, setCandlesShown); }, canvasRef); // Dragging canvas Effect to scroll useEventListener('mousedown', function () { setIsDragging(true); }, canvasRef); useEventListener('mousemove', function (e) { if (isDragging) { scrollZoom({ x: -Math.round(e.movementX) * 2, y: 0, }, shift, candlesShown, candleArray.length, setShift, setCandlesShown); } if (!otherSettings.cursor) return; cursorFunction({ x: e.clientX, y: e.clientY }); }, canvasRef); var mouseUpOrLeaveListener = function () { setIsDragging(false); }; useEventListener('mouseup', mouseUpOrLeaveListener, canvasRef); // reset cursor when mouse leaves canvas useEventListener('mouseleave', function () { // reset cursor setCursor({ x: -5, y: -5 }); mouseUpOrLeaveListener(); }, canvasRef); //#region Pinching for Desktop // Does not work in Chrome. Tested in Safari. // TODO implement GestureEvent type var _p = useState(0), lastPinchScale = _p[0], setLastPinchScale = _p[1]; useEventListener( // @ts-expect-error 'gesturestart' exists 'gesturestart', function (event) { if (!otherSettings.zoom) return; if (!touchEventHasScale(event)) return; event.preventDefault(); setIsPinching(true); }, canvasRef); useEventListener( // @ts-expect-error 'gesturechange' exists 'gesturechange', function (event) { if (!otherSettings.zoom) return; if (!touchEventHasScale(event)) return; // @ts-expect-error .scale exists var pinchScale = event.scale - 1; var differenceInPinchScale = pinchScale - lastPinchScale; scrollZoom({ x: 0, y: Math.round(-differenceInPinchScale * 1000), }, shift, candlesShown, candleArray.length, setShift, setCandlesShown); setLastPinchScale(pinchScale); }, canvasRef); useEventListener( // @ts-expect-error 'gestureend' exists 'gestureend', function (event) { if (!otherSettings.zoom) return; if (!touchEventHasScale(event)) return; setLastPinchScale(0); setIsPinching(false); }, canvasRef); //#endregion useEventListener('resize', function () { if (!fullscreen) return; setWidth(window.innerWidth); setHeight(window.innerHeight - (otherSettings.ao ? window.innerHeight / 5 : 0)); }); // useEffect for cursor useEffect(function () { if (!otherSettings.cursor) return; var canvas = cursorRef.current; if (!canvas) return; var cursorCtx = canvas.getContext('2d'); if (!cursorCtx) return; drawCursor(cursorCtx, canvas.width, canvas.height, cursor); cursorFunction(cursor, true); }, [cursor, cursorFunction, otherSettings.cursor]); // useEffect to reset shift and zoom when candleArray shrinks in size (e.g. switching to a smaller data example, or setting smaller history length) useEffect(function () { var currentlyRequiredLength = shift + candlesShown; if (candleArray.length < currentlyRequiredLength && candleArray.length >= canvasSettings.minCandlesShown) { /** * works simpler logic: * if somehow the settings that user has set for the previous graph are not possible for the new graph, we just reset them to default. * I would even reset them to default every time the candleArray changes, but it will effect when the new candle appears, so we are good for now. * the settings that were set for one graph are not convenient either way for any new graph, as you don't want to appear in the middle of the graph unknown to you. */ setShift(0); setCandlesShown(Math.min(initialCandlesShown.current, candleArray.length)); } }, [candleArray, candlesShown, setCandlesShown, setShift, shift]); // useEffect to reset shift and zoom when unselecting trade. useEffect(function () { if (shownTrade === undefined) { // reset shift and zoom on unselecting trade. console.log('resetting shift and zoom'); setShift(shiftProp !== null && shiftProp !== void 0 ? shiftProp : 0); //! idk if its actually correct, not working with trades for now setCandlesShown(initialCandlesShown.current); } }, [shownTrade, setShift, setCandlesShown, shiftProp]); // useEffect to shift graph when shownTrade changes useEffect(function () { if (shownTrade === undefined) { return; } var startCandle = findCandleWithTrade(candleArray, shownTrade); var endCandle = findCandleWithTrade(candleArray, shownTrade, true); if (!startCandle.candle || !endCandle.candle) return; var newShift = Math.min(Math.max(candleArray.length - candleArray.indexOf(endCandle.candle) - 10, 0), candleArray.length - 40); var newCandlesShown = candleArray.indexOf(endCandle.candle) - candleArray.indexOf(startCandle.candle) + 20; setShift(newShift); setCandlesShown(newCandlesShown); }, [candleArray, setCandlesShown, setShift, shownTrade]); // use effect to apply zoom and shift when shownFibonacci changes useEffect(function () { if (shownFibonacci === undefined || !candlesForAllFibonacci || !otherSettings.autoFocusOnSelectedFibonacci) { return; } var fiboCandles = candlesForAllFibonacci[shownFibonacci]; if (!fiboCandles) return; var newShift = Math.min(Math.max(candleArray.length - fiboCandles.endCandle.index - 20, 0), candleArray.length - 40); var newCandlesShown = Math.min(fiboCandles.endCandle.index - fiboCandles.startCandle.index + 40, candleArray.length - newShift - 3); setShift(newShift); setCandlesShown(newCandlesShown); }, [ candlesForAllFibonacci, candleArray, setCandlesShown, setShift, shownFibonacci, otherSettings.autoFocusOnSelectedFibonacci, ]); return (_jsxs(Wrap, __assign({ onMouseMove: otherSettings.resizable ? function (event) { var isMouseDown = event.buttons > 0; if (!isMouseDown) return; var newWidth = Math.round(event.currentTarget.clientWidth); if (width !== newWidth) { setWidth(newWidth); } var aoHeight = otherSettings.ao ? Number(height) / 5 + 5 : 0; var newHeight = Math.round(event.currentTarget.clientHeight - aoHeight); if (height !== newHeight) { setHeight(newHeight); } } : undefined, resizable: otherSettings.resizable, width: Number(width), height: Number(height), style: style, stdev: otherSettings.stdev, ao: otherSettings.ao, fullscreen: fullscreen, onDoubleClick: function () { setFullscreen(function (prev) { setWidth(prev ? widthProp : window.innerWidth); setHeight(prev ? heightProp : window.innerHeight - (otherSettings.ao ? window.innerHeight / 5 : 0)); return !prev; }); } }, { children: [otherSettings.showAsset && ((lastCandle === null || lastCandle === void 0 ? void 0 : lastCandle.asset) || candleArray[candleArray.length - 1].asset) && (_jsx(AssetLabel, __assign({ height: Number(height), width: Number(width), aoShown: otherSettings.ao, opacity: canvasSettings.assetOpacity }, { children: (lastCandle === null || lastCandle === void 0 ? void 0 : lastCandle.asset) || candleArray[candleArray.length - 1].asset }))), _jsx(PriceLabel, __assign({ height: Number(height), cursor: cursor }, { children: displayedPrice })), _jsx(DateLabel, __assign({ width: Number(width), height: Number(height), cursor: cursor, ao: otherSettings.ao, stdev: otherSettings.stdev }, { children: displayedDate })), _jsxs(OclhLabel, __assign({ canvasWidth: Number(width), canvasHeight: Number(height) }, { children: [((lastCandle === null || lastCandle === void 0 ? void 0 : lastCandle.asset) || candleArray[candleArray.length - 1].asset) && (_jsxs("span", { children: [(lastCandle === null || lastCandle === void 0 ? void 0 : lastCandle.asset) || candleArray[candleArray.length - 1].asset, ' ', props.resolution && "\u2022 ".concat(props.resolution)] })), displayedOclh && Object.keys(displayedOclh).map(function (key) { return (_jsxs("p", { children: [key.toUpperCase(), ":", _jsx("label", __assign({ style: displayedOclh.c > displayedOclh.o ? { color: candleColors.green } : { color: candleColors.red } }, { children: displayedOclh[key] }))] }, key)); })] })), _jsx(MainCanvas, __assign({}, props, { resizable: otherSettings.resizable, width: Number(width) * canvasSettings.scaleForQuality, height: Number(height) * canvasSettings.scaleForQuality, ref: canvasRef })), _jsx(CursorCanvas, { ref: cursorRef, width: Number(width) * canvasSettings.scaleForQuality, height: Number(height) * canvasSettings.scaleForQuality }), otherSettings.stdev && (_jsxs(_Fragment, { children: [_jsx(StdevCanvasNameLabel, __assign({ width: Number(width), height: Number(height), aoShown: otherSettings.ao }, { children: "Stdev" })), _jsx(AoCanvas, { style: otherSettings.aoCanvasStyle, resizable: otherSettings.resizable, width: Number(width) * canvasSettings.scaleForQuality, height: (Number(height) * canvasSettings.scaleForQuality) / 5, ref: stdevCanvasRef })] })), otherSettings.ao && (_jsxs(_Fragment, { children: [_jsx(AOCanvasNameLabel, __assign({ width: Number(width), height: Number(height) }, { children: "AO" })), _jsx(AoCanvas, { style: otherSettings.aoCanvasStyle, resizable: otherSettings.resizable, width: Number(width) * canvasSettings.scaleForQuality, height: (Number(height) * canvasSettings.scaleForQuality) / 5, ref: aoCanvasRef })] }))] }))); }; export default Canvas;