canvas-trading
Version:
Interactive HTML5 Canvas Trading Chart
463 lines (462 loc) • 25.7 kB
JavaScript
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;