diff --git a/packages/components/src/components/DistributionChart.tsx b/packages/components/src/components/DistributionChart.tsx index 1e1c3822..70ea697d 100644 --- a/packages/components/src/components/DistributionChart.tsx +++ b/packages/components/src/components/DistributionChart.tsx @@ -16,27 +16,43 @@ import { import { NumberShower } from "./NumberShower"; import { hasMassBelowZero } from "../lib/distributionUtils"; -export type DistributionPlottingSettings = { +export type PlotSettings = { /** Whether to show a summary of means, stdev, percentiles etc */ showSummary: boolean; - actions?: boolean; + /** Whether to show vega actions to the user, so they can copy the chart spec */ + actions: boolean; } & DistributionChartSpecOptions; +export const plotSettingsFromPartial = ( + partial: Partial +): PlotSettings => { + return { + showSummary: false, + logX: false, + expY: false, + color: "#739ECC", + tickFormat: ".9~s", + title: "", + actions: false, + ...partial, + }; +}; + export type DistributionChartProps = { distribution: Distribution; width?: number; height: number; -} & DistributionPlottingSettings; + settings: PlotSettings; +}; export const DistributionChart: React.FC = (props) => { const { distribution, height, - showSummary, width, - logX, - actions = false, + settings: { showSummary, logX, actions }, } = props; + const shape = distribution.pointSet(); const [sized] = useSize((size) => { if (shape.tag === "Error") { @@ -47,7 +63,7 @@ export const DistributionChart: React.FC = (props) => { ); } - const spec = buildVegaSpec(props); + const spec = buildVegaSpec(props.settings); let widthProp = width ? width : size.width; if (widthProp < 20) { @@ -131,15 +147,15 @@ const SummaryTable: React.FC = ({ distribution }) => { - {"Mean"} - {hasResult(stdev) && {"Stdev"}} - {"5%"} - {"10%"} - {"25%"} - {"50%"} - {"75%"} - {"90%"} - {"95%"} + Mean + {hasResult(stdev) && Stdev} + 5% + 10% + 25% + 50% + 75% + 90% + 95% diff --git a/packages/components/src/components/FunctionChart.tsx b/packages/components/src/components/FunctionChart.tsx index 73378cd8..2b4da8ed 100644 --- a/packages/components/src/components/FunctionChart.tsx +++ b/packages/components/src/components/FunctionChart.tsx @@ -7,28 +7,42 @@ import { } from "@quri/squiggle-lang"; import { FunctionChart1Dist } from "./FunctionChart1Dist"; import { FunctionChart1Number } from "./FunctionChart1Number"; -import { DistributionPlottingSettings } from "./DistributionChart"; +import { PlotSettings } from "./DistributionChart"; import { ErrorAlert, MessageAlert } from "./Alert"; -export type FunctionChartSettings = { +export type FunctionSettings = { + /** Where the function domain starts */ start: number; + /** Where the function domain ends */ stop: number; + /** The amount of stops sampled */ count: number; }; interface FunctionChartProps { fn: lambdaValue; - chartSettings: FunctionChartSettings; - distributionPlotSettings: DistributionPlottingSettings; + functionSettings: FunctionSettings; + plotSettings: PlotSettings; environment: environment; height: number; } +export const functionSettingsFromPartial = ( + partial: Partial +): FunctionSettings => { + return { + start: 0, + stop: 10, + count: 20, + ...partial, + }; +}; + export const FunctionChart: React.FC = ({ fn, - chartSettings, + functionSettings, environment, - distributionPlotSettings, + plotSettings, height, }) => { if (fn.parameters.length > 1) { @@ -38,8 +52,8 @@ export const FunctionChart: React.FC = ({ ); } - const result1 = runForeign(fn, [chartSettings.start], environment); - const result2 = runForeign(fn, [chartSettings.stop], environment); + const result1 = runForeign(fn, [functionSettings.start], environment); + const result2 = runForeign(fn, [functionSettings.stop], environment); const getValidResult = () => { if (result1.tag === "Ok") { return result1; @@ -64,17 +78,17 @@ export const FunctionChart: React.FC = ({ return ( ); case "number": return ( diff --git a/packages/components/src/components/FunctionChart1Dist.tsx b/packages/components/src/components/FunctionChart1Dist.tsx index f8d072d7..e9db4ee8 100644 --- a/packages/components/src/components/FunctionChart1Dist.tsx +++ b/packages/components/src/components/FunctionChart1Dist.tsx @@ -13,12 +13,10 @@ import { } from "@quri/squiggle-lang"; import { createClassFromSpec } from "react-vega"; import * as percentilesSpec from "../vega-specs/spec-percentiles.json"; -import { - DistributionChart, - DistributionPlottingSettings, -} from "./DistributionChart"; +import { DistributionChart, PlotSettings } from "./DistributionChart"; import { NumberShower } from "./NumberShower"; import { ErrorAlert } from "./Alert"; +import { FunctionSettings } from "./FunctionChart"; let SquigglePercentilesChart = createClassFromSpec({ spec: percentilesSpec as Spec, @@ -38,16 +36,11 @@ function unwrap(x: result): a { throw Error("FAILURE TO UNWRAP"); } } -export type FunctionChartSettings = { - start: number; - stop: number; - count: number; -}; interface FunctionChart1DistProps { fn: lambdaValue; - chartSettings: FunctionChartSettings; - distributionPlotSettings: DistributionPlottingSettings; + chartSettings: FunctionSettings; + distributionPlotSettings: PlotSettings; environment: environment; height: number; } @@ -182,7 +175,7 @@ export const FunctionChart1Dist: React.FC = ({ distribution={mouseItem.value.value} width={400} height={50} - {...distributionPlotSettings} + settings={distributionPlotSettings} /> ) : null; diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index 86b2b078..b69612f0 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -10,6 +10,8 @@ import { } from "@quri/squiggle-lang"; import { useSquiggle } from "../lib/hooks"; import { SquiggleViewer } from "./SquiggleViewer"; +import { FunctionSettings, functionSettingsFromPartial } from "./FunctionChart"; +import { PlotSettings, plotSettingsFromPartial } from "./DistributionChart"; export interface SquiggleChartProps { /** The input string for squiggle */ @@ -20,12 +22,8 @@ export interface SquiggleChartProps { sampleCount?: number; /** The amount of points returned to draw the distribution */ environment?: environment; - /** If the result is a function, where the function domain starts */ - diagramStart?: number; - /** If the result is a function, where the function domain ends */ - diagramStop?: number; - /** If the result is a function, the amount of stops sampled */ - diagramCount?: number; + plotSettings?: PlotSettings; + functionSettings?: FunctionSettings; /** When the squiggle code gets reevaluated */ onChange?(expr: squiggleExpression | undefined): void; /** CSS width of the element */ @@ -35,24 +33,6 @@ export interface SquiggleChartProps { bindings?: bindings; /** JS imported parameters */ jsImports?: jsImports; - /** Whether to show a summary of the distribution */ - showSummary?: boolean; - /** Set the x scale to be logarithmic by deault */ - logX?: boolean; - /** Set the y scale to be exponential by deault */ - expY?: boolean; - /** How to format numbers on the x axis */ - tickFormat?: string; - /** Title of the graphed distribution */ - title?: string; - /** Color of the graphed distribution */ - color?: string; - /** Specify the lower bound of the x scale */ - minX?: number; - /** Specify the upper bound of the x scale */ - maxX?: number; - /** Whether to show vega actions to the user, so they can copy the chart spec */ - distributionChartActions?: boolean; enableLocalSettings?: boolean; } @@ -67,19 +47,9 @@ export const SquiggleChart: React.FC = React.memo( height = 200, bindings = defaultBindings, jsImports = defaultImports, - showSummary = false, width, - logX = false, - expY = false, - diagramStart = 0, - diagramStop = 10, - diagramCount = 100, - tickFormat, - minX, - maxX, - color, - title, - distributionChartActions, + functionSettings, + plotSettings, enableLocalSettings = false, }) => { const result = useSquiggle({ @@ -91,31 +61,13 @@ export const SquiggleChart: React.FC = React.memo( executionId, }); - const distributionPlotSettings = { - showSummary, - logX, - expY, - format: tickFormat, - minX, - maxX, - color, - title, - actions: distributionChartActions, - }; - - const chartSettings = { - start: diagramStart, - stop: diagramStop, - count: diagramCount, - }; - return ( diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 837c4bed..1412828d 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -6,7 +6,7 @@ import React, { useRef, useCallback, } from "react"; -import { useForm, UseFormRegister, useWatch } from "react-hook-form"; +import { useForm, UseFormRegister } from "react-hook-form"; import * as yup from "yup"; import { useMaybeControlledValue, useRunnerState } from "../lib/hooks"; import { yupResolver } from "@hookform/resolvers/yup"; @@ -36,10 +36,8 @@ import { InputItem } from "./ui/InputItem"; import { Text } from "./ui/Text"; import { ViewSettings, viewSettingsSchema } from "./ViewSettings"; import { HeadedSection } from "./ui/HeadedSection"; -import { - defaultColor, - defaultTickFormat, -} from "../lib/distributionSpecBuilder"; +import { plotSettingsFromPartial } from "./DistributionChart"; +import { functionSettingsFromPartial } from "./FunctionChart"; type PlaygroundProps = SquiggleChartProps & { /** The initial squiggle string to put in the playground */ @@ -52,24 +50,25 @@ type PlaygroundProps = SquiggleChartProps & { }; const schema = yup - .object({}) - .shape({ - sampleCount: yup - .number() - .required() - .positive() - .integer() - .default(1000) - .min(10) - .max(1000000), - xyPointLength: yup - .number() - .required() - .positive() - .integer() - .default(1000) - .min(10) - .max(10000), + .object({ + sampleSettings: yup.object({ + sampleCount: yup + .number() + .required() + .positive() + .integer() + .default(1000) + .min(10) + .max(1000000), + xyPointLength: yup + .number() + .required() + .positive() + .integer() + .default(1000) + .min(10) + .max(10000), + }), }) .concat(viewSettingsSchema); @@ -81,7 +80,7 @@ const SamplingSettings: React.FC<{ register: UseFormRegister }> = ({
}> = ({
({ export const SquigglePlayground: FC = ({ defaultCode = "", height = 500, - showSummary = false, - logX = false, - expY = false, - title, - minX, - maxX, - color = defaultColor, - tickFormat = defaultTickFormat, - distributionChartActions, + plotSettings: initialPlotSettings, + functionSettings: initialFunctionSettings, code: controlledCode, onCodeChange, onSettingsChange, @@ -229,41 +221,40 @@ export const SquigglePlayground: FC = ({ const [imports, setImports] = useState({}); - const { register, control } = useForm({ - resolver: yupResolver(schema), - defaultValues: { + const defaultValues = { + chartHeight: 150, + showEditor, + sampleSettings: { sampleCount: 1000, xyPointLength: 1000, - chartHeight: 150, - logX, - expY, - title, - minX, - maxX, - color, - tickFormat, - distributionChartActions, - showSummary, - showEditor, - diagramStart: 0, - diagramStop: 10, - diagramCount: 20, }, - }); - const vars = useWatch({ - control, - }); + plotSettings: plotSettingsFromPartial(initialPlotSettings || {}), + functionSettings: functionSettingsFromPartial( + initialFunctionSettings || {} + ), + }; + const { register, watch, getValues } = useForm({ + resolver: yupResolver(schema), + defaultValues, + }); + watch(); + + const [settings, setSettings] = useState(() => getValues()); useEffect(() => { - onSettingsChange?.(vars); - }, [vars, onSettingsChange]); + const subscription = watch(() => { + setSettings(getValues()); + onSettingsChange?.(getValues()); + }); + return () => subscription.unsubscribe(); + }, [onSettingsChange, getValues, watch]); const env: environment = useMemo( () => ({ - sampleCount: Number(vars.sampleCount), - xyPointLength: Number(vars.xyPointLength), + sampleCount: Number(settings.sampleSettings.sampleCount), + xyPointLength: Number(settings.sampleSettings.xyPointLength), }), - [vars.sampleCount, vars.xyPointLength] + [settings.sampleSettings.sampleCount, settings.sampleSettings.xyPointLength] ); const { @@ -285,7 +276,8 @@ export const SquigglePlayground: FC = ({ code={renderedCode} executionId={executionId} environment={env} - {...vars} + plotSettings={settings.plotSettings} + functionSettings={settings.functionSettings} bindings={defaultBindings} jsImports={imports} enableLocalSettings={true} @@ -293,7 +285,7 @@ export const SquigglePlayground: FC = ({
); - const firstTab = vars.showEditor ? ( + const firstTab = settings.showEditor ? (
= ({
@@ -378,7 +370,7 @@ export const SquigglePlayground: FC = ({ onAutorunModeChange={setAutorunMode} />
- {vars.showEditor ? withEditor : withoutEditor} + {settings.showEditor ? withEditor : withoutEditor}
diff --git a/packages/components/src/components/SquiggleViewer/ExpressionViewer.tsx b/packages/components/src/components/SquiggleViewer/ExpressionViewer.tsx index 1417fb28..ce5a1124 100644 --- a/packages/components/src/components/SquiggleViewer/ExpressionViewer.tsx +++ b/packages/components/src/components/SquiggleViewer/ExpressionViewer.tsx @@ -2,7 +2,7 @@ import React from "react"; import { squiggleExpression, declaration } from "@quri/squiggle-lang"; import { NumberShower } from "../NumberShower"; import { DistributionChart } from "../DistributionChart"; -import { FunctionChart, FunctionChartSettings } from "../FunctionChart"; +import { FunctionChart, FunctionSettings } from "../FunctionChart"; import clsx from "clsx"; import { VariableBox } from "./VariableBox"; import { ItemSettingsMenu } from "./ItemSettingsMenu"; @@ -21,7 +21,7 @@ function getRange(x: declaration) { } } -function getChartSettings(x: declaration): FunctionChartSettings { +function getFunctionSettings(x: declaration): FunctionSettings { const range = getRange(x); const min = range.floats ? range.floats.min : 0; const max = range.floats ? range.floats.max : 10; @@ -96,7 +96,7 @@ export const ExpressionViewer: React.FC = ({ return ( @@ -189,8 +189,8 @@ export const ExpressionViewer: React.FC = ({ )})`}
= ({ {(settings) => ( ({ resolver: yupResolver(viewSettingsSchema), defaultValues: { - // this is a mess and should be fixed showEditor: true, // doesn't matter chartHeight: mergedSettings.height, - showSummary: mergedSettings.distributionPlotSettings.showSummary, - logX: mergedSettings.distributionPlotSettings.logX, - expY: mergedSettings.distributionPlotSettings.expY, - tickFormat: - mergedSettings.distributionPlotSettings.format || defaultTickFormat, - title: mergedSettings.distributionPlotSettings.title, - color: mergedSettings.distributionPlotSettings.color || defaultColor, - minX: mergedSettings.distributionPlotSettings.minX, - maxX: mergedSettings.distributionPlotSettings.maxX, - distributionChartActions: mergedSettings.distributionPlotSettings.actions, - diagramStart: mergedSettings.chartSettings.start, - diagramStop: mergedSettings.chartSettings.stop, - diagramCount: mergedSettings.chartSettings.count, + plotSettings: mergedSettings.plotSettings, + functionSettings: mergedSettings.functionSettings, }, }); + useEffect(() => { const subscription = watch((vars) => { const settings = getSettings(path); // get the latest version setSettings(path, { ...settings, - distributionPlotSettings: { - showSummary: vars.showSummary, - logX: vars.logX, - expY: vars.expY, - format: vars.tickFormat, - title: vars.title, - color: vars.color, - minX: vars.minX, - maxX: vars.maxX, - actions: vars.distributionChartActions, - }, - chartSettings: { - start: vars.diagramStart, - stop: vars.diagramStop, - count: vars.diagramCount, - }, + // vars should be defined and react-hook-form is too conservative with its types so these `?.` are ok... hopefully. + // This might cause a bug in the future. I don't really understand why react-hook-form needs to return partials here. + plotSettings: vars?.plotSettings, + functionSettings: vars?.functionSettings, }); onChange(); }); @@ -141,13 +118,13 @@ export const ItemSettingsMenu: React.FC = (props) => { className="h-5 w-5 cursor-pointer text-slate-400 hover:text-slate-500" onClick={() => setIsOpen(!isOpen)} /> - {settings.distributionPlotSettings || settings.chartSettings ? ( + {settings.plotSettings || settings.functionSettings ? (