diff --git a/packages/components/README.md b/packages/components/README.md index 63a98d34..2b911caa 100644 --- a/packages/components/README.md +++ b/packages/components/README.md @@ -54,7 +54,6 @@ export function DynamicSquiggleChart({ squiggleString }) { width={445} height={200} showSummary={true} - showTypes={true} /> ); } diff --git a/packages/components/package.json b/packages/components/package.json index adaf90a4..8e11a913 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -3,6 +3,8 @@ "version": "0.2.23", "license": "MIT", "dependencies": { + "@floating-ui/react-dom": "^0.7.2", + "@floating-ui/react-dom-interactions": "^0.6.6", "@headlessui/react": "^1.6.6", "@heroicons/react": "^1.0.6", "@hookform/resolvers": "^2.9.6", diff --git a/packages/components/src/components/DistributionChart.tsx b/packages/components/src/components/DistributionChart.tsx index af644d29..1e1c3822 100644 --- a/packages/components/src/components/DistributionChart.tsx +++ b/packages/components/src/components/DistributionChart.tsx @@ -8,26 +8,24 @@ import { import { Vega } from "react-vega"; import { ErrorAlert } from "./Alert"; import { useSize } from "react-use"; -import clsx from "clsx"; import { buildVegaSpec, DistributionChartSpecOptions, } from "../lib/distributionSpecBuilder"; import { NumberShower } from "./NumberShower"; +import { hasMassBelowZero } from "../lib/distributionUtils"; export type DistributionPlottingSettings = { /** Whether to show a summary of means, stdev, percentiles etc */ showSummary: boolean; - /** Whether to show the user graph controls (scale etc) */ - showControls: boolean; + actions?: boolean; } & DistributionChartSpecOptions; export type DistributionChartProps = { distribution: Distribution; width?: number; height: number; - actions?: boolean; } & DistributionPlottingSettings; export const DistributionChart: React.FC = (props) => { @@ -36,17 +34,9 @@ export const DistributionChart: React.FC = (props) => { height, showSummary, width, - showControls, logX, - expY, actions = false, } = props; - const [isLogX, setLogX] = React.useState(logX); - const [isExpY, setExpY] = React.useState(expY); - - React.useEffect(() => setLogX(logX), [logX]); - React.useEffect(() => setExpY(expY), [expY]); - const shape = distribution.pointSet(); const [sized] = useSize((size) => { if (shape.tag === "Error") { @@ -57,9 +47,6 @@ export const DistributionChart: React.FC = (props) => { ); } - const massBelow0 = - shape.value.continuous.some((x) => x.x <= 0) || - shape.value.discrete.some((x) => x.x <= 0); const spec = buildVegaSpec(props); let widthProp = width ? width : size.width; @@ -72,7 +59,11 @@ export const DistributionChart: React.FC = (props) => { return (
- {!(isLogX && massBelow0) ? ( + {logX && hasMassBelowZero(shape.value) ? ( + + Cannot graph distribution with negative values on logarithmic scale. + + ) : ( = (props) => { height={height} actions={actions} /> - ) : ( - - Cannot graph distribution with negative values on logarithmic scale. - )}
{showSummary && }
- {showControls && ( -
- - -
- )}
); }); return sized; }; -interface CheckBoxProps { - label: string; - onChange: (x: boolean) => void; - value: boolean; - disabled?: boolean; - tooltip?: string; -} - -export const CheckBox: React.FC = ({ - label, - onChange, - value, - disabled = false, - tooltip, -}) => { - return ( - - onChange(!value)} - disabled={disabled} - className="form-checkbox" - /> - - - ); -}; - const TableHeadCell: React.FC<{ children: React.ReactNode }> = ({ children, }) => ( diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index f9f2b368..10e4e22c 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -9,8 +9,7 @@ import { defaultEnvironment, } from "@quri/squiggle-lang"; import { useSquiggle } from "../lib/hooks"; -import { SquiggleErrorAlert } from "./SquiggleErrorAlert"; -import { SquiggleItem } from "./SquiggleItem"; +import { SquiggleViewer } from "./SquiggleViewer"; export interface SquiggleChartProps { /** The input string for squiggle */ @@ -36,10 +35,6 @@ export interface SquiggleChartProps { jsImports?: jsImports; /** Whether to show a summary of the distribution */ showSummary?: boolean; - /** Whether to show type information about returns, default false */ - showTypes?: boolean; - /** Whether to show graph controls (scale etc)*/ - showControls?: boolean; /** Set the x scale to be logarithmic by deault */ logX?: boolean; /** Set the y scale to be exponential by deault */ @@ -56,6 +51,7 @@ export interface SquiggleChartProps { maxX?: number; /** Whether to show vega actions to the user, so they can copy the chart spec */ distributionChartActions?: boolean; + enableLocalSettings?: boolean; } const defaultOnChange = () => {}; @@ -70,8 +66,6 @@ export const SquiggleChart: React.FC = React.memo( jsImports = defaultImports, showSummary = false, width, - showTypes = false, - showControls = false, logX = false, expY = false, diagramStart = 0, @@ -83,6 +77,7 @@ export const SquiggleChart: React.FC = React.memo( color, title, distributionChartActions, + enableLocalSettings = false, }) => { const result = useSquiggle({ code, @@ -92,12 +87,7 @@ export const SquiggleChart: React.FC = React.memo( onChange, }); - if (result.tag !== "Ok") { - return ; - } - - let distributionPlotSettings = { - showControls, + const distributionPlotSettings = { showSummary, logX, expY, @@ -109,21 +99,21 @@ export const SquiggleChart: React.FC = React.memo( actions: distributionChartActions, }; - let chartSettings = { + const chartSettings = { start: diagramStart, stop: diagramStop, count: diagramCount, }; return ( - ); } diff --git a/packages/components/src/components/SquiggleContainer.tsx b/packages/components/src/components/SquiggleContainer.tsx index 63dbb54a..bb3f1db4 100644 --- a/packages/components/src/components/SquiggleContainer.tsx +++ b/packages/components/src/components/SquiggleContainer.tsx @@ -13,6 +13,7 @@ const SquiggleContext = React.createContext({ export const SquiggleContainer: React.FC = ({ children }) => { const context = useContext(SquiggleContext); + if (context.containerized) { return <>{children}; } else { diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 5ca84040..d333985a 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -1,5 +1,12 @@ -import React, { FC, useState, useEffect, useMemo } from "react"; -import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form"; +import React, { + FC, + useState, + useEffect, + useMemo, + useRef, + useCallback, +} from "react"; +import { useForm, UseFormRegister, useWatch } from "react-hook-form"; import * as yup from "yup"; import { useMaybeControlledValue } from "../lib/hooks"; import { yupResolver } from "@hookform/resolvers/yup"; @@ -24,13 +31,19 @@ import { JsonEditor } from "./JsonEditor"; import { ErrorAlert, SuccessAlert } from "./Alert"; import { SquiggleContainer } from "./SquiggleContainer"; import { Toggle } from "./ui/Toggle"; -import { Checkbox } from "./ui/Checkbox"; import { StyledTab } from "./ui/StyledTab"; +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"; type PlaygroundProps = SquiggleChartProps & { /** The initial squiggle string to put in the playground */ defaultCode?: string; - /** How many pixels high is the playground */ onCodeChange?(expr: string): void; /* When settings change */ onSettingsChange?(settings: any): void; @@ -38,91 +51,30 @@ type PlaygroundProps = SquiggleChartProps & { showEditor?: boolean; }; -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), - chartHeight: yup.number().required().positive().integer().default(350), - leftSizePercent: yup - .number() - .required() - .positive() - .integer() - .min(10) - .max(100) - .default(50), - showTypes: yup.boolean().required(), - showControls: yup.boolean().required(), - showSummary: yup.boolean().required(), - showEditor: yup.boolean().required(), - logX: yup.boolean().required(), - expY: yup.boolean().required(), - tickFormat: yup.string().default(".9~s"), - title: yup.string(), - color: yup.string().default("#739ECC").required(), - minX: yup.number(), - maxX: yup.number(), - distributionChartActions: yup.boolean(), - showSettingsPage: yup.boolean().default(false), - diagramStart: yup.number().required().positive().integer().default(0).min(0), - diagramStop: yup.number().required().positive().integer().default(10).min(0), - diagramCount: yup.number().required().positive().integer().default(20).min(2), -}); +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), + }) + .concat(viewSettingsSchema); type FormFields = yup.InferType; -const HeadedSection: FC<{ title: string; children: React.ReactNode }> = ({ - title, - children, -}) => ( -
-
- {title} -
-
{children}
-
-); - -const Text: FC<{ children: React.ReactNode }> = ({ children }) => ( -

{children}

-); - -function InputItem({ - name, - label, - type, - register, -}: { - name: Path; - label: string; - type: "number" | "text" | "color"; - register: UseFormRegister; -}) { - return ( - - ); -} - const SamplingSettings: React.FC<{ register: UseFormRegister }> = ({ register, }) => ( @@ -158,128 +110,6 @@ const SamplingSettings: React.FC<{ register: UseFormRegister }> = ({ ); -const ViewSettings: React.FC<{ register: UseFormRegister }> = ({ - register, -}) => ( -
- -
- - - -
-
- -
- -
- - - - - - - - - - -
-
-
- -
- -
- - When displaying functions of single variables that return numbers or - distributions, we need to use defaults for the x-axis. We need to - select a minimum and maximum value of x to sample, and a number n of - the number of points to sample. - -
- - - -
-
-
-
-
-); - const InputVariablesSettings: React.FC<{ initialImports: any; // TODO - any json type setImports: (imports: any) => void; @@ -406,19 +236,24 @@ const useRunnerState = (code: string) => { }; }; +type PlaygroundContextShape = { + getLeftPanelElement: () => HTMLDivElement | undefined; +}; +export const PlaygroundContext = React.createContext({ + getLeftPanelElement: () => undefined, +}); + export const SquigglePlayground: FC = ({ defaultCode = "", height = 500, - showTypes = false, - showControls = false, showSummary = false, logX = false, expY = false, title, minX, maxX, - color = "#739ECC", - tickFormat = ".9~s", + color = defaultColor, + tickFormat = defaultTickFormat, distributionChartActions, code: controlledCode, onCodeChange, @@ -439,8 +274,6 @@ export const SquigglePlayground: FC = ({ sampleCount: 1000, xyPointLength: 1000, chartHeight: 150, - showTypes, - showControls, logX, expY, title, @@ -451,8 +284,6 @@ export const SquigglePlayground: FC = ({ distributionChartActions, showSummary, showEditor, - leftSizePercent: 50, - showSettingsPage: false, diagramStart: 0, diagramStop: 10, diagramCount: 20, @@ -484,6 +315,7 @@ export const SquigglePlayground: FC = ({ {...vars} bindings={defaultBindings} jsImports={imports} + enableLocalSettings={true} /> ); @@ -509,7 +341,15 @@ export const SquigglePlayground: FC = ({ - + + > + } + /> = ({ ); + const leftPanelRef = useRef(null); + const withEditor = (
-
{tabs}
+
+ {tabs} +
{squiggleChart}
); const withoutEditor =
{tabs}
; + const getLeftPanelElement = useCallback(() => { + return leftPanelRef.current ?? undefined; + }, []); + return ( - -
-
- - + +
+
+ + + + + + + - - - - - +
+ {vars.showEditor ? withEditor : withoutEditor}
- {vars.showEditor ? withEditor : withoutEditor} -
- + + ); }; diff --git a/packages/components/src/components/SquiggleViewer/ExpressionViewer.tsx b/packages/components/src/components/SquiggleViewer/ExpressionViewer.tsx new file mode 100644 index 00000000..1417fb28 --- /dev/null +++ b/packages/components/src/components/SquiggleViewer/ExpressionViewer.tsx @@ -0,0 +1,291 @@ +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 clsx from "clsx"; +import { VariableBox } from "./VariableBox"; +import { ItemSettingsMenu } from "./ItemSettingsMenu"; +import { hasMassBelowZero } from "../../lib/distributionUtils"; +import { MergedItemSettings } from "./utils"; + +function getRange(x: declaration) { + const first = x.args[0]; + switch (first.tag) { + case "Float": { + return { floats: { min: first.value.min, max: first.value.max } }; + } + case "Date": { + return { time: { min: first.value.min, max: first.value.max } }; + } + } +} + +function getChartSettings(x: declaration): FunctionChartSettings { + const range = getRange(x); + const min = range.floats ? range.floats.min : 0; + const max = range.floats ? range.floats.max : 10; + return { + start: min, + stop: max, + count: 20, + }; +} + +const VariableList: React.FC<{ + path: string[]; + heading: string; + children: (settings: MergedItemSettings) => React.ReactNode; +}> = ({ path, heading, children }) => ( + + {(settings) => ( +
+ {children(settings)} +
+ )} +
+); + +export interface Props { + /** The output of squiggle's run */ + expression: squiggleExpression; + /** Path to the current item, e.g. `['foo', 'bar', '3']` for `foo.bar[3]`; can be empty on the top-level item. */ + path: string[]; + width?: number; +} + +export const ExpressionViewer: React.FC = ({ + path, + expression, + width, +}) => { + switch (expression.tag) { + case "number": + return ( + + {() => ( +
+ +
+ )} +
+ ); + case "distribution": { + const distType = expression.value.type(); + return ( + { + const shape = expression.value.pointSet(); + return ( + + ); + }} + > + {(settings) => { + return ( + + ); + }} + + ); + } + case "string": + return ( + + {() => ( + <> + " + + {expression.value} + + " + + )} + + ); + case "boolean": + return ( + + {() => expression.value.toString()} + + ); + case "symbol": + return ( + + {() => ( + <> + Undefined Symbol: + {expression.value} + + )} + + ); + case "call": + return ( + + {() => expression.value} + + ); + case "arraystring": + return ( + + {() => expression.value.map((r) => `"${r}"`).join(", ")} + + ); + case "date": + return ( + + {() => expression.value.toDateString()} + + ); + case "void": + return ( + + {() => "Void"} + + ); + case "timeDuration": { + return ( + + {() => } + + ); + } + case "lambda": + return ( + { + return ( + + ); + }} + > + {(settings) => ( + <> +
{`function(${expression.value.parameters.join( + "," + )})`}
+ + + )} +
+ ); + case "lambdaDeclaration": { + return ( + { + return ( + + ); + }} + > + {(settings) => ( + + )} + + ); + } + case "module": { + return ( + + {(settings) => + Object.entries(expression.value) + .filter(([key, r]) => !key.match(/^(Math|System)\./)) + .map(([key, r]) => ( + + )) + } + + ); + } + case "record": + return ( + + {(settings) => + Object.entries(expression.value).map(([key, r]) => ( + + )) + } + + ); + case "array": + return ( + + {(settings) => + expression.value.map((r, i) => ( + + )) + } + + ); + default: { + return ( +
+ No display for type: {" "} + {expression.tag} +
+ ); + } + } +}; diff --git a/packages/components/src/components/SquiggleViewer/ItemSettingsMenu.tsx b/packages/components/src/components/SquiggleViewer/ItemSettingsMenu.tsx new file mode 100644 index 00000000..2c26b9aa --- /dev/null +++ b/packages/components/src/components/SquiggleViewer/ItemSettingsMenu.tsx @@ -0,0 +1,168 @@ +import { CogIcon } from "@heroicons/react/solid"; +import React, { useContext, useRef, useState, useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { Modal } from "../ui/Modal"; +import { ViewSettings, viewSettingsSchema } from "../ViewSettings"; +import { Path, pathAsString } from "./utils"; +import { ViewerContext } from "./ViewerContext"; +import { + defaultColor, + defaultTickFormat, +} from "../../lib/distributionSpecBuilder"; +import { PlaygroundContext } from "../SquigglePlayground"; + +type Props = { + path: Path; + onChange: () => void; + disableLogX?: boolean; + withFunctionSettings: boolean; +}; + +const ItemSettingsModal: React.FC< + Props & { close: () => void; resetScroll: () => void } +> = ({ + path, + onChange, + disableLogX, + withFunctionSettings, + close, + resetScroll, +}) => { + const { setSettings, getSettings, getMergedSettings } = + useContext(ViewerContext); + + const mergedSettings = getMergedSettings(path); + + const { register, watch } = useForm({ + 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, + }, + }); + 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, + }, + }); + onChange(); + }); + return () => subscription.unsubscribe(); + }, [getSettings, setSettings, onChange, path, watch]); + + const { getLeftPanelElement } = useContext(PlaygroundContext); + + return ( + + + Chart settings + {path.length ? ( + <> + {" for "} + + {pathAsString(path)} + {" "} + + ) : ( + "" + )} + + + + + + ); +}; + +export const ItemSettingsMenu: React.FC = (props) => { + const [isOpen, setIsOpen] = useState(false); + const { enableLocalSettings, setSettings, getSettings } = + useContext(ViewerContext); + + const ref = useRef(null); + + if (!enableLocalSettings) { + return null; + } + const settings = getSettings(props.path); + + const resetScroll = () => { + if (!ref.current) return; + window.scroll({ + top: ref.current.getBoundingClientRect().y + window.scrollY, + behavior: "smooth", + }); + }; + + return ( +
+ setIsOpen(!isOpen)} + /> + {settings.distributionPlotSettings || settings.chartSettings ? ( + + ) : null} + {isOpen ? ( + setIsOpen(false)} + resetScroll={resetScroll} + /> + ) : null} +
+ ); +}; diff --git a/packages/components/src/components/SquiggleViewer/VariableBox.tsx b/packages/components/src/components/SquiggleViewer/VariableBox.tsx new file mode 100644 index 00000000..97c31d45 --- /dev/null +++ b/packages/components/src/components/SquiggleViewer/VariableBox.tsx @@ -0,0 +1,79 @@ +import React, { useContext, useReducer } from "react"; +import { Tooltip } from "../ui/Tooltip"; +import { LocalItemSettings, MergedItemSettings } from "./utils"; +import { ViewerContext } from "./ViewerContext"; + +type SettingsMenuParams = { + onChange: () => void; // used to notify VariableBox that settings have changed, so that VariableBox could re-render itself +}; + +type VariableBoxProps = { + path: string[]; + heading: string; + renderSettingsMenu?: (params: SettingsMenuParams) => React.ReactNode; + children: (settings: MergedItemSettings) => React.ReactNode; +}; + +export const VariableBox: React.FC = ({ + path, + heading = "Error", + renderSettingsMenu, + children, +}) => { + const { setSettings, getSettings, getMergedSettings } = + useContext(ViewerContext); + + // Since ViewerContext doesn't keep the actual settings, VariableBox won't rerender when setSettings is called. + // So we use `forceUpdate` to force rerendering. + const [_, forceUpdate] = useReducer((x) => x + 1, 0); + + const settings = getSettings(path); + + const setSettingsAndUpdate = (newSettings: LocalItemSettings) => { + setSettings(path, newSettings); + forceUpdate(); + }; + + const toggleCollapsed = () => { + setSettingsAndUpdate({ ...settings, collapsed: !settings.collapsed }); + }; + + const isTopLevel = path.length === 0; + const name = isTopLevel ? "Result" : path[path.length - 1]; + + return ( +
+
+ + + {name}: + + + {settings.collapsed ? ( + + ... + + ) : renderSettingsMenu ? ( + renderSettingsMenu({ onChange: forceUpdate }) + ) : null} +
+ {settings.collapsed ? null : ( +
+ {path.length ? ( +
+ ) : null} +
{children(getMergedSettings(path))}
+
+ )} +
+ ); +}; diff --git a/packages/components/src/components/SquiggleViewer/ViewerContext.ts b/packages/components/src/components/SquiggleViewer/ViewerContext.ts new file mode 100644 index 00000000..0769f3b1 --- /dev/null +++ b/packages/components/src/components/SquiggleViewer/ViewerContext.ts @@ -0,0 +1,35 @@ +import { defaultEnvironment } from "@quri/squiggle-lang"; +import React from "react"; +import { LocalItemSettings, MergedItemSettings, Path } from "./utils"; + +type ViewerContextShape = { + // Note that we don't store settings themselves in the context (that would cause rerenders of the entire tree on each settings update). + // Instead, we keep settings in local state and notify the global context via setSettings to pass them down the component tree again if it got rebuilt from scratch. + // See ./SquiggleViewer.tsx and ./VariableBox.tsx for other implementation details on this. + getSettings(path: Path): LocalItemSettings; + getMergedSettings(path: Path): MergedItemSettings; + setSettings(path: Path, value: LocalItemSettings): void; + enableLocalSettings: boolean; // show local settings icon in the UI +}; + +export const ViewerContext = React.createContext({ + getSettings: () => ({ collapsed: false }), + getMergedSettings: () => ({ + collapsed: false, + // copy-pasted from SquiggleChart + chartSettings: { + start: 0, + stop: 10, + count: 100, + }, + distributionPlotSettings: { + showSummary: false, + logX: false, + expY: false, + }, + environment: defaultEnvironment, + height: 150, + }), + setSettings() {}, + enableLocalSettings: false, +}); diff --git a/packages/components/src/components/SquiggleViewer/index.tsx b/packages/components/src/components/SquiggleViewer/index.tsx new file mode 100644 index 00000000..6feb3cad --- /dev/null +++ b/packages/components/src/components/SquiggleViewer/index.tsx @@ -0,0 +1,100 @@ +import React, { useCallback, useRef } from "react"; +import { environment } from "@quri/squiggle-lang"; +import { DistributionPlottingSettings } from "../DistributionChart"; +import { FunctionChartSettings } from "../FunctionChart"; +import { ExpressionViewer } from "./ExpressionViewer"; +import { ViewerContext } from "./ViewerContext"; +import { + LocalItemSettings, + MergedItemSettings, + Path, + pathAsString, +} from "./utils"; +import { useSquiggle } from "../../lib/hooks"; +import { SquiggleErrorAlert } from "../SquiggleErrorAlert"; + +type Props = { + /** The output of squiggle's run */ + result: ReturnType; + width?: number; + height: number; + distributionPlotSettings: DistributionPlottingSettings; + /** Settings for displaying functions */ + chartSettings: FunctionChartSettings; + /** Environment for further function executions */ + environment: environment; + enableLocalSettings?: boolean; +}; + +type Settings = { + [k: string]: LocalItemSettings; +}; + +const defaultSettings: LocalItemSettings = { collapsed: false }; + +export const SquiggleViewer: React.FC = ({ + result, + width, + height, + distributionPlotSettings, + chartSettings, + environment, + enableLocalSettings = false, +}) => { + // can't store settings in the state because we don't want to rerender the entire tree on every change + const settingsRef = useRef({}); + + const getSettings = useCallback( + (path: Path) => { + return settingsRef.current[pathAsString(path)] || defaultSettings; + }, + [settingsRef] + ); + + const setSettings = useCallback( + (path: Path, value: LocalItemSettings) => { + settingsRef.current[pathAsString(path)] = value; + }, + [settingsRef] + ); + + const getMergedSettings = useCallback( + (path: Path) => { + const localSettings = getSettings(path); + const result: MergedItemSettings = { + distributionPlotSettings: { + ...distributionPlotSettings, + ...(localSettings.distributionPlotSettings || {}), + }, + chartSettings: { + ...chartSettings, + ...(localSettings.chartSettings || {}), + }, + environment: { + ...environment, + ...(localSettings.environment || {}), + }, + height: localSettings.height || height, + }; + return result; + }, + [distributionPlotSettings, chartSettings, environment, height, getSettings] + ); + + return ( + + {result.tag === "Ok" ? ( + + ) : ( + + )} + + ); +}; diff --git a/packages/components/src/components/SquiggleViewer/utils.ts b/packages/components/src/components/SquiggleViewer/utils.ts new file mode 100644 index 00000000..3053966b --- /dev/null +++ b/packages/components/src/components/SquiggleViewer/utils.ts @@ -0,0 +1,22 @@ +import { DistributionPlottingSettings } from "../DistributionChart"; +import { FunctionChartSettings } from "../FunctionChart"; +import { environment } from "@quri/squiggle-lang"; + +export type LocalItemSettings = { + collapsed: boolean; + distributionPlotSettings?: Partial; + chartSettings?: Partial; + height?: number; + environment?: Partial; +}; + +export type MergedItemSettings = { + distributionPlotSettings: DistributionPlottingSettings; + chartSettings: FunctionChartSettings; + height: number; + environment: environment; +}; + +export type Path = string[]; + +export const pathAsString = (path: Path) => path.join("."); diff --git a/packages/components/src/components/ViewSettings.tsx b/packages/components/src/components/ViewSettings.tsx new file mode 100644 index 00000000..9a2ce562 --- /dev/null +++ b/packages/components/src/components/ViewSettings.tsx @@ -0,0 +1,163 @@ +import React from "react"; +import * as yup from "yup"; +import { UseFormRegister } from "react-hook-form"; +import { InputItem } from "./ui/InputItem"; +import { Checkbox } from "./ui/Checkbox"; +import { HeadedSection } from "./ui/HeadedSection"; +import { Text } from "./ui/Text"; +import { + defaultColor, + defaultTickFormat, +} from "../lib/distributionSpecBuilder"; + +export const viewSettingsSchema = yup.object({}).shape({ + chartHeight: yup.number().required().positive().integer().default(350), + showSummary: yup.boolean().required(), + showEditor: yup.boolean().required(), + logX: yup.boolean().required(), + expY: yup.boolean().required(), + tickFormat: yup.string().default(defaultTickFormat), + title: yup.string(), + color: yup.string().default(defaultColor).required(), + minX: yup.number(), + maxX: yup.number(), + distributionChartActions: yup.boolean(), + diagramStart: yup.number().required().positive().integer().default(0).min(0), + diagramStop: yup.number().required().positive().integer().default(10).min(0), + diagramCount: yup.number().required().positive().integer().default(20).min(2), +}); + +type FormFields = yup.InferType; + +// This component is used in two places: for global settings in SquigglePlayground, and for item-specific settings in modal dialogs. +export const ViewSettings: React.FC<{ + withShowEditorSetting?: boolean; + withFunctionSettings?: boolean; + disableLogXSetting?: boolean; + register: UseFormRegister; +}> = ({ + withShowEditorSetting = true, + withFunctionSettings = true, + disableLogXSetting, + register, +}) => { + return ( +
+ +
+ {withShowEditorSetting ? ( + + ) : null} + +
+
+ +
+ +
+ + + + + + + + + +
+
+
+ + {withFunctionSettings ? ( +
+ +
+ + When displaying functions of single variables that return + numbers or distributions, we need to use defaults for the + x-axis. We need to select a minimum and maximum value of x to + sample, and a number n of the number of points to sample. + +
+ + + +
+
+
+
+ ) : null} +
+ ); +}; diff --git a/packages/components/src/components/ui/Checkbox.tsx b/packages/components/src/components/ui/Checkbox.tsx index c06e347b..36ab68d9 100644 --- a/packages/components/src/components/ui/Checkbox.tsx +++ b/packages/components/src/components/ui/Checkbox.tsx @@ -1,3 +1,4 @@ +import clsx from "clsx"; import React from "react"; import { Path, UseFormRegister } from "react-hook-form"; @@ -5,20 +6,32 @@ export function Checkbox({ name, label, register, + disabled, + tooltip, }: { name: Path; label: string; register: UseFormRegister; + disabled?: boolean; + tooltip?: string; }) { return ( -