|
1 | 1 | import { ISimulationBlock, ISimulationTransaction } from '@/contexts/SimContext/types'; |
2 | | -import cx from "classnames"; |
3 | | -import { FC, MouseEvent, PropsWithChildren, useMemo, useState } from "react"; |
4 | | - |
5 | 2 | import { printBytes } from '@/utils'; |
6 | | -import classes from "./styles.module.css"; |
| 3 | +import Highcharts from 'highcharts'; |
| 4 | +import HighchartsReact from 'highcharts-react-official'; |
| 5 | +import { FC, useMemo, useState } from "react"; |
7 | 6 |
|
8 | | -export interface IBlockContentsProps { |
9 | | - block: ISimulationBlock; |
| 7 | +// highcharts modules can only load client-side |
| 8 | +if (typeof window === 'object') { |
| 9 | + await import('highcharts/modules/sankey'); |
| 10 | + await import('highcharts/modules/organization'); |
10 | 11 | } |
11 | 12 |
|
12 | | -interface IBoxProps extends PropsWithChildren { |
13 | | - className?: string; |
14 | | - selected?: boolean; |
15 | | - proportion?: number; |
16 | | - onClick?: (e: MouseEvent) => void; |
17 | | -} |
| 13 | +type NodeOptions = Highcharts.SeriesSankeyNodesOptionsObject & { width?: number }; |
18 | 14 |
|
19 | | -const Box: FC<IBoxProps> = ({ selected, proportion = 1, onClick, children, className }) => { |
20 | | - const color = selected ? "border-black" : "border-gray-400"; |
21 | | - return ( |
22 | | - <span onClick={onClick} className={cx("border-2 border-solid w-48 min-h-16 max-h-48 flex flex-col items-center justify-center text-center", color, { 'cursor-pointer': !!onClick }, className)} style={{ |
23 | | - height: 32 * proportion |
24 | | - }}> |
25 | | - {children} |
26 | | - </span> |
27 | | - ) |
| 15 | +export interface IBlockContentsProps { |
| 16 | + block: ISimulationBlock; |
28 | 17 | } |
29 | 18 |
|
30 | 19 | interface ITXStats { |
@@ -117,62 +106,114 @@ export const BlockContents: FC<IBlockContentsProps> = ({ block }) => { |
117 | 106 | }, [block]); |
118 | 107 |
|
119 | 108 | const [selected, setSelected] = useState<SelectState | null>(null); |
120 | | - const selectBox = (box: string | null) => (e: MouseEvent) => { |
121 | | - e.stopPropagation(); |
122 | | - if (box) { |
123 | | - setSelected({ |
124 | | - key: box, |
125 | | - position: [e.pageX, e.pageY], |
| 109 | + |
| 110 | + const options = useMemo(() => { |
| 111 | + const eb = block.cert?.eb; |
| 112 | + const ibs = block.cert?.eb?.ibs ?? []; |
| 113 | + |
| 114 | + const nodes: NodeOptions[] = [ |
| 115 | + { |
| 116 | + id: 'block', |
| 117 | + name: 'Block', |
| 118 | + title: `Slot ${block.slot}, ${block.txs.length} TX(s)`, |
| 119 | + width: 200, |
| 120 | + height: 100, |
| 121 | + } |
| 122 | + ]; |
| 123 | + const edges: Highcharts.SeriesSankeyPointOptionsObject[] = [ |
| 124 | + { from: 'block' }, |
| 125 | + ]; |
| 126 | + |
| 127 | + if (eb) { |
| 128 | + nodes.push({ |
| 129 | + id: 'eb', |
| 130 | + name: 'Endorsement Block', |
| 131 | + title: `Slot ${eb.slot}`, |
| 132 | + width: 200, |
126 | 133 | }); |
127 | | - } else { |
128 | | - setSelected(null); |
| 134 | + edges.push({ from: 'block', to: 'eb' }); |
| 135 | + } |
| 136 | + |
| 137 | + for (const ib of ibs) { |
| 138 | + const node: NodeOptions = { |
| 139 | + id: ib.id, |
| 140 | + name: 'Input Block', |
| 141 | + title: `Slot ${ib.slot}, ${ib.txs.length} TX(s)`, |
| 142 | + height: Math.max(50, 100 * Math.min(ib.txs.length / block.txs.length, 200)), |
| 143 | + }; |
| 144 | + if (ibs.length <= 3) { |
| 145 | + node.width = 200; |
| 146 | + } |
| 147 | + nodes.push(node); |
| 148 | + edges.push({ from: 'eb', to: ib.id }); |
129 | 149 | } |
130 | | - }; |
131 | 150 |
|
132 | | - const eb = block.cert?.eb; |
133 | | - const ibs = block.cert?.eb?.ibs ?? []; |
| 151 | + const series: Highcharts.SeriesOrganizationOptions = { |
| 152 | + type: 'organization', |
| 153 | + data: edges, |
| 154 | + levels: [ |
| 155 | + { |
| 156 | + level: 0, |
| 157 | + color: '#8884d8' |
| 158 | + }, |
| 159 | + { |
| 160 | + level: 1, |
| 161 | + color: '#cccccc' |
| 162 | + }, |
| 163 | + { |
| 164 | + level: 2, |
| 165 | + color: '#82ca9d' |
| 166 | + } |
| 167 | + ], |
| 168 | + nodes, |
| 169 | + colorByPoint: false, |
| 170 | + nodeWidth: 100, |
| 171 | + events: { |
| 172 | + click(event: any) { |
| 173 | + const id = event.point.id as string; |
| 174 | + event.stopPropagation(); |
| 175 | + setSelected({ |
| 176 | + key: id, |
| 177 | + position: [event.pageX, event.pageY], |
| 178 | + }); |
| 179 | + } |
| 180 | + } |
| 181 | + }; |
| 182 | + |
| 183 | + const title = 'Block Transactions'; |
| 184 | + const subtitle = allTxs.size === 1 ? `1 transaction` : `${allTxs.size} transactions`; |
| 185 | + const options: Highcharts.Options = { |
| 186 | + chart: { |
| 187 | + inverted: true, |
| 188 | + width: 640, |
| 189 | + height: 500, |
| 190 | + events: { |
| 191 | + click() { |
| 192 | + setSelected(null); |
| 193 | + } |
| 194 | + }, |
| 195 | + }, |
| 196 | + title: { |
| 197 | + text: `<h2 class="font-bold text-xl">${title}</h2><h3 class="font-bold text-l">${subtitle}</h3>`, |
| 198 | + useHTML: true, |
| 199 | + }, |
| 200 | + tooltip: { |
| 201 | + enabled: false, |
| 202 | + }, |
| 203 | + series: [series], |
| 204 | + }; |
| 205 | + return options; |
| 206 | + }, [block]); |
| 207 | + |
134 | 208 |
|
135 | 209 | return ( |
136 | 210 | <> |
137 | 211 | {selected && <Stats {...stats.get(selected.key)!} position={selected.position} />} |
138 | | - |
139 | | - <div className='flex flex-col w-full h-3/5 items-center' onClick={selectBox(null)}> |
140 | | - <h2 className='font-bold text-xl'>Block Transactions</h2> |
141 | | - <h2 className='font-bold text-l'>{allTxs.size} transaction{allTxs.size === 1 ? '' : 's'} total</h2> |
142 | | - <div className="flex w-full h-full items-center justify-center"> |
143 | | - {ibs.length ? ( |
144 | | - <div className={cx("pr-6 border-r-2 border-black")}> |
145 | | - <div className="flex flex-col gap-2"> |
146 | | - {ibs.map(ib => { |
147 | | - const isSelected = selected?.key === ib.id; |
148 | | - const proportion = 2 * ib.txs.length / block.txs.length; |
149 | | - return ( |
150 | | - <Box key={ib.id} selected={isSelected} proportion={proportion} onClick={selectBox(ib.id)} className={classes.input}> |
151 | | - Input Block |
152 | | - <span className="text-sm">Slot {ib.slot}, {ib.txs.length} TX</span> |
153 | | - </Box> |
154 | | - ); |
155 | | - })} |
156 | | - </div> |
157 | | - </div> |
158 | | - ) : null} |
159 | | - {eb && ( |
160 | | - <div className={cx('pr-4 pl-6', classes.endorser, { [classes['has-ibs']]: ibs.length })}> |
161 | | - <Box selected={selected?.key === "eb"} proportion={1} onClick={selectBox("eb")}> |
162 | | - Endorsement Block |
163 | | - <span className='text-sm'>Slot {eb.slot}</span> |
164 | | - </Box> |
165 | | - </div> |
166 | | - )} |
167 | | - <div className={cx('pl-4', classes.block, { [classes['has-eb']]: !!eb })}> |
168 | | - <Box selected={selected?.key === "block"} proportion={2} onClick={selectBox("block")}> |
169 | | - Block |
170 | | - <span className='text-sm'>Slot {block.slot}, {block.txs.length} TX</span> |
171 | | - </Box> |
172 | | - </div> |
173 | | - </div> |
174 | | - |
175 | | - </div> |
| 212 | + <HighchartsReact |
| 213 | + highcharts={Highcharts} |
| 214 | + options={options} |
| 215 | + allowChartUpdate={false} |
| 216 | + /> |
176 | 217 | </> |
177 | 218 | ); |
178 | 219 | } |
0 commit comments