Skip to content

Commit e449410

Browse files
committed
Use highcharts to render nicer blocks view
1 parent 616e6a3 commit e449410

File tree

4 files changed

+125
-72
lines changed

4 files changed

+125
-72
lines changed

ui/bun.lockb

818 Bytes
Binary file not shown.

ui/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
"concat-stream": "^2.0.0",
3434
"d3": "^7.9.0",
3535
"debounce": "^2.2.0",
36+
"highcharts": "^12.1.2",
37+
"highcharts-react-official": "^3.2.1",
3638
"linebyline": "^1.3.0",
3739
"msgpack-lite": "^0.1.26",
3840
"next": "^15.1.6",

ui/src/components/Blocks/modules/BlockContents.tsx

Lines changed: 111 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,19 @@
11
import { ISimulationBlock, ISimulationTransaction } from '@/contexts/SimContext/types';
2-
import cx from "classnames";
3-
import { FC, MouseEvent, PropsWithChildren, useMemo, useState } from "react";
4-
52
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";
76

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');
1011
}
1112

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 };
1814

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;
2817
}
2918

3019
interface ITXStats {
@@ -117,62 +106,114 @@ export const BlockContents: FC<IBlockContentsProps> = ({ block }) => {
117106
}, [block]);
118107

119108
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,
126133
});
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 });
129149
}
130-
};
131150

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+
134208

135209
return (
136210
<>
137211
{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+
/>
176217
</>
177218
);
178219
}

ui/yarn.lock

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
22
# yarn lockfile v1
3-
# bun ./bun.lockb --hash: 5B20F68013EADAD7-492c5a926412da30-957145A4C239BECD-8a939d63ce674a08
3+
# bun ./bun.lockb --hash: 0AB71206C8C4C585-888824034f71d3d4-1B5B5B8A5FFF4A71-fd40629fa34e5785
44

55

66
"3d-force-graph@^1.76":
@@ -1964,6 +1964,16 @@ hasown@^2.0.2:
19641964
dependencies:
19651965
function-bind "^1.1.2"
19661966

1967+
highcharts@>=6.0.0, highcharts@^12.1.2:
1968+
version "12.1.2"
1969+
resolved "https://registry.npmjs.org/highcharts/-/highcharts-12.1.2.tgz"
1970+
integrity sha512-paZ72q1um0zZT1sS+O/3JfXVSOLPmZ0zlo8SgRc0rEplPFPQUPc4VpkgQS8IUTueeOBgIWwVpAWyC9tBYbQ0kg==
1971+
1972+
highcharts-react-official@^3.2.1:
1973+
version "3.2.1"
1974+
resolved "https://registry.npmjs.org/highcharts-react-official/-/highcharts-react-official-3.2.1.tgz"
1975+
integrity sha512-hyQTX7ezCxl7JqumaWiGsroGWalzh24GedQIgO3vJbkGOZ6ySRAltIYjfxhrq4HszJOySZegotEF7v+haQ75UA==
1976+
19671977
http-cache-semantics@^4.0.0:
19681978
version "4.1.1"
19691979
resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz"
@@ -2623,7 +2633,7 @@ queue-microtask@^1.2.2:
26232633
resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
26242634
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
26252635

2626-
react@*, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0", react@>=16.13.1, react@>=16.6.0, "react@^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0", "react@^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react@^18.3.1:
2636+
react@*, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0", react@>=16.13.1, react@>=16.6.0, react@>=16.8.0, "react@^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0", "react@^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", react@^18.3.1:
26272637
version "18.3.1"
26282638
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
26292639
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==

0 commit comments

Comments
 (0)