Finish porting simulator into React (#1)
* Preview bid results; toggle bid type * Code cleanup: move hooks to where they're used * Extract header to separate component * Fix & reactify according to James's review * Remove unnecessary useMemo * Hack Chartjs type * Add some notes on DX Todos * Move non-page elements to lib/
This commit is contained in:
parent
47eba79d05
commit
634c0af85b
|
@ -11,3 +11,8 @@
|
||||||
Before committing, run `npm run format` to format your code.
|
Before committing, run `npm run format` to format your code.
|
||||||
|
|
||||||
Recommended: Use a [Prettier editor integration](https://prettier.io/docs/en/editors.html) to automatically format on save
|
Recommended: Use a [Prettier editor integration](https://prettier.io/docs/en/editors.html) to automatically format on save
|
||||||
|
|
||||||
|
## Developer Experience TODOs
|
||||||
|
|
||||||
|
- Automatically run prettier as code commit hook?
|
||||||
|
- Prevent git pushing if there are Typescript errors?
|
||||||
|
|
52
web/components/header.tsx
Normal file
52
web/components/header.tsx
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { Popover } from '@headlessui/react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
const navigation = [
|
||||||
|
{
|
||||||
|
name: 'About',
|
||||||
|
href: 'https://mantic.notion.site/About-Mantic-Markets-09bdde9044614e62a27477b4b1bf77ea',
|
||||||
|
},
|
||||||
|
{ name: 'Simulator', href: '/simulator' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
return (
|
||||||
|
<Popover as="header" className="relative">
|
||||||
|
<div className="pt-6">
|
||||||
|
<nav
|
||||||
|
className="relative max-w-7xl mx-auto flex items-center justify-between px-4 sm:px-6 bg-dark-50"
|
||||||
|
aria-label="Global"
|
||||||
|
>
|
||||||
|
<div className="flex items-center flex-1">
|
||||||
|
<div className="flex items-center justify-between w-full md:w-auto">
|
||||||
|
<Link href="/">
|
||||||
|
<a className="inline-grid grid-flow-col align-items-center h-6 sm:h-10">
|
||||||
|
<img
|
||||||
|
className="w-auto h-6 sm:h-10 inline-block mr-3"
|
||||||
|
src="/logo-icon.svg"
|
||||||
|
/>
|
||||||
|
<span className="text-white font-major-mono lowercase sm:text-2xl my-auto">
|
||||||
|
Mantic Markets
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-x-8 md:flex md:ml-16">
|
||||||
|
{navigation.map((item) => (
|
||||||
|
<Link href={item.href}>
|
||||||
|
<a
|
||||||
|
key={item.name}
|
||||||
|
className="text-base font-medium text-white hover:text-gray-300"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,57 +1,10 @@
|
||||||
import { Popover } from '@headlessui/react'
|
|
||||||
import { ConvertKitEmailForm } from './convert-kit-email-form'
|
import { ConvertKitEmailForm } from './convert-kit-email-form'
|
||||||
|
import { Header } from './header'
|
||||||
const navigation = [
|
|
||||||
{
|
|
||||||
name: 'About',
|
|
||||||
href: 'https://mantic.notion.site/About-Mantic-Markets-09bdde9044614e62a27477b4b1bf77ea',
|
|
||||||
},
|
|
||||||
{ name: 'Simulator', href: 'https://simulator.mantic.markets' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const Hero = () => {
|
export const Hero = () => {
|
||||||
return (
|
return (
|
||||||
<div className="relative overflow-hidden h-screen bg-world-trading bg-cover bg-gray-900">
|
<div className="relative overflow-hidden h-screen bg-world-trading bg-cover bg-gray-900">
|
||||||
{/* <div className="absolute w-full h-full overflow-hidden bg-world-trading bg-cover bg-gray-900 z--1" /> */}
|
<Header />
|
||||||
|
|
||||||
<Popover as="header" className="relative">
|
|
||||||
<div className="pt-6">
|
|
||||||
<nav
|
|
||||||
className="relative max-w-7xl mx-auto flex items-center justify-between px-4 sm:px-6"
|
|
||||||
aria-label="Global"
|
|
||||||
>
|
|
||||||
<div className="flex items-center flex-1">
|
|
||||||
<div className="flex items-center justify-between w-full md:w-auto">
|
|
||||||
<a
|
|
||||||
href="#"
|
|
||||||
className="inline-grid grid-flow-col align-items-center h-6 sm:h-10"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
className="w-auto h-6 sm:h-10 inline-block mr-3"
|
|
||||||
src="/logo-icon.svg"
|
|
||||||
/>
|
|
||||||
<span className="text-white font-major-mono lowercase sm:text-2xl my-auto">
|
|
||||||
Mantic Markets
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-x-8 md:flex md:ml-16">
|
|
||||||
{navigation.map((item) => (
|
|
||||||
<a
|
|
||||||
key={item.name}
|
|
||||||
href={item.href}
|
|
||||||
className="text-base font-medium text-white hover:text-gray-300"
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div className="pt-40 sm:pt-16 lg:pt-8 lg:pb-14 lg:overflow-hidden">
|
<div className="pt-40 sm:pt-16 lg:pt-8 lg:pb-14 lg:overflow-hidden">
|
||||||
<div className="mx-auto max-w-7xl lg:px-8">
|
<div className="mx-auto max-w-7xl lg:px-8">
|
||||||
|
|
|
@ -13,8 +13,8 @@ export type Entry = {
|
||||||
prob: number
|
prob: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export function makeEntries(bids: Bid[]): Entry[] {
|
function makeWeights(bids: Bid[]) {
|
||||||
const entries: Entry[] = []
|
const weights = []
|
||||||
let yesPot = 0
|
let yesPot = 0
|
||||||
let noPot = 0
|
let noPot = 0
|
||||||
// First pass: calculate all the weights
|
// First pass: calculate all the weights
|
||||||
|
@ -28,33 +28,36 @@ export function makeEntries(bids: Bid[]): Entry[] {
|
||||||
noPot += noBid
|
noPot += noBid
|
||||||
const prob = yesPot / (yesPot + noPot)
|
const prob = yesPot / (yesPot + noPot)
|
||||||
|
|
||||||
entries.push({
|
weights.push({
|
||||||
yesBid,
|
yesBid,
|
||||||
noBid,
|
noBid,
|
||||||
yesWeight,
|
yesWeight,
|
||||||
noWeight,
|
noWeight,
|
||||||
prob,
|
prob,
|
||||||
// To be filled in below
|
|
||||||
yesPayout: 0,
|
|
||||||
noPayout: 0,
|
|
||||||
yesReturn: 0,
|
|
||||||
noReturn: 0,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return weights
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeEntries(bids: Bid[]): Entry[] {
|
||||||
const YES_SEED = bids[0].yesBid
|
const YES_SEED = bids[0].yesBid
|
||||||
const NO_SEED = bids[0].noBid
|
const NO_SEED = bids[0].noBid
|
||||||
const yesWeightsSum = entries.reduce((sum, entry) => sum + entry.yesWeight, 0)
|
const weights = makeWeights(bids)
|
||||||
const noWeightsSum = entries.reduce((sum, entry) => sum + entry.noWeight, 0)
|
const yesPot = weights.reduce((sum, { yesBid }) => sum + yesBid, 0)
|
||||||
|
const noPot = weights.reduce((sum, { noBid }) => sum + noBid, 0)
|
||||||
|
const yesWeightsSum = weights.reduce((sum, entry) => sum + entry.yesWeight, 0)
|
||||||
|
const noWeightsSum = weights.reduce((sum, entry) => sum + entry.noWeight, 0)
|
||||||
// Second pass: calculate all the payouts
|
// Second pass: calculate all the payouts
|
||||||
for (const entry of entries) {
|
const entries: Entry[] = []
|
||||||
const { yesBid, noBid, yesWeight, noWeight } = entry
|
for (const weight of weights) {
|
||||||
|
const { yesBid, noBid, yesWeight, noWeight } = weight
|
||||||
// Payout: You get your initial bid back, as well as your share of the
|
// Payout: You get your initial bid back, as well as your share of the
|
||||||
// (noPot - seed) according to your yesWeight
|
// (noPot - seed) according to your yesWeight
|
||||||
entry.yesPayout = yesBid + (yesWeight / yesWeightsSum) * (noPot - NO_SEED)
|
const yesPayout = yesBid + (yesWeight / yesWeightsSum) * (noPot - NO_SEED)
|
||||||
entry.noPayout = noBid + (noWeight / noWeightsSum) * (yesPot - YES_SEED)
|
const noPayout = noBid + (noWeight / noWeightsSum) * (yesPot - YES_SEED)
|
||||||
entry.yesReturn = (entry.yesPayout - yesBid) / yesBid
|
const yesReturn = (yesPayout - yesBid) / yesBid
|
||||||
entry.noReturn = (entry.noPayout - noBid) / noBid
|
const noReturn = (noPayout - noBid) / noBid
|
||||||
|
entries.push({ ...weight, yesPayout, noPayout, yesReturn, noReturn })
|
||||||
}
|
}
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { Fragment, useEffect, useMemo, useState } from 'react'
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
Chart,
|
Chart,
|
||||||
|
@ -11,8 +11,8 @@ import {
|
||||||
} from 'chart.js'
|
} from 'chart.js'
|
||||||
import { ChartData } from 'chart.js'
|
import { ChartData } from 'chart.js'
|
||||||
import { Line } from 'react-chartjs-2'
|
import { Line } from 'react-chartjs-2'
|
||||||
import { bids as sampleBids } from './sample-bids'
|
import { bids as sampleBids } from '../../lib/simulator/sample-bids'
|
||||||
import { Entry, makeEntries } from './entries'
|
import { Entry, makeEntries } from '../../lib/simulator/entries'
|
||||||
|
|
||||||
// Auto import doesn't work for some reason...
|
// Auto import doesn't work for some reason...
|
||||||
// So we manually register ChartJS components instead:
|
// So we manually register ChartJS components instead:
|
||||||
|
@ -26,97 +26,136 @@ Chart.register(
|
||||||
Legend
|
Legend
|
||||||
)
|
)
|
||||||
|
|
||||||
function toTable(entries: Entry[]) {
|
function TableBody(props: { entries: Entry[] }) {
|
||||||
return entries.map((entry, i) => {
|
|
||||||
return (
|
return (
|
||||||
|
<tbody>
|
||||||
|
{props.entries.map((entry, i) => (
|
||||||
<tr key={i}>
|
<tr key={i}>
|
||||||
<th>{i + 1}</th>
|
<th>{i + 1}</th>
|
||||||
{toRowStart(entry)}
|
<TableRowStart entry={entry} />
|
||||||
{toRowEnd(entry)}
|
<TableRowEnd entry={entry} />
|
||||||
</tr>
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
)
|
)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toRowStart(entry: Entry) {
|
function TableRowStart(props: { entry: Entry }) {
|
||||||
|
const { entry } = props
|
||||||
if (entry.yesBid && entry.noBid) {
|
if (entry.yesBid && entry.noBid) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<td>
|
<td>
|
||||||
<div className="badge">SEED</div>
|
<div className="badge">SEED</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{entry.yesBid} / {entry.noBid}
|
{entry.yesBid} / {entry.noBid}
|
||||||
</td>
|
</td>
|
||||||
</Fragment>
|
</>
|
||||||
)
|
)
|
||||||
} else if (entry.yesBid) {
|
} else if (entry.yesBid) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<td>
|
<td>
|
||||||
<div className="badge badge-success">YES</div>
|
<div className="badge badge-success">YES</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{entry.yesBid}</td>
|
<td>{entry.yesBid}</td>
|
||||||
</Fragment>
|
</>
|
||||||
)
|
)
|
||||||
} else if (entry.noBid) {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<td>
|
<td>
|
||||||
<div className="badge badge-error">NO</div>
|
<div className="badge badge-error">NO</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{entry.noBid}</td>
|
<td>{entry.noBid}</td>
|
||||||
</Fragment>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function toRowEnd(entry: Entry) {
|
function TableRowEnd(props: { entry: Entry | null }) {
|
||||||
if (!entry.yesBid && !entry.noBid) {
|
const { entry } = props
|
||||||
|
if (!entry) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<td>N/A</td>
|
<td>N/A</td>
|
||||||
<td>N/A</td>
|
<td>N/A</td>
|
||||||
<td>N/A</td>
|
<td>N/A</td>
|
||||||
<td>N/A</td>
|
<td>N/A</td>
|
||||||
</Fragment>
|
</>
|
||||||
)
|
)
|
||||||
} else if (entry.yesBid && entry.noBid) {
|
} else if (entry.yesBid && entry.noBid) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<td>N/A</td>
|
<td>N/A</td>
|
||||||
<td>{entry.prob.toFixed(2)}</td>
|
<td>{entry.prob.toFixed(2)}</td>
|
||||||
<td>N/A</td>
|
<td>N/A</td>
|
||||||
<td>N/A</td>
|
<td>N/A</td>
|
||||||
</Fragment>
|
</>
|
||||||
)
|
)
|
||||||
} else if (entry.yesBid) {
|
} else if (entry.yesBid) {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<td>{entry.yesWeight.toFixed(2)}</td>
|
<td>{entry.yesWeight.toFixed(2)}</td>
|
||||||
<td>{entry.prob.toFixed(2)}</td>
|
<td>{entry.prob.toFixed(2)}</td>
|
||||||
<td>{entry.yesPayout.toFixed(2)}</td>
|
<td>{entry.yesPayout.toFixed(2)}</td>
|
||||||
<td>{(entry.yesReturn * 100).toFixed(2)}%</td>
|
<td>{(entry.yesReturn * 100).toFixed(2)}%</td>
|
||||||
</Fragment>
|
</>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<>
|
||||||
<td>{entry.noWeight.toFixed(2)}</td>
|
<td>{entry.noWeight.toFixed(2)}</td>
|
||||||
<td>{entry.prob.toFixed(2)}</td>
|
<td>{entry.prob.toFixed(2)}</td>
|
||||||
<td>{entry.noPayout.toFixed(2)}</td>
|
<td>{entry.noPayout.toFixed(2)}</td>
|
||||||
<td>{(entry.noReturn * 100).toFixed(2)}%</td>
|
<td>{(entry.noReturn * 100).toFixed(2)}%</td>
|
||||||
</Fragment>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function newBidTable(
|
function NewBidTable(props: {
|
||||||
steps: number,
|
steps: number
|
||||||
newBid: number,
|
bids: any[]
|
||||||
setNewBid: (newBid: number) => void,
|
setSteps: (steps: number) => void
|
||||||
submitBid: () => void
|
setBids: (bids: any[]) => void
|
||||||
) {
|
}) {
|
||||||
|
const { steps, bids, setSteps, setBids } = props
|
||||||
|
// Prepare for new bids
|
||||||
|
const [newBid, setNewBid] = useState(0)
|
||||||
|
const [newBidType, setNewBidType] = useState('YES')
|
||||||
|
|
||||||
|
function makeBid(type: string, bid: number) {
|
||||||
|
return {
|
||||||
|
yesBid: type == 'YES' ? bid : 0,
|
||||||
|
noBid: type == 'YES' ? 0 : bid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitBid() {
|
||||||
|
if (newBid <= 0) return
|
||||||
|
const bid = makeBid(newBidType, newBid)
|
||||||
|
bids.splice(steps, 0, bid)
|
||||||
|
setBids(bids)
|
||||||
|
setSteps(steps + 1)
|
||||||
|
setNewBid(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleBidType() {
|
||||||
|
setNewBidType(newBidType === 'YES' ? 'NO' : 'YES')
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextEntry = useMemo(() => {
|
||||||
|
if (newBid) {
|
||||||
|
const nextBid = makeBid(newBidType, newBid)
|
||||||
|
const fakeBids = [...bids.slice(0, steps), nextBid]
|
||||||
|
const entries = makeEntries(fakeBids)
|
||||||
|
return entries[entries.length - 1]
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}, [newBid, newBidType, steps])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="table table-compact my-8 w-full">
|
<table className="table table-compact my-8 w-full">
|
||||||
<thead>
|
<thead>
|
||||||
|
@ -137,16 +176,20 @@ function newBidTable(
|
||||||
<td>
|
<td>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
`badge clickable ` + ('YES' ? 'badge-success' : 'badge-ghost')
|
`badge hover:cursor-pointer ` +
|
||||||
|
(newBidType == 'YES' ? 'badge-success' : 'badge-ghost')
|
||||||
}
|
}
|
||||||
|
onClick={toggleBidType}
|
||||||
>
|
>
|
||||||
YES
|
YES
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
`badge clickable ` + ('NO' ? 'badge-error' : 'badge-ghost')
|
`badge hover:cursor-pointer ` +
|
||||||
|
(newBidType == 'NO' ? 'badge-error' : 'badge-ghost')
|
||||||
}
|
}
|
||||||
|
onClick={toggleBidType}
|
||||||
>
|
>
|
||||||
NO
|
NO
|
||||||
</div>
|
</div>
|
||||||
|
@ -166,7 +209,7 @@ function newBidTable(
|
||||||
onFocus={(e) => e.target.select()}
|
onFocus={(e) => e.target.select()}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{/* <EntryRow :entry="nextEntry" /> */}
|
<TableRowEnd entry={nextEntry} />
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
|
@ -191,8 +234,7 @@ export default function Simulator() {
|
||||||
() => makeEntries(bids.slice(0, steps)),
|
() => makeEntries(bids.slice(0, steps)),
|
||||||
[bids, steps]
|
[bids, steps]
|
||||||
)
|
)
|
||||||
|
const probs = entries.map((entry) => entry.prob)
|
||||||
const probs = useMemo(() => entries.map((entry) => entry.prob), [entries])
|
|
||||||
|
|
||||||
// Set up chart
|
// Set up chart
|
||||||
const [chartData, setChartData] = useState({ datasets: [] } as ChartData)
|
const [chartData, setChartData] = useState({ datasets: [] } as ChartData)
|
||||||
|
@ -210,26 +252,6 @@ export default function Simulator() {
|
||||||
})
|
})
|
||||||
}, [steps])
|
}, [steps])
|
||||||
|
|
||||||
// Prepare for new bids
|
|
||||||
const [newBid, setNewBid] = useState(0)
|
|
||||||
const [newBidType, setNewBidType] = useState('YES')
|
|
||||||
|
|
||||||
function makeBid(type: string, bid: number) {
|
|
||||||
return {
|
|
||||||
yesBid: type == 'YES' ? bid : 0,
|
|
||||||
noBid: type == 'YES' ? 0 : bid,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitBid() {
|
|
||||||
if (newBid <= 0) return
|
|
||||||
const bid = makeBid(newBidType, newBid)
|
|
||||||
bids.splice(steps, 0, bid)
|
|
||||||
setBids(bids)
|
|
||||||
setSteps(steps + 1)
|
|
||||||
setNewBid(0)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto px-12 mt-8 text-center">
|
<div className="overflow-x-auto px-12 mt-8 text-center">
|
||||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||||
|
@ -249,8 +271,7 @@ export default function Simulator() {
|
||||||
onChange={(e) => setSteps(parseInt(e.target.value))}
|
onChange={(e) => setSteps(parseInt(e.target.value))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* New bid table */}
|
<NewBidTable {...{ steps, bids, setSteps, setBids }} />
|
||||||
{newBidTable(steps, newBid, setNewBid, submitBid)}
|
|
||||||
|
|
||||||
{/* History of bids */}
|
{/* History of bids */}
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
|
@ -266,7 +287,8 @@ export default function Simulator() {
|
||||||
<th>Return</th>
|
<th>Return</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>{toTable(entries)}</tbody>
|
|
||||||
|
<TableBody entries={entries} />
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -277,7 +299,7 @@ export default function Simulator() {
|
||||||
Probability of
|
Probability of
|
||||||
<div className="badge badge-success text-2xl h-8 w-18">YES</div>
|
<div className="badge badge-success text-2xl h-8 w-18">YES</div>
|
||||||
</h1>
|
</h1>
|
||||||
<Line data={chartData} height={200} />
|
<Line data={chartData as any} height={200} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user