feat: water wave chart

This commit is contained in:
bartosz-lipinski 2020-12-01 13:49:03 -06:00
parent d02ea83f1d
commit 2bd278fd41
6 changed files with 253 additions and 130 deletions

View File

@ -16,7 +16,7 @@ import { PublicKey } from "@solana/web3.js";
import "./style.less";
import { LoadingOutlined } from "@ant-design/icons";
import { ActionConfirmation } from "./../ActionConfirmation";
import { marks } from "../../constants";
import { LABELS, marks } from "../../constants";
const antIcon = <LoadingOutlined style={{ fontSize: 24 }} spin />;
@ -37,7 +37,6 @@ export const DepositInput = (props: {
const { accounts: fromAccounts, balance, balanceLamports } = useUserBalance(
reserve?.liquidityMint
);
// const collateralBalance = useUserBalance(reserve?.collateralMint);
const convert = useCallback(
(val: string | number) => {
@ -111,15 +110,13 @@ export const DepositInput = (props: {
}}
>
<div className="deposit-input-title">
How much would you like to deposit?
{LABELS.DEPOSIT_QUESTION}
</div>
<div className="token-input">
<TokenIcon mintAddress={reserve?.liquidityMint} />
<NumericInput
value={value}
onChange={(val: any) => {
setValue(val);
}}
onChange={setValue}
autoFocus={true}
style={{
fontSize: 20,
@ -139,7 +136,7 @@ export const DepositInput = (props: {
onClick={onDeposit}
disabled={fromAccounts.length === 0 || pendingTx}
>
Deposit
{LABELS.DEPOSIT_ACTION}
{pendingTx && (
<Spin indicator={antIcon} className="action-spinner" />
)}

View File

@ -1,12 +1,12 @@
import React, { useCallback, useMemo, useState } from "react";
import { useAccountByMint, useTokenName, useUserBalance } from "../../hooks";
import { InputType, useAccountByMint, useSliderInput, useTokenName, useUserBalance } from "../../hooks";
import {
LendingObligation,
LendingReserve,
LendingReserveParser,
} from "../../models";
import { TokenIcon } from "../TokenIcon";
import { Button, Card, Spin } from "antd";
import { Button, Card, Slider, Spin } from "antd";
import { cache, ParsedAccount, useMint } from "../../contexts/accounts";
import { NumericInput } from "../Input/numeric";
import { useConnection } from "../../contexts/connection";
@ -14,8 +14,7 @@ import { useWallet } from "../../contexts/wallet";
import { repay } from "../../actions";
import { CollateralSelector } from "./../CollateralSelector";
import "./style.less";
import { formatNumber, toLamports } from "../../utils/utils";
import { LABELS } from "../../constants";
import { LABELS, marks } from "../../constants";
import { LoadingOutlined } from "@ant-design/icons";
import { ActionConfirmation } from "./../ActionConfirmation";
@ -28,7 +27,6 @@ export const RepayInput = (props: {
}) => {
const connection = useConnection();
const { wallet } = useWallet();
const [value, setValue] = useState("");
const [pendingTx, setPendingTx] = useState(false);
const [showConfirmation, setShowConfirmation] = useState(false);
@ -47,7 +45,7 @@ export const RepayInput = (props: {
}, [collateralReserveMint]);
const name = useTokenName(repayReserve?.info.liquidityMint);
const { accounts: fromAccounts, balance } = useUserBalance(
const { accounts: fromAccounts, balance, balanceLamports } = useUserBalance(
repayReserve.info.liquidityMint
);
@ -56,11 +54,19 @@ export const RepayInput = (props: {
const obligationAccount = useAccountByMint(obligation?.info.tokenMint);
const lamports = useMemo(
() => toLamports(parseFloat(value), repayLiquidityMint),
[value, repayLiquidityMint]
const convert = useCallback(
(val: string | number) => {
if (typeof val === "string") {
return (parseFloat(val) / balance) * 100;
} else {
return ((val * balance) / 100).toFixed(2);
}
},
[balance]
);
const { value, setValue, mark, setMark, type } = useSliderInput(convert);
const onRepay = useCallback(() => {
if (
!collateralReserve ||
@ -77,7 +83,9 @@ export const RepayInput = (props: {
try {
await repay(
fromAccounts[0],
lamports,
type === InputType.Slider
? (mark * balanceLamports) / 100
: Math.ceil(balanceLamports * (parseFloat(value) / balance)),
obligation,
obligationAccount,
repayReserve,
@ -95,7 +103,11 @@ export const RepayInput = (props: {
}
})();
}, [
lamports,
mark,
value,
balance,
balanceLamports,
type,
connection,
wallet,
obligation,
@ -126,16 +138,13 @@ export const RepayInput = (props: {
}}
>
<div className="repay-input-title">
{LABELS.REPAY_QUESTION} (Currently: ){formatNumber.format(balance)}{" "}
{name})
{LABELS.REPAY_QUESTION}
</div>
<div className="token-input">
<TokenIcon mintAddress={repayReserve?.info.liquidityMint} />
<NumericInput
value={value}
onChange={(val: any) => {
setValue(val);
}}
onChange={setValue}
autoFocus={true}
style={{
fontSize: 20,
@ -147,23 +156,11 @@ export const RepayInput = (props: {
/>
<div>{name}</div>
</div>
{/* TODO: finish slider implementation */}
{/* <Slider
marks={marks}
value={mark}
onChange={(val: number) =>
setValue(
(
(fromLamports(
wadToLamports(obligation?.info.borrowAmountWad).toNumber(),
repayLiquidityMint
) *
val) /
100
).toFixed(2)
)
}
/> */}
<Slider
marks={marks}
value={mark}
onChange={setMark}
/>
<div className="repay-input-title">{LABELS.SELECT_COLLATERAL}</div>
<CollateralSelector
reserve={repayReserve.info}

View File

@ -1,26 +1,13 @@
import React, { useEffect, useMemo, useRef } from "react";
import React, { useMemo } from "react";
import { LendingReserve } from "../../models/lending";
import echarts from "echarts";
import {
formatNumber,
formatUSD,
fromLamports,
wadToLamports,
} from "../../utils/utils";
import { useMint } from "../../contexts/accounts";
import { WaterWave } from "./../WaterWave";
export const ReserveUtilizationChart = (props: { reserve: LendingReserve }) => {
const chartDiv = useRef<HTMLDivElement>(null);
// dispose chart
useEffect(() => {
const div = chartDiv.current;
return () => {
let instance = div && echarts.getInstanceByDom(div);
instance && instance.dispose();
};
}, []);
const liquidityMint = useMint(props.reserve.liquidityMint);
const availableLiquidity = fromLamports(
props.reserve.availableLiquidity.toNumber(),
@ -36,79 +23,7 @@ export const ReserveUtilizationChart = (props: { reserve: LendingReserve }) => {
[props.reserve, liquidityMint]
);
useEffect(() => {
if (!chartDiv.current) {
return;
}
let instance = echarts.getInstanceByDom(chartDiv.current);
if (!instance) {
instance = echarts.init(chartDiv.current as any);
}
const data = [
{
name: "Available Liquidity",
value: availableLiquidity,
tokens: availableLiquidity,
},
{
name: "Total Borrowed",
value: totalBorrows,
tokens: totalBorrows,
},
];
instance.setOption({
tooltip: {
trigger: "item",
formatter: function (params: any) {
var val = formatUSD.format(params.value);
var tokenAmount = formatNumber.format(params.data.tokens);
return `${params.name}: \n${val}\n(${tokenAmount})`;
},
},
series: [
{
name: "Liquidity",
type: "pie",
radius: ["50%", "70%"],
top: 0,
bottom: 0,
left: 0,
right: 0,
animation: false,
label: {
fontSize: 14,
show: true,
formatter: function (params: any) {
var val = formatUSD.format(params.value);
var tokenAmount = formatNumber.format(params.data.tokens);
return `{c|${params.name}}\n{r|${tokenAmount}}\n{r|${val}}`;
},
rich: {
c: {
color: "#999",
lineHeight: 22,
align: "center",
},
r: {
color: "#999",
align: "right",
},
},
color: "rgba(255, 255, 255, 0.5)",
},
itemStyle: {
normal: {
borderColor: "#000",
},
},
data,
},
],
});
}, [totalBorrows, availableLiquidity]);
return <div ref={chartDiv} style={{ height: 300, width: 400 }} />;
return <WaterWave
style={{ height: 300 }}
percent={availableLiquidity * 100 / (availableLiquidity + totalBorrows)} />;
};

View File

@ -0,0 +1,29 @@
@import '~antd/lib/style/themes/default.less';
.waterWave {
display: inline-block;
position: relative;
transform-origin: left;
.text {
position: absolute;
left: 5px;
top: calc(50% - 15px);
text-align: center;
width: 100%;
span {
color: @text-color-secondary;
font-size: 14px;
line-height: 22px;
}
h4 {
color: @heading-color;
line-height: 32px;
font-size: 24px;
}
}
.waterWaveCanvasWrapper {
transform: scale(0.5);
transform-origin: 0 0;
}
}

View File

@ -0,0 +1,184 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import "./index.less";
export const WaterWave = (props: any) => {
const node = useRef<HTMLCanvasElement>();
const root = useRef<HTMLDivElement>();
const [radio, setRadio] = useState(1);
const { percent, title, style, color } = props;
const { height } = style;
const resize = useCallback(() => {
const { offsetWidth } = root.current?.parentNode as HTMLElement;
setRadio(offsetWidth < height ? offsetWidth / height : 1);
}, [height]);
// resize
useEffect(() => {
resize();
window.addEventListener('resize', resize);
return () => {
window.removeEventListener('resize', resize);
};
}, [resize]);
useEffect(() => {
let timer = 0;
renderChart(
node.current,
percent,
(val) => {
timer = val;
},
color);
return () => {
cancelAnimationFrame(timer);
}
}, [percent, color]);
return (
<div
className="waterWave"
ref={root as any}
style={{ transform: `scale(${radio})` }}
>
<div style={{ width: height, height, overflow: 'hidden' }}>
<canvas
className="waterWaveCanvasWrapper"
ref={node as any}
width={height * 2}
height={height * 2}
/>
</div>
<div className="text" style={{ width: height }}>
{title && <span>{title}</span>}
<h4>{percent.toFixed(2)}%</h4>
</div>
</div>
);
}
const renderChart = (canvas: HTMLCanvasElement | undefined, percent: number, setTimer: (timer: number) => void, color = '#1890FF',) => {
const data = percent / 100;
if (!canvas || !data) {
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
return;
}
const canvasWidth = canvas.width;
const canvasHeight = canvas.height;
const radius = canvasWidth / 2;
const lineWidth = 2;
const cR = radius - lineWidth;
ctx.beginPath();
ctx.lineWidth = lineWidth * 2;
const axisLength = canvasWidth - lineWidth;
const unit = axisLength / 8;
const range = 0.2;
let currRange = range;
const xOffset = lineWidth;
let sp = 0;
let currData = 0;
const waveupsp = 0.005;
const bR = radius - lineWidth;
const circleOffset = -(Math.PI / 2);
let circleLock = true;
const cStartPoint = [radius + bR * Math.cos(circleOffset), radius + bR * Math.sin(circleOffset)];
ctx.strokeStyle = color;
ctx.moveTo(cStartPoint[0], cStartPoint[1]);
const drawSin = () => {
ctx.beginPath();
ctx.save();
const sinStack = [];
for (let i = xOffset; i <= xOffset + axisLength; i += 20 / axisLength) {
const x = sp + (xOffset + i) / unit;
const y = Math.sin(x) * currRange;
const dx = i;
const dy = 2 * cR * (1 - currData) + (radius - cR) - unit * y;
ctx.lineTo(dx, dy);
sinStack.push([dx, dy]);
}
const startPoint = sinStack.shift();
if (!startPoint) {
return;
}
ctx.lineTo(xOffset + axisLength, canvasHeight);
ctx.lineTo(xOffset, canvasHeight);
ctx.lineTo(startPoint[0], startPoint[1]);
const gradient = ctx.createLinearGradient(0, 0, 0, canvasHeight);
gradient.addColorStop(0, '#ffffff');
gradient.addColorStop(1, '#1890FF');
ctx.fillStyle = gradient;
ctx.fill();
ctx.restore();
}
const render = () => {
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
if (circleLock) {
circleLock = false;
ctx.globalCompositeOperation = 'destination-over';
ctx.beginPath();
ctx.save();
ctx.arc(radius, radius, radius - 3 * lineWidth, 0, 2 * Math.PI, true);
ctx.stroke();
ctx.restore();
ctx.clip();
ctx.fillStyle = '#1890FF';
} else {
if (data >= 0.85) {
if (currRange > range / 4) {
const t = range * 0.01;
currRange -= t;
}
} else if (data <= 0.1) {
if (currRange < range * 1.5) {
const t = range * 0.01;
currRange += t;
}
} else {
if (currRange <= range) {
const t = range * 0.01;
currRange += t;
}
if (currRange >= range) {
const t = range * 0.01;
currRange -= t;
}
}
if (data - currData > 0) {
currData += waveupsp;
}
if (data - currData < 0) {
currData -= waveupsp;
}
sp += 0.07;
drawSin();
}
setTimer(requestAnimationFrame(render));
}
render();
}

View File

@ -37,6 +37,7 @@ export const LABELS = {
TABLE_TITLE_MAX_BORROW: "Available for you",
DASHBOARD_TITLE_LOANS: "Loans",
DASHBOARD_TITLE_DEPOSITS: "Deposits",
DEPOSIT_QUESTION: "How much would you like to deposit?",
WITHDRAW_ACTION: "Withdraw",
WITHDRAW_QUESTION: "How much would you like to withdraw?",
DASHBOARD_ACTION: "Go to dashboard",