Re-structure inputs + better tooltips

This commit is contained in:
dapplion 2019-08-10 03:30:49 +02:00
parent f17679e651
commit 9c4de97ae3
9 changed files with 362 additions and 179 deletions

View File

@ -9,7 +9,8 @@ import Grid from "@material-ui/core/Grid";
import Button from "@material-ui/core/Button";
// Components
import Header from "./Header";
import InputParams from "./InputParams";
import CurveDesignInputParams from "./CurveDesignInputParams";
import SimulationInputParams from "./SimulationInputParams";
import SupplyVsDemandChart from "./SupplyVsDemandChart";
import ResultParams from "./ResultParams";
import PriceSimulationChart from "./PriceSimulationChart";
@ -53,8 +54,7 @@ const useStyles = makeStyles((theme: Theme) =>
backgroundColor: "#293640"
},
box: {
padding: theme.spacing(3, 3),
minHeight: 310
padding: theme.spacing(3, 3)
},
boxButton: {
padding: theme.spacing(3, 3)
@ -66,6 +66,12 @@ const useStyles = makeStyles((theme: Theme) =>
alignItems: "center",
borderBottom: "1px solid #313d47"
},
boxBorderBottom: {
borderBottom: "1px solid #313d47"
},
initialRaise: {
justifyContent: "space-between"
},
boxChart: {
width: "100%",
height: "100%",
@ -93,7 +99,8 @@ const useStyles = makeStyles((theme: Theme) =>
marginBottom: -theme.spacing(headerOffset)
},
button: {
background: "linear-gradient(290deg, #2ad179, #4ab47c)",
// background: "linear-gradient(290deg, #2ad179, #4ab47c)", // Green gradient
background: "linear-gradient(290deg, #1880e0, #3873d8)", // blue gradient
color: "white"
},
// Descriptions
@ -170,11 +177,11 @@ const resultParameterDescriptions = [
export default function App() {
const [curveParams, setCurveParams] = useState({
d0: 1e6, // Initial raise, d0 (DAI)
theta: 0.35, // fraction allocated to reserve (.)
p0: 0.1, // Hatch sale price p0 (DAI / token)
p1: 0.3, // Return factor (.)
wFee: 0.05 // friction coefficient (.)
wFee: 0.05, // friction coefficient (.)
d0: 3e6 // Initial raise, d0 (DAI)
});
const { d0, theta, p0, p1, wFee } = curveParams;
@ -321,6 +328,7 @@ export default function App() {
];
const classes = useStyles();
return (
<>
<header className={classes.header}>
@ -364,8 +372,22 @@ export default function App() {
/>
</Box>
<Box className={`${classes.box} ${classes.boxBorderBottom}`}>
<CurveDesignInputParams
curveParams={curveParams}
setCurveParams={setCurveParamsThrottle}
/>
</Box>
<Box className={`${classes.boxHeader} ${classes.initialRaise}`}>
<Typography variant="h6">Run parameters</Typography>
</Box>
<Box className={classes.box}>
<InputParams setCurveParams={setCurveParamsThrottle} />
<SimulationInputParams
curveParams={curveParams}
setCurveParams={setCurveParamsThrottle}
/>
</Box>
</Paper>
</Grid>

View File

@ -0,0 +1,103 @@
import React, { useState, useEffect } from "react";
import { InputFieldInterface, CurveParamsInterface } from "./types";
import InputParams from "./InputParams";
export default function CurveDesignInputParams({
curveParams,
setCurveParams
}: {
curveParams: CurveParamsInterface;
setCurveParams(newCurveParams: any): void;
}) {
const [theta, setTheta] = useState(0.35); // fraction allocated to reserve (.)
const [p0, setP0] = useState(0.1); // Hatch sale Price p0 (DAI / token)
const [p1, setP1] = useState(0.3); // Return factor (.)
const [wFee, setWFee] = useState(0.05); // friction coefficient (.)
useEffect(() => {
setTheta(curveParams.theta);
setP0(curveParams.p0);
setP1(curveParams.p1);
setWFee(curveParams.wFee);
}, [curveParams]);
function _setP0(newP0: number) {
setP0(newP0);
if (p1 < newP0) setP1(newP0);
else if (p1 > newP0 * maxReturnRate) setP1(newP0 * maxReturnRate);
}
function setParentCurveParams() {
setCurveParams((params: CurveParamsInterface) => ({
...params,
theta,
p0,
p1,
wFee
}));
}
const maxReturnRate = 10;
const inputFields: InputFieldInterface[] = [
{
label: "Allocation to funding pool",
value: theta,
setter: setTheta,
min: 0,
max: 0.9,
step: 0.01,
unit: "%",
suffix: "%",
format: (n: number) => `${Math.round(100 * n)}%`,
toText: (n: number) => String(+(n * 1e2).toFixed(0)),
toNum: (n: string) => parseFloat(n) * 1e-2
},
{
label: "Hatch price",
value: p0,
setter: _setP0,
min: 0.01,
max: 1,
step: 0.01,
unit: "$",
prefix: "$",
toText: (n: number) => String(+n.toFixed(2)),
toNum: (n: string) => parseFloat(n),
format: (n: number) => `$${n}`
},
{
label: "Post-hatch price",
value: p1,
setter: setP1,
min: p0 || 0.1,
max: Number((maxReturnRate * p0).toFixed(2)),
step: 0.01,
unit: "$",
prefix: "$",
toText: (n: number) => String(+n.toFixed(2)),
toNum: (n: string) => parseFloat(n),
format: (n: number) => `$${n}`
},
{
label: "Exit tribute",
value: wFee,
setter: setWFee,
min: 0,
max: 0.1,
step: 0.001,
unit: "%",
suffix: "%",
format: (n: number) => `${+(100 * n).toFixed(1)}%`,
toText: (n: number) => String(+(n * 1e2).toFixed(1)),
toNum: (n: string) => parseFloat(n) * 1e-2
}
];
return (
<InputParams
inputFields={inputFields}
onChangeCommited={setParentCurveParams}
/>
);
}

View File

@ -1,47 +1,11 @@
import React, { useState } from "react";
import {
createStyles,
makeStyles,
withStyles,
Theme
} from "@material-ui/core/styles";
import React from "react";
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import Slider from "@material-ui/core/Slider";
import Grid from "@material-ui/core/Grid";
import TextField from "@material-ui/core/TextField";
import NumberFormat from "react-number-format";
const PrettoSlider = withStyles({
root: {
height: 8
},
thumb: {
height: 24,
width: 24,
backgroundColor: "#fff",
border: "2px solid currentColor",
marginTop: -8,
marginLeft: -12,
"&:focus,&:hover,&$active": {
boxShadow: "inherit"
}
},
active: {},
valueLabel: {
left: "calc(-50% + 4px)"
},
track: {
height: 8,
borderRadius: 4
},
rail: {
height: 8,
borderRadius: 4
},
markLabel: {
top: 30
}
})(Slider);
import { InputFieldInterface } from "./types";
import PrettoSlider from "./PrettoSlider";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@ -79,6 +43,9 @@ const useStyles = makeStyles((theme: Theme) =>
},
slider: {
color: theme.palette.primary.main
},
secondaryColor: {
color: theme.palette.secondary.light
}
})
);
@ -101,118 +68,12 @@ function NumberFormatCustom(props: any) {
}
export default function InputParams({
curveParams,
setCurveParams
inputFields,
onChangeCommited
}: {
curveParams?: {
d0: number;
theta: number;
p0: number;
p1: number;
wFee: number;
};
setCurveParams(newCurveParams: any): void;
inputFields: InputFieldInterface[];
onChangeCommited(): void;
}) {
const [d0, setD0] = useState(1e6); // Initial raise, d0 (DAI)
const [theta, setTheta] = useState(0.35); // fraction allocated to reserve (.)
const [p0, setP0] = useState(0.1); // Hatch sale Price p0 (DAI / token)
const [p1, setP1] = useState(0.3); // Return factor (.)
const [wFee, setWFee] = useState(0.05); // friction coefficient (.)
function _setP0(newP0: number) {
setP0(newP0);
if (p1 < newP0) setP1(newP0);
else if (p1 > newP0 * maxReturnRate) setP1(newP0 * maxReturnRate);
}
function setParentCurveParams() {
setCurveParams({ d0, theta, p0, p1, wFee });
}
const maxReturnRate = 10;
const inputFields: {
label: string;
value: number;
setter(newValue: any): void;
min: number;
max: number;
step: number;
unit?: string;
prefix?: string;
suffix?: string;
toText?(value: number): string;
toNum?(value: string): number;
format(value: number): string;
}[] = [
{
label: "Initial raise",
value: d0,
setter: setD0,
min: 0.1e6,
max: 10e6,
step: 0.1e6,
unit: "$M",
prefix: "$",
suffix: "M",
format: (n: number) => `$${+(n * 1e-6).toFixed(1)}M`,
toText: (n: number) => String(+(n * 1e-6).toFixed(1)),
toNum: (n: string) => Math.floor(parseFloat(n) * 1e6)
},
{
label: "Allocation to funding pool",
value: theta,
setter: setTheta,
min: 0,
max: 0.9,
step: 0.01,
unit: "%",
suffix: "%",
format: (n: number) => `${Math.round(100 * n)}%`,
toText: (n: number) => String(+(n * 1e2).toFixed(0)),
toNum: (n: string) => parseFloat(n) * 1e-2
},
{
label: "Hatch price",
value: p0,
setter: _setP0,
min: 0.01,
max: 1,
step: 0.01,
unit: "$",
prefix: "$",
toText: (n: number) => String(+n.toFixed(2)),
toNum: (n: string) => parseFloat(n),
format: (n: number) => `$${n}`
},
{
label: "Post-hatch price",
value: p1,
setter: setP1,
min: p0 || 0.1,
max: Number((maxReturnRate * p0).toFixed(2)),
step: 0.01,
unit: "$",
prefix: "$",
toText: (n: number) => String(+n.toFixed(2)),
toNum: (n: string) => parseFloat(n),
format: (n: number) => `$${n}`
},
{
label: "Exit tribute",
value: wFee,
setter: setWFee,
min: 0,
max: 0.1,
step: 0.001,
unit: "%",
suffix: "%",
format: (n: number) => `${+(100 * n).toFixed(1)}%`,
toText: (n: number) => String(+(n * 1e2).toFixed(1)),
toNum: (n: string) => parseFloat(n) * 1e-2
}
];
const classes = useStyles();
return (
@ -227,6 +88,7 @@ export default function InputParams({
step,
prefix,
suffix,
secondaryColor,
format,
toText,
toNum
@ -253,7 +115,7 @@ export default function InputParams({
sanitizeInput(
toNum ? toNum(e.target.value) : parseFloat(e.target.value)
);
setParentCurveParams();
onChangeCommited();
}}
InputProps={{
inputComponent: NumberFormatCustom,
@ -269,12 +131,14 @@ export default function InputParams({
<Grid item xs={4}>
<PrettoSlider
className={classes.slider}
className={`${classes.slider} ${
secondaryColor ? classes.secondaryColor : ""
}`}
valueLabelDisplay="auto"
aria-label={label}
defaultValue={value}
onChange={(_, newValue) => sanitizeInput(Number(newValue))}
onChangeCommitted={setParentCurveParams}
onChangeCommitted={onChangeCommited}
value={value}
min={min}
max={max}

34
src/PrettoSlider.tsx Normal file
View File

@ -0,0 +1,34 @@
import { withStyles } from "@material-ui/core/styles";
import Slider from "@material-ui/core/Slider";
export default withStyles({
root: {
height: 8
},
thumb: {
height: 24,
width: 24,
backgroundColor: "#fff",
border: "2px solid currentColor",
marginTop: -8,
marginLeft: -12,
"&:focus,&:hover,&$active": {
boxShadow: "inherit"
}
},
active: {},
valueLabel: {
left: "calc(-50% + 4px)"
},
track: {
height: 8,
borderRadius: 4
},
rail: {
height: 8,
borderRadius: 4
},
markLabel: {
top: 30
}
})(Slider);

View File

@ -10,9 +10,25 @@ import {
ResponsiveContainer,
Tooltip
} from "recharts";
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
import { useTheme } from "@material-ui/styles";
import { linspace } from "./utils";
const keyHorizontal = "x";
const keyVerticalLeft = "Price (DAI/token)";
const keyVerticalRight = "Total exit tributes (DAI)";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
tooltip: {
border: "1px solid #313d47",
backgroundColor: "#384b59",
padding: theme.spacing(1),
color: "#c7ccd2"
}
})
);
function PriceSimulationChart({
priceTimeseries,
withdrawFeeTimeseries,
@ -30,10 +46,6 @@ function PriceSimulationChart({
// returnF - Return factor (.)
// wFee - friction coefficient (.)
const keyHorizontal = "x";
const keyVerticalLeft = "Price (DAI / token)";
const keyVerticalRight = "Total exit tributes (DAI)";
const data = [];
for (let t = 0; t < priceTimeseries.length; t++) {
data.push({
@ -46,6 +58,7 @@ function PriceSimulationChart({
// Chart components
const theme: any = useTheme();
const classes = useStyles();
function renderColorfulLegendText(value: string) {
return <span style={{ color: theme.palette.text.secondary }}>{value}</span>;
@ -67,6 +80,35 @@ function PriceSimulationChart({
);
}
function CustomTooltip({ active, payload, label }: any) {
if (active) {
const price = payload[0].value;
const exit = payload[1].value;
const weekNum = label;
const toolTipData: string[][] = [
["Price", price.toFixed(2), "DAI/tk"],
["Exit t.", formatter(exit), "DAI"],
["Week", weekNum, ""]
];
return (
<div className={classes.tooltip}>
<table>
<tbody>
{toolTipData.map(([name, value, unit]) => (
<tr key={name}>
<td>{name}</td>
<td>{value}</td>
<td>{unit}</td>
</tr>
))}
</tbody>
</table>
</div>
);
} else return null;
}
return (
<ResponsiveContainer debounce={1}>
<AreaChart
@ -115,7 +157,7 @@ function PriceSimulationChart({
stroke={theme.palette.text.secondary}
/>
<Tooltip formatter={value => Number(value)} />
<Tooltip content={<CustomTooltip />} />
<Area
isAnimationActive={false}
@ -146,8 +188,9 @@ function PriceSimulationChart({
yAxisId="right"
type="monotone"
dataKey={keyVerticalRight}
stroke={theme.palette.secondary.main}
fill={theme.palette.secondary.main}
stroke={theme.palette.secondary.dark}
fill={theme.palette.secondary.dark}
fillOpacity="0.8"
/>
{/* <ReferenceLine

View File

@ -0,0 +1,49 @@
import React, { useState, useEffect } from "react";
import { InputFieldInterface, CurveParamsInterface } from "./types";
import InputParams from "./InputParams";
export default function CurveDesignInputParams({
curveParams,
setCurveParams
}: {
curveParams: CurveParamsInterface;
setCurveParams(newCurveParams: any): void;
}) {
const [d0, setD0] = useState(3e6); // Initial raise, d0 (DAI)
useEffect(() => {
setD0(curveParams.d0);
}, [curveParams]);
function setParentCurveParams() {
setCurveParams((params: CurveParamsInterface) => ({
...params,
d0
}));
}
const inputFields: InputFieldInterface[] = [
{
label: "Initial raise",
value: d0,
setter: setD0,
min: 0.1e6,
max: 10e6,
step: 0.1e6,
unit: "$M",
prefix: "$",
suffix: "M",
format: (n: number) => `$${+(n * 1e-6).toFixed(1)}M`,
toText: (n: number) => String(+(n * 1e-6).toFixed(1)),
toNum: (n: string) => Math.floor(parseFloat(n) * 1e6),
secondaryColor: true
}
];
return (
<InputParams
inputFields={inputFields}
onChangeCommited={setParentCurveParams}
/>
);
}

View File

@ -10,10 +10,25 @@ import {
ResponsiveContainer,
Tooltip
} from "recharts";
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";
import { getLinspaceTicks } from "./utils";
import { getInitialParams } from "./math";
import { getInitialParams, getPriceR } from "./math";
import { useTheme } from "@material-ui/styles";
const keyHorizontal = "x";
const keyVertical = "Supply (tokens) / Reserve (DAI)";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
tooltip: {
border: "1px solid #313d47",
backgroundColor: "#384b59",
padding: theme.spacing(1),
color: "#c7ccd2"
}
})
);
function SupplyVsDemandChart({
theta,
d0,
@ -35,21 +50,23 @@ function SupplyVsDemandChart({
const {
k, // Invariant power kappa (.)
R0, // Initial reserve (DAI)
S0 // initial supply of tokens (token)
S0, // initial supply of tokens (token)
V0 // invariant coef
} = getInitialParams({
d0,
theta,
p0,
p1
});
const S_of_R = (R: number) => S0 * (R / R0) ** (1 / k);
const R0_round = Math.round(R0);
const S_of_R = (R: number) => S0 * (R / R0_round) ** (1 / k);
// Function setup
const f = S_of_R;
const from = 0;
const to = 4 * R0;
const to = 4 * R0_round;
const steps = 100 + 1; // Add 1 for the ticks to match
const step = (to - from) / steps;
const step = Math.round((to - from) / (steps - 1));
/**
* Prettify the result converting 1000000 to 1M
@ -68,12 +85,9 @@ function SupplyVsDemandChart({
: // No scale
[1, ""];
const keyHorizontal = "x";
const keyVertical = "Supply (tokens) / Reserve (DAI)";
const data = [];
for (let i = 0; i < steps; i++) {
const x = from + step * i;
const x = Math.round(from + step * i);
data.push({
[keyHorizontal]: x,
[keyVertical]: f(x)
@ -83,6 +97,7 @@ function SupplyVsDemandChart({
// Chart components
const theme: any = useTheme();
const classes = useStyles();
const formatter = (n: number) =>
(+(n / scaling).toPrecision(2)).toLocaleString();
@ -105,6 +120,34 @@ function SupplyVsDemandChart({
);
}
function CustomTooltip({ active, payload, label }: any) {
if (active) {
const supply = payload[0].value;
const reserve = label;
const price = getPriceR({ R: reserve, V0, k });
const toolTipData: string[][] = [
["Supply", formatter(supply) + unit, "tokens"],
["Reserve", formatter(reserve) + unit, "DAI"],
["Price", price.toFixed(2), "DAI/token"]
];
return (
<div className={classes.tooltip}>
<table>
<tbody>
{toolTipData.map(([name, value, unit]) => (
<tr key={name}>
<td>{name}</td>
<td>{value}</td>
<td>{unit}</td>
</tr>
))}
</tbody>
</table>
</div>
);
} else return null;
}
return (
<ResponsiveContainer debounce={1}>
<AreaChart
@ -136,7 +179,7 @@ function SupplyVsDemandChart({
domain={[0, f(to)]}
stroke={theme.palette.text.secondary}
/>
<Tooltip formatter={value => formatter(Number(value))} />
<Tooltip content={<CustomTooltip />} />
<Area
isAnimationActive={false}
type="monotone"
@ -145,7 +188,7 @@ function SupplyVsDemandChart({
fill={theme.palette.primary.main}
/>
<ReferenceLine
x={R0}
x={R0_round}
stroke={theme.palette.primary.main}
strokeDasharray="9 0"
label={<ReferenceLabel />}

View File

@ -9,7 +9,9 @@ const theme = createMuiTheme({
main: "#2ecd79"
},
secondary: {
main: "#116be0"
main: "#116be0",
light: "#0f8bff",
dark: "#116be0"
},
error: {
main: red.A400

23
src/types.ts Normal file
View File

@ -0,0 +1,23 @@
export interface InputFieldInterface {
label: string;
value: number;
setter(newValue: any): void;
min: number;
max: number;
step: number;
unit?: string;
prefix?: string;
suffix?: string;
secondaryColor?: boolean;
toText?(value: number): string;
toNum?(value: string): number;
format(value: number): string;
}
export interface CurveParamsInterface {
d0: number;
theta: number;
p0: number;
p1: number;
wFee: number;
}