diff --git a/README.md b/README.md index 2713497..d4ac411 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ # Maybe Spreadsheet -Maybe is a React-based spreadsheet application with built-in support for uncertainty through Gaussian distributions and sampled distributions. +Maybe is a React-based spreadsheet application with built-in support for uncertainty through explicit probability distributions and sampled distributions. ## Features - Full spreadsheet functionality powered by GaussFormula - Support for formulas and calculations -- Built-in support for Gaussian distributions +- Built-in support for explicit distributions such as `N(mean, variance)`, `LN(mu, sigma)`, `U(min, max)`, `N.CI(lower, upper, confidence)`, and `LN.CI(lower, upper, confidence)` - Interactive visualizations for distributions - Statistical analysis tools including: - Percentile calculations @@ -46,12 +46,20 @@ pnpm dev ### Working with Distributions -The spreadsheet supports Gaussian distributions that can be created and manipulated using the GaussFormula engine. +The spreadsheet supports explicit probability distributions that can be created and manipulated using the GaussFormula engine. - Use the spreadsheet interface to input data and formulas - Visualize distributions with the interactive plotting tool - Analyze statistical properties of distributions +### Distribution notation + +- `N(mean, variance)` uses normal-distribution parameters directly. +- `LN(mu, sigma)` uses log-space parameters: if `X ~ LN(mu, sigma)`, then `ln(X) ~ N(mu, sigma^2)`. `mu` and `sigma` are not the mean and standard deviation of `X`. +- Prefer `LN.CI(lower, upper, confidence)` for user-facing lognormal inputs because the bounds are in the original value scale. +- `N.CI(lower, upper, confidence)` derives a normal distribution from a value-scale confidence interval. +- `U(min, max)` represents a uniform range on the original value scale. + ## Technical Details This project is built with: diff --git a/package.json b/package.json index 14ae5c6..3e71fec 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "ai": "^6.0.168", "echarts": "^6.0.0", "echarts-for-react": "^3.0.6", - "gaussformula": "^0.1.6", + "gaussformula": "file:../gaussformula/gaussformula-0.1.6.tgz", "react": "^19.2.5", "react-dom": "^19.2.5", "zod": "^4.3.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7acc080..c40a7b9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ importers: specifier: ^3.0.6 version: 3.0.6(echarts@6.0.0)(react@19.2.5) gaussformula: - specifier: ^0.1.6 - version: 0.1.6 + specifier: file:../gaussformula/gaussformula-0.1.6.tgz + version: file:../gaussformula/gaussformula-0.1.6.tgz react: specifier: ^19.2.5 version: 19.2.5 @@ -1002,8 +1002,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - gaussformula@0.1.6: - resolution: {integrity: sha512-cVx9S4UFr0w9z7NAlm5PkUH+aRMNH35hucVzg/NXkIh20lSVIAunPVbpDNLAr9BquYISfm+IRAw6YBC+adTh+A==} + gaussformula@file:../gaussformula/gaussformula-0.1.6.tgz: + resolution: {integrity: sha512-Py/KrGcDzMUj+Kn5TzlTJ0gIV8zcF3XwfPbi7f5jG5lUdtRZkkDdJDHnj+yO1Hai7wpQVPRSmLyUFKlMzIscgw==, tarball: file:../gaussformula/gaussformula-0.1.6.tgz} + version: 0.1.6 gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} @@ -2362,7 +2363,7 @@ snapshots: fsevents@2.3.3: optional: true - gaussformula@0.1.6: + gaussformula@file:../gaussformula/gaussformula-0.1.6.tgz: dependencies: chevrotain: 6.5.0 tiny-emitter: 2.1.0 diff --git a/src/app/analysisState.ts b/src/app/analysisState.ts index 12a19ad..5d776cc 100644 --- a/src/app/analysisState.ts +++ b/src/app/analysisState.ts @@ -1,11 +1,9 @@ import { getSamplesFromDistribution, - isConfidenceIntervalValue, + isDistributionValue, isSampledDistributionValue, - parseConfidenceIntervalValue, - type ParsedConfidenceInterval, + parseDistributionValue, } from "../utils/distribution/distributionUtils"; -import type { DistributionViewType } from "../context/SpreadsheetContext"; type DistributionType = "normal" | "lognormal" | "uniform" | "sampled"; @@ -18,7 +16,6 @@ interface AnalysisState { cellLabel: string; samples: number[] | null; distributionType: DistributionType; - confidenceInterval?: ParsedConfidenceInterval | null; message?: string; } @@ -30,10 +27,6 @@ interface DeriveAnalysisStateInput { analysisMode: boolean; formulaEngine: FormulaValueReader | null; selectedCell: SelectedCell | null; - getDistributionView: ( - row: number, - col: number, - ) => DistributionViewType | null; } export const getColumnName = (index: number): string => { @@ -50,7 +43,6 @@ export const deriveAnalysisState = ({ analysisMode, formulaEngine, selectedCell, - getDistributionView, }: DeriveAnalysisStateInput): AnalysisState | null => { if (!analysisMode || !formulaEngine || !selectedCell) { return null; @@ -64,31 +56,26 @@ export const deriveAnalysisState = ({ }); const hasDistribution = - isConfidenceIntervalValue(value) || isSampledDistributionValue(value); + isDistributionValue(value) || + isSampledDistributionValue(value); if (!hasDistribution) { return { cellLabel, samples: null, distributionType: "normal", - confidenceInterval: null, message: - "Select a cell containing uncertainty (CI[lower, upper] inputs or simulated outputs) to inspect its distribution.", + "Select a cell containing uncertainty (N, LN, U, N.CI, LN.CI, or simulated outputs) to inspect its distribution.", }; } - const confidenceInterval = parseConfidenceIntervalValue(value); + const distribution = parseDistributionValue(value); const samples = getSamplesFromDistribution(value); - const viewOverride = getDistributionView(selectedCell.row, selectedCell.col); let distributionType: DistributionType = "normal"; - if (viewOverride) { - distributionType = viewOverride; - } else if (confidenceInterval?.interpretation === "lognormal") { - distributionType = "lognormal"; - } else if (confidenceInterval?.interpretation === "uniform") { - distributionType = "uniform"; - } else if (!confidenceInterval && isSampledDistributionValue(value)) { + if (distribution) { + distributionType = distribution.kind; + } else if (isSampledDistributionValue(value)) { distributionType = "sampled"; } @@ -97,9 +84,8 @@ export const deriveAnalysisState = ({ cellLabel, samples: null, distributionType, - confidenceInterval, message: - "Unable to generate samples for this cell. Ensure the value exposes samples or a confidence interval.", + "Unable to generate samples for this cell. Ensure the value exposes samples or an explicit distribution.", }; } @@ -107,6 +93,5 @@ export const deriveAnalysisState = ({ cellLabel, samples, distributionType, - confidenceInterval, }; }; diff --git a/src/assistant/formulaValidation.ts b/src/assistant/formulaValidation.ts index f822494..7d8e19f 100644 --- a/src/assistant/formulaValidation.ts +++ b/src/assistant/formulaValidation.ts @@ -3,9 +3,148 @@ import { cellAddressFromA1 } from "./address"; import { type ValidateFormulaInput } from "./toolSchemas"; import { buildWorkbookSnapshot, + type WorkbookCellSnapshot, + type WorkbookDistributionSummary, type WorkbookSnapshot, } from "./workbookSnapshot"; +interface DistributionConstructor { + normal?: ( + mean: number, + variance: number, + options?: Record, + ) => unknown; + lognormal?: ( + mu: number, + sigma: number, + options?: Record, + ) => unknown; + uniform?: ( + min: number, + max: number, + options?: Record, + ) => unknown; + normalFromCI?: ( + lower: number, + upper: number, + confidenceLevel: number, + options?: Record, + ) => unknown; + lognormalFromCI?: ( + lower: number, + upper: number, + confidenceLevel: number, + options?: Record, + ) => unknown; +} + +const getDistributionConstructor = (): DistributionConstructor | undefined => + (GaussFormulaEngine as unknown as { DistributionNumber?: DistributionConstructor }) + .DistributionNumber; + +const representativeDistributionValue = ( + distribution: WorkbookDistributionSummary, +): number | string => { + switch (distribution.kind) { + case "normal": + return distribution.mean ?? + (distribution.lower !== undefined && distribution.upper !== undefined + ? (distribution.lower + distribution.upper) / 2 + : "0"); + case "lognormal": + if (distribution.mu !== undefined && distribution.sigma !== undefined) { + return Math.exp(distribution.mu + (distribution.sigma ** 2) / 2); + } + if (distribution.lower !== undefined && distribution.upper !== undefined) { + return Math.sqrt(distribution.lower * distribution.upper); + } + return "0"; + case "uniform": + return distribution.min !== undefined && distribution.max !== undefined + ? (distribution.min + distribution.max) / 2 + : "0"; + } +}; + +const reconstructDistributionValue = ( + distribution: WorkbookDistributionSummary, +): unknown => { + const ctor = getDistributionConstructor(); + + if (ctor) { + if ( + distribution.kind === "normal" && + distribution.source === "ci" && + distribution.lower !== undefined && + distribution.upper !== undefined && + distribution.confidenceLevel !== undefined && + ctor.normalFromCI + ) { + return ctor.normalFromCI( + distribution.lower, + distribution.upper, + distribution.confidenceLevel, + ); + } + + if ( + distribution.kind === "lognormal" && + distribution.source === "ci" && + distribution.lower !== undefined && + distribution.upper !== undefined && + distribution.confidenceLevel !== undefined && + ctor.lognormalFromCI + ) { + return ctor.lognormalFromCI( + distribution.lower, + distribution.upper, + distribution.confidenceLevel, + ); + } + + if ( + distribution.kind === "normal" && + distribution.mean !== undefined && + distribution.variance !== undefined && + ctor.normal + ) { + return ctor.normal(distribution.mean, distribution.variance); + } + + if ( + distribution.kind === "lognormal" && + distribution.mu !== undefined && + distribution.sigma !== undefined && + ctor.lognormal + ) { + return ctor.lognormal(distribution.mu, distribution.sigma); + } + + if ( + distribution.kind === "uniform" && + distribution.min !== undefined && + distribution.max !== undefined && + ctor.uniform + ) { + return ctor.uniform(distribution.min, distribution.max); + } + } + + return representativeDistributionValue(distribution); +}; + +const reconstructCellValue = (cell: WorkbookCellSnapshot): unknown => { + if (cell.formula) { + return cell.formula; + } + + if (cell.distribution) { + return reconstructDistributionValue(cell.distribution); + } + + return cell.displayValue; +}; + const createFormulaEngineFromSnapshot = (snapshot: WorkbookSnapshot) => { const formulaEngine = GaussFormulaEngine.buildEmpty({ licenseKey: "gpl-v3", @@ -18,7 +157,7 @@ const createFormulaEngineFromSnapshot = (snapshot: WorkbookSnapshot) => { const rows = Math.max(1, snapshot.dimensions.height); const cols = Math.max(1, snapshot.dimensions.width); - const sheetData: Array> = Array.from( + const sheetData: unknown[][] = Array.from( { length: rows }, () => Array.from({ length: cols }, () => null), ); @@ -28,7 +167,7 @@ const createFormulaEngineFromSnapshot = (snapshot: WorkbookSnapshot) => { continue; } - sheetData[cell.row][cell.col] = cell.formula ?? cell.displayValue; + sheetData[cell.row][cell.col] = reconstructCellValue(cell); } formulaEngine.setSheetContent(0, sheetData); diff --git a/src/assistant/messages.ts b/src/assistant/messages.ts index 84649d6..399cf6f 100644 --- a/src/assistant/messages.ts +++ b/src/assistant/messages.ts @@ -38,17 +38,24 @@ const workbookCellKindSchema = z.enum([ "scalar", "text", "boolean", - "confidence_interval", + "distribution", "sampled_distribution", "error", "unknown_object", ]); -const workbookConfidenceIntervalSchema = z.object({ - lower: z.number(), - upper: z.number(), - confidenceLevel: z.number(), - interpretation: z.string(), +const workbookDistributionSchema = z.object({ + kind: z.enum(["normal", "lognormal", "uniform"]), + source: z.enum(["parameters", "ci"]), + mean: z.number().optional(), + variance: z.number().optional(), + mu: z.number().optional(), + sigma: z.number().optional(), + min: z.number().optional(), + max: z.number().optional(), + lower: z.number().optional(), + upper: z.number().optional(), + confidenceLevel: z.number().optional(), }); const workbookSampleSummarySchema = z.object({ @@ -67,7 +74,7 @@ const workbookCellSnapshotSchema = z.object({ formula: z.string().nullable(), displayValue: z.string(), kind: workbookCellKindSchema, - confidenceInterval: workbookConfidenceIntervalSchema.optional(), + distribution: workbookDistributionSchema.optional(), sampleSummary: workbookSampleSummarySchema.optional(), }); diff --git a/src/assistant/workbookSnapshot.ts b/src/assistant/workbookSnapshot.ts index 802d3de..7c5083d 100644 --- a/src/assistant/workbookSnapshot.ts +++ b/src/assistant/workbookSnapshot.ts @@ -1,8 +1,9 @@ import type { FormulaEngine } from "../lib/formulaEngine"; import { + confidenceLevelToFormulaArgument, getSamplesFromDistribution, isSampledDistributionValue, - parseConfidenceIntervalValue, + parseDistributionValue, parseSampledDistributionValue, } from "../utils/distribution/distributionUtils"; import { percentileFromSorted } from "../utils/distribution/distributionMath"; @@ -17,16 +18,23 @@ export type WorkbookCellKind = | "scalar" | "text" | "boolean" - | "confidence_interval" + | "distribution" | "sampled_distribution" | "error" | "unknown_object"; -export interface WorkbookConfidenceIntervalSummary { - lower: number; - upper: number; - confidenceLevel: number; - interpretation: string; +export interface WorkbookDistributionSummary { + kind: "normal" | "lognormal" | "uniform"; + source: "parameters" | "ci"; + mean?: number; + variance?: number; + mu?: number; + sigma?: number; + min?: number; + max?: number; + lower?: number; + upper?: number; + confidenceLevel?: number; } export interface WorkbookSampleSummary { @@ -45,7 +53,7 @@ export interface WorkbookCellSnapshot { formula: string | null; displayValue: string; kind: WorkbookCellKind; - confidenceInterval?: WorkbookConfidenceIntervalSummary; + distribution?: WorkbookDistributionSummary; sampleSummary?: WorkbookSampleSummary; } @@ -109,15 +117,19 @@ const summarizeSamples = (value: unknown): WorkbookSampleSummary | undefined => const sorted = [...samples].sort((a, b) => a - b); const mean = + parsed?.mean ?? samples.reduce((sum, sample) => sum + sample, 0) / samples.length; - const variance = - samples.reduce((sum, sample) => sum + Math.pow(sample - mean, 2), 0) / - samples.length; + const standardDeviation = + parsed?.std ?? + Math.sqrt( + samples.reduce((sum, sample) => sum + Math.pow(sample - mean, 2), 0) / + samples.length, + ); return { sampleCount: samples.length, mean: round6(mean), - standardDeviation: round6(Math.sqrt(variance)), + standardDeviation: round6(standardDeviation), p10: round6(percentileFromSorted(sorted, 10)), p50: round6(percentileFromSorted(sorted, 50)), p90: round6(percentileFromSorted(sorted, 90)), @@ -126,11 +138,11 @@ const summarizeSamples = (value: unknown): WorkbookSampleSummary | undefined => const getCellKind = ( value: unknown, - confidenceInterval: ReturnType, + distribution: ReturnType, sampleSummary: WorkbookSampleSummary | undefined, ): WorkbookCellKind => { - if (confidenceInterval) { - return "confidence_interval"; + if (distribution) { + return "distribution"; } if (isSampledDistributionValue(value) || sampleSummary) { return "sampled_distribution"; @@ -161,7 +173,7 @@ const getCellKind = ( const serializeCellValue = ( value: unknown, - confidenceInterval: ReturnType, + distribution: ReturnType, sampleSummary: WorkbookSampleSummary | undefined, ): string => { if (value === null || value === undefined) { @@ -173,8 +185,19 @@ const serializeCellValue = ( if (typeof value === "number" || typeof value === "boolean") { return String(value); } - if (confidenceInterval) { - return `CI[${confidenceInterval.lower}, ${confidenceInterval.upper}]`; + if (distribution) { + switch (distribution.kind) { + case "normal": + return distribution.source === "ci" + ? `N.CI(${distribution.lower}, ${distribution.upper}, ${confidenceLevelToFormulaArgument(distribution.confidenceLevel ?? 95)})` + : `N(${distribution.mean}, ${distribution.variance})`; + case "lognormal": + return distribution.source === "ci" + ? `LN.CI(${distribution.lower}, ${distribution.upper}, ${confidenceLevelToFormulaArgument(distribution.confidenceLevel ?? 95)})` + : `LN(${distribution.mu}, ${distribution.sigma})`; + case "uniform": + return `U(${distribution.min}, ${distribution.max})`; + } } if (sampleSummary) { return `samples(mean=${sampleSummary.mean}, p10=${sampleSummary.p10}, p90=${sampleSummary.p90})`; @@ -219,7 +242,7 @@ export const buildWorkbookSnapshot = ({ continue; } - const confidenceInterval = parseConfidenceIntervalValue(value); + const distribution = parseDistributionValue(value); const sampleSummary = summarizeSamples(value); cells.push({ @@ -227,14 +250,21 @@ export const buildWorkbookSnapshot = ({ row, col, formula, - displayValue: serializeCellValue(value, confidenceInterval, sampleSummary), - kind: getCellKind(value, confidenceInterval, sampleSummary), - confidenceInterval: confidenceInterval + displayValue: serializeCellValue(value, distribution, sampleSummary), + kind: getCellKind(value, distribution, sampleSummary), + distribution: distribution ? { - lower: confidenceInterval.lower, - upper: confidenceInterval.upper, - confidenceLevel: confidenceInterval.confidenceLevel, - interpretation: confidenceInterval.interpretation ?? "normal", + kind: distribution.kind, + source: distribution.source, + mean: distribution.mean, + variance: distribution.variance, + mu: distribution.mu, + sigma: distribution.sigma, + min: distribution.min, + max: distribution.max, + lower: distribution.lower, + upper: distribution.upper, + confidenceLevel: distribution.confidenceLevel, } : undefined, sampleSummary, diff --git a/src/components/charts/DistributionPlot.tsx b/src/components/charts/DistributionPlot.tsx index 7ddd140..af3e705 100644 --- a/src/components/charts/DistributionPlot.tsx +++ b/src/components/charts/DistributionPlot.tsx @@ -4,6 +4,7 @@ import ReactEChartsCore, { echarts } from "../../lib/echarts"; import { createDistributionChartData, createDistributionChartModelFromData, + type DistributionParams, type DistributionKind, type DistributionQueryState, } from "./distributionChartOptions"; @@ -15,6 +16,7 @@ interface DistributionPlotProps { position?: { top: number; left: number }; inline?: boolean; distributionType?: DistributionKind; + distributionParams?: DistributionParams | null; confidenceInterval?: { lower: number; upper: number; @@ -101,6 +103,7 @@ export const DistributionPlot: React.FC = ({ position, inline = false, distributionType, + distributionParams, confidenceInterval, }) => { const confidenceLower = confidenceInterval?.lower; @@ -220,9 +223,10 @@ export const DistributionPlot: React.FC = ({ createDistributionChartData({ samples, distributionKind, + distributionParams, confidenceInterval: normalizedConfidenceInterval, }), - [samples, distributionKind, normalizedConfidenceInterval], + [samples, distributionKind, distributionParams, normalizedConfidenceInterval], ); const chartModel = useMemo( diff --git a/src/components/charts/distributionChartOptions.ts b/src/components/charts/distributionChartOptions.ts index ac4e603..95d108f 100644 --- a/src/components/charts/distributionChartOptions.ts +++ b/src/components/charts/distributionChartOptions.ts @@ -1,4 +1,3 @@ -import { zScoreForConfidence } from "../../utils/distribution/distributionUtils"; import { formatNumber } from "../../utils/formatting/formatUtils"; import { createLogNormalData, @@ -57,6 +56,7 @@ export interface DistributionChartDataInput { samples: number[]; distributionKind: DistributionKind; confidenceInterval?: ChartConfidenceInterval; + distributionParams?: DistributionParams | null; } export interface DistributionChartData { @@ -112,11 +112,16 @@ const getDistributionTitle = (distributionKind: DistributionKind): string => { export const createDistributionChartData = ( input: DistributionChartDataInput, ): DistributionChartData => { - const { samples, distributionKind, confidenceInterval } = input; + const { + samples, + distributionKind, + confidenceInterval, + distributionParams: providedDistributionParams, + } = input; const sortedSamples = [...samples].sort((a, b) => a - b); - const distributionParams = (() => { + const distributionParams = providedDistributionParams ?? (() => { switch (distributionKind) { case "uniform": { if (confidenceInterval) { @@ -131,33 +136,10 @@ export const createDistributionChartData = ( return params ? { type: "uniform" as const, ...params } : null; } case "lognormal": { - if ( - confidenceInterval && - confidenceInterval.lower > 0 && - confidenceInterval.upper > 0 - ) { - const z = zScoreForConfidence(confidenceInterval.confidenceLevel); - const lnLower = Math.log(confidenceInterval.lower); - const lnUpper = Math.log(confidenceInterval.upper); - const mu = (lnLower + lnUpper) / 2; - const sigma = (lnUpper - lnLower) / (2 * z); - if (Number.isFinite(mu) && Number.isFinite(sigma) && sigma > 0) { - return { type: "lognormal" as const, mu, sigma }; - } - } - const params = toLogNormalParams(samples); return params ? { type: "lognormal" as const, ...params } : null; } case "normal": { - if (confidenceInterval) { - const z = zScoreForConfidence(confidenceInterval.confidenceLevel); - const mean = (confidenceInterval.lower + confidenceInterval.upper) / 2; - const std = - (confidenceInterval.upper - confidenceInterval.lower) / (2 * z); - return { type: "normal" as const, mean, std }; - } - const { mean, std } = toMeanAndStd(samples); return std > 0 ? { type: "normal" as const, mean, std } : null; } diff --git a/src/components/flow-diagram/FlowDiag.tsx b/src/components/flow-diagram/FlowDiag.tsx index 2fa2790..69a965d 100644 --- a/src/components/flow-diagram/FlowDiag.tsx +++ b/src/components/flow-diagram/FlowDiag.tsx @@ -14,9 +14,8 @@ import type { VisualDependencyNodeValue, } from "../../types/visualDependencyGraph"; import { - getAvailableDistributionViews, - isConfidenceIntervalValue, isSampledDistributionValue, + parseDistributionValue, parseSampledDistributionValue, } from "../../utils/distribution/distributionUtils"; import { @@ -53,9 +52,12 @@ interface FlowDiagramNodeData extends Record { formula: string; row: number; col: number; - valueKind: "scalar" | "confidence_interval" | "sampled_distribution"; - interpretation?: "normal" | "uniform" | "lognormal" | "auto"; - confidenceIntervalBounds?: { lower: number; upper: number }; + valueKind: + | "scalar" + | "distribution" + | "sampled_distribution"; + distributionKind?: "normal" | "uniform" | "lognormal"; + distributionSource?: "parameters" | "ci"; isSelected: boolean; focusState: GraphFocusState; onSelect: () => void; @@ -81,9 +83,12 @@ interface PlainNodeData extends Record { formula: string; row: number; col: number; - valueKind: "scalar" | "confidence_interval" | "sampled_distribution"; - interpretation?: "normal" | "uniform" | "lognormal" | "auto"; - confidenceIntervalBounds?: { lower: number; upper: number }; + valueKind: + | "scalar" + | "distribution" + | "sampled_distribution"; + distributionKind?: "normal" | "uniform" | "lognormal"; + distributionSource?: "parameters" | "ci"; } interface PlainNode extends LayoutNode { @@ -112,12 +117,9 @@ function buildPlainElements(graph: VisualDependencyGraph): { col: node.col, valueKind: node.value.kind, }; - if (node.value.kind === "confidence_interval") { - data.interpretation = node.value.interpretation; - data.confidenceIntervalBounds = { - lower: node.value.lower, - upper: node.value.upper, - }; + if (node.value.kind === "distribution") { + data.distributionKind = node.value.distribution; + data.distributionSource = node.value.source; } return { id: node.id, @@ -152,21 +154,21 @@ function cellValueToNodeValue(raw: unknown): VisualDependencyNodeValue | null { } if (typeof raw === "object" && raw !== null) { - if (isConfidenceIntervalValue(raw)) { - const obj = raw as { - lower: number; - upper: number; - confidenceLevel: number; - interpretation?: string; - }; + const distribution = parseDistributionValue(raw); + if (distribution) { return { - kind: "confidence_interval", - lower: obj.lower, - upper: obj.upper, - confidenceLevel: obj.confidenceLevel, - interpretation: - (obj.interpretation as "normal" | "lognormal" | "uniform" | "auto") ?? - "normal", + kind: "distribution", + distribution: distribution.kind, + source: distribution.source, + mean: distribution.mean, + variance: distribution.variance, + mu: distribution.mu, + sigma: distribution.sigma, + min: distribution.min, + max: distribution.max, + lower: distribution.lower, + upper: distribution.upper, + confidenceLevel: distribution.confidenceLevel, }; } @@ -233,13 +235,10 @@ function collectOrphanNodes( valueKind: nodeValue.kind, }; - if (nodeValue.kind === "confidence_interval") { - data.interpretation = nodeValue.interpretation; - data.confidenceIntervalBounds = { - lower: nodeValue.lower, - upper: nodeValue.upper, - }; - } + if (nodeValue.kind === "distribution") { + data.distributionKind = nodeValue.distribution; + data.distributionSource = nodeValue.source; + } orphans.push({ id: nodeId, @@ -341,8 +340,8 @@ function getValueKindLabel( valueKind: FlowDiagramNodeData["valueKind"], ): string { switch (valueKind) { - case "confidence_interval": - return "confidence interval"; + case "distribution": + return "distribution"; case "sampled_distribution": return "sampled distribution"; case "scalar": @@ -392,18 +391,17 @@ function getSelectedNodeInspectorDetails( // ── CSS class helpers ─────────────────────────────────────────────────────── function getDistributionClass(data: FlowDiagramNodeData): string { - if (data.valueKind === "confidence_interval" && data.interpretation) { - switch (data.interpretation) { + if (data.valueKind === "distribution" && data.distributionKind) { + switch (data.distributionKind) { case "normal": return styles.nodeDistNormal; case "lognormal": return styles.nodeDistLognormal; case "uniform": return styles.nodeDistUniform; - case "auto": - return styles.nodeDistNormal; } } + if (data.valueKind === "sampled_distribution") return styles.nodeDistSampled; return ""; } @@ -426,11 +424,9 @@ function getSparklineProps(data: FlowDiagramNodeData): { type: "normal" | "lognormal" | "uniform" | "sampled"; color: string; } | null { - if (data.valueKind === "confidence_interval") { - const interp = data.interpretation ?? "normal"; - switch (interp) { + if (data.valueKind === "distribution" && data.distributionKind) { + switch (data.distributionKind) { case "normal": - case "auto": return { type: "normal", color: "var(--dist-normal-accent)" }; case "lognormal": return { type: "lognormal", color: "var(--dist-lognormal-accent)" }; @@ -438,6 +434,7 @@ function getSparklineProps(data: FlowDiagramNodeData): { return { type: "uniform", color: "var(--dist-uniform-accent)" }; } } + if (data.valueKind === "sampled_distribution") return { type: "sampled", color: "var(--dist-sampled-accent)" }; return null; @@ -621,11 +618,6 @@ const SelectedNodeInspector: React.FC<{ }> = ({ details }) => { const { node, role, directInputs, directOutputs } = details; const { data } = node; - const { setDistributionView } = useContext(SpreadsheetContext); - const distributionOptions = - data.valueKind === "confidence_interval" && data.confidenceIntervalBounds - ? getAvailableDistributionViews(data.confidenceIntervalBounds) - : []; return (