diff --git a/package-lock.json b/package-lock.json index de08d99..aa053fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4311,6 +4311,32 @@ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, + "chart.js": { + "version": "2.9.4", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz", + "integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==", + "requires": { + "chartjs-color": "^2.1.0", + "moment": "^2.10.2" + } + }, + "chartjs-color": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz", + "integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==", + "requires": { + "chartjs-color-string": "^0.6.0", + "color-convert": "^1.9.3" + } + }, + "chartjs-color-string": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz", + "integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==", + "requires": { + "color-name": "^1.0.0" + } + }, "cheerio": { "version": "0.22.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-0.22.0.tgz", @@ -12566,6 +12592,15 @@ "whatwg-fetch": "^3.0.0" } }, + "react-chartjs-2": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-2.11.1.tgz", + "integrity": "sha512-G7cNq/n2Bkh/v4vcI+GKx7Q1xwZexKYhOSj2HmrFXlvNeaURWXun6KlOUpEQwi1cv9Tgs4H3kGywDWMrX2kxfA==", + "requires": { + "lodash": "^4.17.19", + "prop-types": "^15.7.2" + } + }, "react-copy-to-clipboard": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz", diff --git a/package.json b/package.json index 7545483..53be587 100644 --- a/package.json +++ b/package.json @@ -19,12 +19,14 @@ "bn.js": "^5.1.3", "bs58": "^4.0.1", "buffer-layout": "^1.2.0", + "chart.js": "^2.9.4", "craco-less": "^1.17.0", "echarts": "^4.9.0", "eventemitter3": "^4.0.7", "identicon.js": "^2.3.3", "jazzicon": "^1.5.0", "react": "^16.13.1", + "react-chartjs-2": "^2.11.1", "react-dom": "^16.13.1", "react-github-btn": "^1.2.0", "react-intl": "^5.10.2", diff --git a/src/views/marginTrading/newPosition/Breakdown.tsx b/src/views/marginTrading/newPosition/Breakdown.tsx index a497cf7..c3a8b30 100644 --- a/src/views/marginTrading/newPosition/Breakdown.tsx +++ b/src/views/marginTrading/newPosition/Breakdown.tsx @@ -3,6 +3,7 @@ import React, { useState } from 'react'; import { Position } from './interfaces'; import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons'; import tokens from '../../../config/tokens.json'; +import GainsChart from './GainsChart'; export function Breakdown({ item }: { item: Position }) { let myPart = parseFloat(item.asset?.value || '0') / item.leverage; @@ -89,6 +90,7 @@ export function Breakdown({ item }: { item: Position }) { /> + {progressBar} ); diff --git a/src/views/marginTrading/newPosition/GainsChart.tsx b/src/views/marginTrading/newPosition/GainsChart.tsx new file mode 100644 index 0000000..f98f463 --- /dev/null +++ b/src/views/marginTrading/newPosition/GainsChart.tsx @@ -0,0 +1,230 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { Line } from 'react-chartjs-2'; +import { Position } from './interfaces'; + +// Special thanks to +// https://github.com/bZxNetwork/fulcrum_ui/blob/development/packages/fulcrum-website/assets/js/trading.js +// For the basis of this code - I copied it directly from there and then modified it for our needs. +// You guys are real heroes - that is beautifully done. +const baseData = [ + { x: 0, y: 65 }, + { x: 1, y: 80 }, + { x: 2, y: 60 }, + { x: 3, y: 30 }, + { x: 4, y: 20 }, + { x: 5, y: 35 }, + { x: 6, y: 25 }, + { x: 7, y: 40 }, + { x: 8, y: 36 }, + { x: 9, y: 34 }, + { x: 10, y: 50 }, + { x: 11, y: 33 }, + { x: 12, y: 37 }, + { x: 13, y: 45 }, + { x: 14, y: 35 }, + { x: 15, y: 37 }, + { x: 16, y: 50 }, + { x: 17, y: 43 }, + { x: 18, y: 50 }, + { x: 19, y: 45 }, + { x: 20, y: 55 }, + { x: 21, y: 50 }, + { x: 22, y: 45 }, + { x: 23, y: 50 }, + { x: 24, y: 45 }, + { x: 25, y: 40 }, + { x: 26, y: 35 }, + { x: 27, y: 40 }, + { x: 28, y: 37 }, + { x: 29, y: 45 }, + { x: 30, y: 50 }, + { x: 31, y: 60 }, + { x: 32, y: 55 }, + { x: 33, y: 50 }, + { x: 34, y: 53 }, + { x: 35, y: 55 }, + { x: 36, y: 50 }, + { x: 37, y: 45 }, + { x: 38, y: 40 }, + { x: 39, y: 45 }, + { x: 40, y: 50 }, + { x: 41, y: 55 }, + { x: 42, y: 65 }, + { x: 43, y: 62 }, + { x: 44, y: 54 }, + { x: 45, y: 65 }, + { x: 46, y: 48 }, + { x: 47, y: 55 }, + { x: 48, y: 60 }, + { x: 49, y: 63 }, + { x: 50, y: 65 }, +]; + +function getChartData({ item, priceChange }: { item: Position; priceChange: number }) { + //the only way to create an immutable copy of array with objects inside. + const baseDashed = JSON.parse(JSON.stringify(baseData.slice(Math.floor(baseData.length) / 2))); + const baseSolid = JSON.parse(JSON.stringify(baseData.slice(0, Math.floor(baseData.length) / 2 + 1))); + + const leverage = item.leverage; + + baseDashed.forEach((item: { y: number; x: number }, index: number) => { + if (index !== 0) item.y += (item.y * priceChange) / 100; + }); + var leverageData = baseDashed.map((item: { x: number; y: number }, index: number) => { + if (index === 0) { + return { x: item.x, y: item.y }; + } + const gain = (priceChange * leverage) / 100; + return { x: item.x, y: item.y * (1 + gain) }; + }); + + return { + datasets: [ + { + backgroundColor: 'transparent', + borderColor: 'rgb(39, 107, 251)', + borderWidth: 4, + radius: 0, + data: baseSolid, + }, + { + backgroundColor: 'transparent', + borderColor: priceChange >= 0 ? 'rgb(51, 223, 204)' : 'rgb(255,79,79)', + borderWidth: 4, + radius: 0, + data: leverageData, + borderDash: [15, 3], + label: 'LEVERAGE', + }, + { + backgroundColor: 'transparent', + borderColor: 'rgb(86, 169, 255)', + borderWidth: 2, + radius: 0, + data: baseDashed, + borderDash: [8, 4], + label: 'HOLD', + }, + ], + }; +} + +function updateChartData({ + item, + priceChange, + chartRef, +}: { + item: Position; + priceChange: number; + chartRef: React.RefObject; +}) { + const data = getChartData({ item, priceChange }); + chartRef.current.chartInstance.data = data; + chartRef.current.chartInstance.canvas.parentNode.style.width = '100%'; + chartRef.current.chartInstance.canvas.parentNode.style.height = 'auto'; + chartRef.current.chartInstance.update(); +} + +function drawLabels(t: any, ctx: any, leverage: number, priceChange: number) { + ctx.save(); + ctx.font = 'normal normal bold 15px /1.5 Muli'; + ctx.textBaseline = 'bottom'; + + const chartInstance = t.chart; + const datasets = chartInstance.config.data.datasets; + datasets.forEach(function (ds: { label: any; borderColor: any }, index: number) { + const label = ds.label; + ctx.fillStyle = ds.borderColor; + + const meta = chartInstance.controller.getDatasetMeta(index); + const len = meta.data.length - 1; + const pointPostition = Math.floor(len / 2) - Math.floor(0.2 * len); + const x = meta.data[pointPostition]._model.x; + const xOffset = x; + const y = meta.data[pointPostition]._model.y; + let yOffset; + if (label === 'HOLD') { + yOffset = leverage * priceChange > 0 ? y * 1.2 : y * 0.8; + } else { + yOffset = leverage * priceChange > 0 ? y * 0.8 : y * 1.2; + } + + if (yOffset > chartInstance.canvas.parentNode.offsetHeight) { + // yOffset = 295; + chartInstance.canvas.parentNode.style.height = `${yOffset * 1.3}px`; + } + if (yOffset < 0) yOffset = 5; + if (label) ctx.fillText(label, xOffset, yOffset); + }); + ctx.restore(); +} + +export default function GainsChart({ item, priceChange }: { item: Position; priceChange: number }) { + const chartRef = useRef(); + useEffect(() => { + if (chartRef.current.chartInstance) updateChartData({ item, priceChange, chartRef }); + }, [priceChange, item.leverage]); + + return useMemo( + () => ( + { + const originalController = chartRef.current?.chartInstance?.controllers?.line; + if (originalController) + chartRef.current.chartInstance.controllers.line = chartRef.current.chartInstance.controllers.line.extend({ + draw: function () { + originalController.prototype.draw.call(this, arguments); + drawLabels(this, canvas.getContext('2d'), item.leverage, priceChange); + }, + }); + return getChartData({ item, priceChange }); + }} + options={{ + responsive: true, + maintainAspectRatio: true, + scaleShowLabels: false, + layout: { + padding: { + top: 30, + bottom: 80, + }, + }, + labels: { + render: 'title', + fontColor: ['green', 'white', 'red'], + precision: 2, + }, + animation: { + easing: 'easeOutExpo', + duration: 500, + }, + scales: { + xAxes: [ + { + display: false, + gridLines: { + display: false, + }, + type: 'linear', + position: 'bottom', + }, + ], + yAxes: [ + { + display: false, + gridLines: { + display: false, + }, + }, + ], + }, + legend: { + display: false, + }, + }} + /> + ), + [] + ); +}