mirror of
https://github.com/NotAShelf/nix-evaluator-stats.git
synced 2026-05-18 21:17:36 +00:00
packages/web: allow selecting integer precision in visuals & comparison view
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Idd4ed0b32ba827254de9c501685d5edb6a6a6964
This commit is contained in:
parent
6ec645f3de
commit
5af2457177
8 changed files with 143 additions and 48 deletions
|
|
@ -1,23 +1,23 @@
|
||||||
export function formatBytes(bytes: number): string {
|
export function formatBytes(bytes: number, precision = 2): string {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
const units = ['B', 'KB', 'MB', 'GB'];
|
const units = ['B', 'KB', 'MB', 'GB'];
|
||||||
const i = Math.floor(Math.log2(bytes) / 10);
|
const i = Math.floor(Math.log2(bytes) / 10);
|
||||||
return `${(bytes / (1 << (i * 10))).toFixed(2)} ${units[i]}`;
|
return `${(bytes / (1 << (i * 10))).toFixed(precision)} ${units[i]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatNumber(num: number): string {
|
export function formatNumber(num: number, precision = 2): string {
|
||||||
if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B';
|
if (num >= 1e9) return (num / 1e9).toFixed(precision) + 'B';
|
||||||
if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M';
|
if (num >= 1e6) return (num / 1e6).toFixed(precision) + 'M';
|
||||||
if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K';
|
if (num >= 1e3) return (num / 1e3).toFixed(precision) + 'K';
|
||||||
return num.toString();
|
return num.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatTime(seconds: number): string {
|
export function formatTime(seconds: number, precision = 2): string {
|
||||||
if (seconds < 0.001) return (seconds * 1e6).toFixed(2) + 'μs';
|
if (seconds < 0.001) return (seconds * 1e6).toFixed(precision) + 'μs';
|
||||||
if (seconds < 1) return (seconds * 1000).toFixed(2) + 'ms';
|
if (seconds < 1) return (seconds * 1000).toFixed(precision) + 'ms';
|
||||||
return seconds.toFixed(3) + 's';
|
return seconds.toFixed(3) + 's';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatPercent(value: number): string {
|
export function formatPercent(value: number, precision = 2): string {
|
||||||
return (value * 100).toFixed(2) + '%';
|
return (value * 100).toFixed(precision) + '%';
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,8 @@ const TOOLTIPS = {
|
||||||
attrSelect: 'Number of attribute selections performed (accessing .attr from an attribute set)',
|
attrSelect: 'Number of attribute selections performed (accessing .attr from an attribute set)',
|
||||||
};
|
};
|
||||||
|
|
||||||
const Analysis: Component<{ stats: StatsData }> = props => {
|
const Analysis: Component<{ stats: StatsData; precision?: number }> = props => {
|
||||||
|
const prec = () => props.precision ?? 2;
|
||||||
const totalMemory = createMemo(
|
const totalMemory = createMemo(
|
||||||
() =>
|
() =>
|
||||||
props.stats.envs.bytes +
|
props.stats.envs.bytes +
|
||||||
|
|
@ -105,18 +106,22 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
||||||
<div class="header-stats">
|
<div class="header-stats">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="CPU Time"
|
label="CPU Time"
|
||||||
value={formatTime(props.stats.cpuTime)}
|
value={formatTime(props.stats.cpuTime, prec())}
|
||||||
tooltip={TOOLTIPS.cpuTime}
|
tooltip={TOOLTIPS.cpuTime}
|
||||||
/>
|
/>
|
||||||
<MetricCard label="Memory" value={formatBytes(totalMemory())} tooltip={TOOLTIPS.memory} />
|
<MetricCard
|
||||||
|
label="Memory"
|
||||||
|
value={formatBytes(totalMemory(), prec())}
|
||||||
|
tooltip={TOOLTIPS.memory}
|
||||||
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Expressions"
|
label="Expressions"
|
||||||
value={formatNumber(props.stats.nrExprs)}
|
value={formatNumber(props.stats.nrExprs, prec())}
|
||||||
tooltip={TOOLTIPS.nrExprs}
|
tooltip={TOOLTIPS.nrExprs}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Thunks"
|
label="Thunks"
|
||||||
value={`${formatNumber(props.stats.nrAvoided)} / ${formatNumber(props.stats.nrThunks)}`}
|
value={`${formatNumber(props.stats.nrAvoided, prec())} / ${formatNumber(props.stats.nrThunks, prec())}`}
|
||||||
tooltip="Avoided / Created thunks. Avoided means the value was already computed and reused."
|
tooltip="Avoided / Created thunks. Avoided means the value was already computed and reused."
|
||||||
highlight={props.stats.nrAvoided >= props.stats.nrThunks}
|
highlight={props.stats.nrAvoided >= props.stats.nrThunks}
|
||||||
/>
|
/>
|
||||||
|
|
@ -131,11 +136,11 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
||||||
<div class="gc-header">GC Statistics</div>
|
<div class="gc-header">GC Statistics</div>
|
||||||
<div class="gc-values">
|
<div class="gc-values">
|
||||||
<span class="gc-label">Heap:</span>
|
<span class="gc-label">Heap:</span>
|
||||||
<span class="gc-value">{formatBytes(props.stats.gc?.heapSize || 0)}</span>
|
<span class="gc-value">{formatBytes(props.stats.gc?.heapSize || 0, prec())}</span>
|
||||||
<span class="gc-label">Allocated:</span>
|
<span class="gc-label">Allocated:</span>
|
||||||
<span class="gc-value">{formatBytes(props.stats.gc?.totalBytes || 0)}</span>
|
<span class="gc-value">{formatBytes(props.stats.gc?.totalBytes || 0, prec())}</span>
|
||||||
<span class="gc-label">Cycles:</span>
|
<span class="gc-label">Cycles:</span>
|
||||||
<span class="gc-value">{formatNumber(props.stats.gc?.cycles || 0)}</span>
|
<span class="gc-value">{formatNumber(props.stats.gc?.cycles || 0, prec())}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
@ -143,15 +148,15 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
||||||
|
|
||||||
<Section title="Time & Thunks" collapsible>
|
<Section title="Time & Thunks" collapsible>
|
||||||
<div class="time-thunks-combined">
|
<div class="time-thunks-combined">
|
||||||
<TimeChart stats={props.stats} />
|
<TimeChart stats={props.stats} precision={prec()} />
|
||||||
<div class="thunks-section">
|
<div class="thunks-section">
|
||||||
<ThunkChart stats={props.stats} />
|
<ThunkChart stats={props.stats} precision={prec()} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section title="Operations" collapsible>
|
<Section title="Operations" collapsible>
|
||||||
<OperationsChart stats={props.stats} />
|
<OperationsChart stats={props.stats} precision={prec()} />
|
||||||
</Section>
|
</Section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -160,17 +165,17 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
||||||
<div class="metrics-grid small">
|
<div class="metrics-grid small">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Count"
|
label="Count"
|
||||||
value={formatNumber(props.stats.envs.number)}
|
value={formatNumber(props.stats.envs.number, prec())}
|
||||||
tooltip={TOOLTIPS.envsNumber}
|
tooltip={TOOLTIPS.envsNumber}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Elements"
|
label="Elements"
|
||||||
value={formatNumber(props.stats.envs.elements)}
|
value={formatNumber(props.stats.envs.elements, prec())}
|
||||||
tooltip={TOOLTIPS.envsElements}
|
tooltip={TOOLTIPS.envsElements}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Memory"
|
label="Memory"
|
||||||
value={formatBytes(props.stats.envs.bytes)}
|
value={formatBytes(props.stats.envs.bytes, prec())}
|
||||||
tooltip={TOOLTIPS.envsBytes}
|
tooltip={TOOLTIPS.envsBytes}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -180,22 +185,22 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
||||||
<div class="metrics-grid small">
|
<div class="metrics-grid small">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Values"
|
label="Values"
|
||||||
value={formatNumber(props.stats.values.number)}
|
value={formatNumber(props.stats.values.number, prec())}
|
||||||
tooltip={TOOLTIPS.valuesNumber}
|
tooltip={TOOLTIPS.valuesNumber}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Value Bytes"
|
label="Value Bytes"
|
||||||
value={formatBytes(props.stats.values.bytes)}
|
value={formatBytes(props.stats.values.bytes, prec())}
|
||||||
tooltip={TOOLTIPS.valuesBytes}
|
tooltip={TOOLTIPS.valuesBytes}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Symbols"
|
label="Symbols"
|
||||||
value={formatNumber(props.stats.symbols.number)}
|
value={formatNumber(props.stats.symbols.number, prec())}
|
||||||
tooltip={TOOLTIPS.symbolsNumber}
|
tooltip={TOOLTIPS.symbolsNumber}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Symbol Bytes"
|
label="Symbol Bytes"
|
||||||
value={formatBytes(props.stats.symbols.bytes)}
|
value={formatBytes(props.stats.symbols.bytes, prec())}
|
||||||
tooltip={TOOLTIPS.symbolsBytes}
|
tooltip={TOOLTIPS.symbolsBytes}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -205,22 +210,22 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
||||||
<div class="metrics-grid small">
|
<div class="metrics-grid small">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="List Elements"
|
label="List Elements"
|
||||||
value={formatNumber(props.stats.list.elements)}
|
value={formatNumber(props.stats.list.elements, prec())}
|
||||||
tooltip={TOOLTIPS.listElements}
|
tooltip={TOOLTIPS.listElements}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Concat Ops"
|
label="Concat Ops"
|
||||||
value={formatNumber(props.stats.list.concats)}
|
value={formatNumber(props.stats.list.concats, prec())}
|
||||||
tooltip={TOOLTIPS.listConcats}
|
tooltip={TOOLTIPS.listConcats}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Set Count"
|
label="Set Count"
|
||||||
value={formatNumber(props.stats.sets.number)}
|
value={formatNumber(props.stats.sets.number, prec())}
|
||||||
tooltip={TOOLTIPS.setsNumber}
|
tooltip={TOOLTIPS.setsNumber}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Attributes"
|
label="Attributes"
|
||||||
value={formatNumber(props.stats.sets.elements)}
|
value={formatNumber(props.stats.sets.elements, prec())}
|
||||||
tooltip={TOOLTIPS.setsElements}
|
tooltip={TOOLTIPS.setsElements}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -235,7 +240,7 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
||||||
<div class="top-item">
|
<div class="top-item">
|
||||||
<span class="rank">{i() + 1}</span>
|
<span class="rank">{i() + 1}</span>
|
||||||
<span class="name">{item.name}</span>
|
<span class="name">{item.name}</span>
|
||||||
<span class="count">{formatNumber(item.count)}</span>
|
<span class="count">{formatNumber(item.count, prec())}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
@ -251,7 +256,7 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
||||||
<div class="top-item">
|
<div class="top-item">
|
||||||
<span class="rank">{i() + 1}</span>
|
<span class="rank">{i() + 1}</span>
|
||||||
<span class="name">{item.name || '<lambda>'}</span>
|
<span class="name">{item.name || '<lambda>'}</span>
|
||||||
<span class="count">{formatNumber(item.count)}</span>
|
<span class="count">{formatNumber(item.count, prec())}</span>
|
||||||
<span class="location">
|
<span class="location">
|
||||||
{item.file}:{item.line}
|
{item.file}:{item.line}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -267,22 +272,22 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
||||||
<div class="metrics-grid small">
|
<div class="metrics-grid small">
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="NAR Reads"
|
label="NAR Reads"
|
||||||
value={formatNumber(props.stats.narRead || 0)}
|
value={formatNumber(props.stats.narRead || 0, prec())}
|
||||||
tooltip={TOOLTIPS.narRead}
|
tooltip={TOOLTIPS.narRead}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="NAR Writes"
|
label="NAR Writes"
|
||||||
value={formatNumber(props.stats.narWrite || 0)}
|
value={formatNumber(props.stats.narWrite || 0, prec())}
|
||||||
tooltip={TOOLTIPS.narWrite}
|
tooltip={TOOLTIPS.narWrite}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Read Bytes"
|
label="Read Bytes"
|
||||||
value={formatBytes(props.stats.narReadBytes || 0)}
|
value={formatBytes(props.stats.narReadBytes || 0, prec())}
|
||||||
tooltip={TOOLTIPS.narReadBytes}
|
tooltip={TOOLTIPS.narReadBytes}
|
||||||
/>
|
/>
|
||||||
<MetricCard
|
<MetricCard
|
||||||
label="Write Bytes"
|
label="Write Bytes"
|
||||||
value={formatBytes(props.stats.narWriteBytes || 0)}
|
value={formatBytes(props.stats.narWriteBytes || 0, prec())}
|
||||||
tooltip={TOOLTIPS.narWriteBytes}
|
tooltip={TOOLTIPS.narWriteBytes}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -299,7 +304,7 @@ const Analysis: Component<{ stats: StatsData }> = props => {
|
||||||
<span class="location">
|
<span class="location">
|
||||||
{item.file}:{item.line}
|
{item.file}:{item.line}
|
||||||
</span>
|
</span>
|
||||||
<span class="count">{formatNumber(item.count)}</span>
|
<span class="count">{formatNumber(item.count, prec())}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,11 @@ interface ComparisonViewProps {
|
||||||
entries: ComparisonEntry[];
|
entries: ComparisonEntry[];
|
||||||
onSelect: (entry: ComparisonEntry) => void;
|
onSelect: (entry: ComparisonEntry) => void;
|
||||||
onDelete: (id: number) => void;
|
onDelete: (id: number) => void;
|
||||||
|
precision?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ComparisonView: Component<ComparisonViewProps> = props => {
|
const ComparisonView: Component<ComparisonViewProps> = props => {
|
||||||
|
const prec = () => props.precision ?? 2;
|
||||||
const [leftEntry, setLeftEntry] = createSignal<ComparisonEntry | null>(null);
|
const [leftEntry, setLeftEntry] = createSignal<ComparisonEntry | null>(null);
|
||||||
const [rightEntry, setRightEntry] = createSignal<ComparisonEntry | null>(null);
|
const [rightEntry, setRightEntry] = createSignal<ComparisonEntry | null>(null);
|
||||||
|
|
||||||
|
|
@ -203,7 +205,7 @@ const ComparisonView: Component<ComparisonViewProps> = props => {
|
||||||
<Show when={!row.isReduction}>
|
<Show when={!row.isReduction}>
|
||||||
<ArrowUpIcon size={14} />
|
<ArrowUpIcon size={14} />
|
||||||
</Show>
|
</Show>
|
||||||
{Math.abs(row.change).toFixed(2)}%
|
{Math.abs(row.change).toFixed(prec())}%
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ import { formatNumber } from '@ns/ui-utils';
|
||||||
|
|
||||||
interface OperationsChartProps {
|
interface OperationsChartProps {
|
||||||
stats: StatsData;
|
stats: StatsData;
|
||||||
|
precision?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OperationsChart: Component<OperationsChartProps> = props => {
|
const OperationsChart: Component<OperationsChartProps> = props => {
|
||||||
|
const prec = () => props.precision ?? 2;
|
||||||
const operations = createMemo(() =>
|
const operations = createMemo(() =>
|
||||||
[
|
[
|
||||||
{ label: 'Lookups', value: props.stats.nrLookups, colorClass: 'chart-1' },
|
{ label: 'Lookups', value: props.stats.nrLookups, colorClass: 'chart-1' },
|
||||||
|
|
@ -34,7 +36,7 @@ const OperationsChart: Component<OperationsChartProps> = props => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="op-value">{formatNumber(item.value)}</div>
|
<div class="op-value">{formatNumber(item.value, prec())}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ import { formatNumber } from '@ns/ui-utils';
|
||||||
|
|
||||||
interface ThunkChartProps {
|
interface ThunkChartProps {
|
||||||
stats: StatsData;
|
stats: StatsData;
|
||||||
|
precision?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThunkChart: Component<ThunkChartProps> = props => {
|
const ThunkChart: Component<ThunkChartProps> = props => {
|
||||||
|
const prec = () => props.precision ?? 2;
|
||||||
const maxValue = createMemo(() => Math.max(props.stats.nrThunks, props.stats.nrAvoided));
|
const maxValue = createMemo(() => Math.max(props.stats.nrThunks, props.stats.nrAvoided));
|
||||||
|
|
||||||
const avoidedRatio = createMemo(() => {
|
const avoidedRatio = createMemo(() => {
|
||||||
|
|
@ -47,7 +49,7 @@ const ThunkChart: Component<ThunkChartProps> = props => {
|
||||||
<div class="ratio-bar">
|
<div class="ratio-bar">
|
||||||
<div class="ratio-fill" style={{ width: `${avoidedRatio() * 100}%` }} />
|
<div class="ratio-fill" style={{ width: `${avoidedRatio() * 100}%` }} />
|
||||||
</div>
|
</div>
|
||||||
<span class="ratio-label">Avoidance rate: {(avoidedRatio() * 100).toFixed(1)}%</span>
|
<span class="ratio-label">Avoidance rate: {(avoidedRatio() * 100).toFixed(prec())}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,11 @@ import { formatBytes, formatTime, formatPercent } from '@ns/ui-utils';
|
||||||
|
|
||||||
interface TimeChartProps {
|
interface TimeChartProps {
|
||||||
stats: StatsData;
|
stats: StatsData;
|
||||||
|
precision?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimeChart: Component<TimeChartProps> = props => {
|
const TimeChart: Component<TimeChartProps> = props => {
|
||||||
|
const prec = () => props.precision ?? 2;
|
||||||
const timeData = createMemo(() => {
|
const timeData = createMemo(() => {
|
||||||
const cpu = props.stats.time.cpu;
|
const cpu = props.stats.time.cpu;
|
||||||
const gc = props.stats.time.gc || 0;
|
const gc = props.stats.time.gc || 0;
|
||||||
|
|
@ -45,7 +47,7 @@ const TimeChart: Component<TimeChartProps> = props => {
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="time-bar-value">{formatTime(item.value)}</div>
|
<div class="time-bar-value">{formatTime(item.value, prec())}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
|
@ -59,11 +61,13 @@ const TimeChart: Component<TimeChartProps> = props => {
|
||||||
</div>
|
</div>
|
||||||
<div class="gc-stat">
|
<div class="gc-stat">
|
||||||
<span class="gc-stat-label">Heap Size</span>
|
<span class="gc-stat-label">Heap Size</span>
|
||||||
<span class="gc-stat-value">{formatBytes(props.stats.gc?.heapSize || 0)}</span>
|
<span class="gc-stat-value">{formatBytes(props.stats.gc?.heapSize || 0, prec())}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gc-stat">
|
<div class="gc-stat">
|
||||||
<span class="gc-stat-label">GC Fraction</span>
|
<span class="gc-stat-label">GC Fraction</span>
|
||||||
<span class="gc-stat-value">{formatPercent(props.stats.time.gcFraction || 0)}</span>
|
<span class="gc-stat-value">
|
||||||
|
{formatPercent(props.stats.time.gcFraction || 0, prec())}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ function App() {
|
||||||
const [showSaveDialog, setShowSaveDialog] = createSignal(false);
|
const [showSaveDialog, setShowSaveDialog] = createSignal(false);
|
||||||
const [showHelp, setShowHelp] = createSignal(false);
|
const [showHelp, setShowHelp] = createSignal(false);
|
||||||
const [showManageSnapshots, setShowManageSnapshots] = createSignal(false);
|
const [showManageSnapshots, setShowManageSnapshots] = createSignal(false);
|
||||||
|
const [precision, setPrecision] = createSignal(2);
|
||||||
const [isLoading, setIsLoading] = createSignal(true);
|
const [isLoading, setIsLoading] = createSignal(true);
|
||||||
|
|
||||||
const STORAGE_KEY = 'ns-data';
|
const STORAGE_KEY = 'ns-data';
|
||||||
|
|
@ -58,6 +59,9 @@ function App() {
|
||||||
if (parsed.view) {
|
if (parsed.view) {
|
||||||
setView(parsed.view);
|
setView(parsed.view);
|
||||||
}
|
}
|
||||||
|
if (typeof parsed.precision === 'number' && parsed.precision >= 0) {
|
||||||
|
setPrecision(parsed.precision);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to load saved data:', e);
|
console.warn('Failed to load saved data:', e);
|
||||||
|
|
@ -84,6 +88,7 @@ function App() {
|
||||||
raw: Record<string, unknown> | null,
|
raw: Record<string, unknown> | null,
|
||||||
snaps: ComparisonEntry[],
|
snaps: ComparisonEntry[],
|
||||||
v: 'analysis' | 'compare',
|
v: 'analysis' | 'compare',
|
||||||
|
prec: number,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
|
|
@ -93,6 +98,7 @@ function App() {
|
||||||
currentStats: stats,
|
currentStats: stats,
|
||||||
currentRaw: raw,
|
currentRaw: raw,
|
||||||
view: v,
|
view: v,
|
||||||
|
precision: prec,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -108,10 +114,11 @@ function App() {
|
||||||
const raw = currentRaw();
|
const raw = currentRaw();
|
||||||
const snaps = snapshots();
|
const snaps = snapshots();
|
||||||
const v = view();
|
const v = view();
|
||||||
|
const prec = precision();
|
||||||
|
|
||||||
if (isLoading()) return;
|
if (isLoading()) return;
|
||||||
|
|
||||||
saveToStorage(stats, raw, snaps, v);
|
saveToStorage(stats, raw, snaps, v, prec);
|
||||||
});
|
});
|
||||||
|
|
||||||
const saveSnapshot = () => {
|
const saveSnapshot = () => {
|
||||||
|
|
@ -190,6 +197,16 @@ function App() {
|
||||||
<Trash2Icon size={16} />
|
<Trash2Icon size={16} />
|
||||||
</button>
|
</button>
|
||||||
</Show>
|
</Show>
|
||||||
|
<select
|
||||||
|
class="precision-select"
|
||||||
|
value={precision()}
|
||||||
|
onChange={e => setPrecision(parseInt(e.currentTarget.value, 10))}
|
||||||
|
title="Decimal Precision"
|
||||||
|
>
|
||||||
|
<For each={[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}>
|
||||||
|
{p => <option value={p}>{p} dp</option>}
|
||||||
|
</For>
|
||||||
|
</select>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -213,7 +230,7 @@ function App() {
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={currentStats()}>
|
<Show when={currentStats()}>
|
||||||
<Analysis stats={currentStats()!} />
|
<Analysis stats={currentStats()!} precision={precision()} />
|
||||||
<Show when={showSaveDialog()}>
|
<Show when={showSaveDialog()}>
|
||||||
<div class="modal-overlay" onClick={() => setShowSaveDialog(false)}>
|
<div class="modal-overlay" onClick={() => setShowSaveDialog(false)}>
|
||||||
<div class="modal" onClick={e => e.stopPropagation()}>
|
<div class="modal" onClick={e => e.stopPropagation()}>
|
||||||
|
|
@ -246,6 +263,7 @@ function App() {
|
||||||
entries={snapshots()}
|
entries={snapshots()}
|
||||||
onSelect={entry => setCurrentStats(entry.data)}
|
onSelect={entry => setCurrentStats(entry.data)}
|
||||||
onDelete={deleteSnapshot}
|
onDelete={deleteSnapshot}
|
||||||
|
precision={precision()}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
||||||
|
|
@ -1628,3 +1628,65 @@ body {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.precision-select {
|
||||||
|
padding: 0.5rem 1.5rem 0.5rem 0.75rem;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 0.5rem center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.precision-select:hover {
|
||||||
|
border-color: var(--text-muted);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.precision-select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-label {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.precision-input {
|
||||||
|
width: 120px;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.precision-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide number input spinners */
|
||||||
|
.precision-input::-webkit-outer-spin-button,
|
||||||
|
.precision-input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.precision-input[type='number'] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-hint {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue