From e8e8a06f7aaf38c72ac339cfe0726f206d77d7d3 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Thu, 16 Jun 2022 13:05:48 +0300 Subject: [PATCH] useSquiggle and useSquigglePartial hooks --- .../components/src/components/CodeEditor.tsx | 9 +- .../src/components/SquiggleChart.tsx | 285 ++---------------- .../src/components/SquiggleEditor.tsx | 212 +++++-------- .../src/components/SquiggleErrorAlert.tsx | 11 + .../src/components/SquiggleItem.tsx | 250 +++++++++++++++ .../src/components/SquigglePlayground.tsx | 11 +- packages/components/src/index.ts | 2 +- packages/components/src/lib/hooks.ts | 63 ++++ 8 files changed, 438 insertions(+), 405 deletions(-) create mode 100644 packages/components/src/components/SquiggleErrorAlert.tsx create mode 100644 packages/components/src/components/SquiggleItem.tsx create mode 100644 packages/components/src/lib/hooks.ts diff --git a/packages/components/src/components/CodeEditor.tsx b/packages/components/src/components/CodeEditor.tsx index 5d9340b8..c869b2ea 100644 --- a/packages/components/src/components/CodeEditor.tsx +++ b/packages/components/src/components/CodeEditor.tsx @@ -1,5 +1,5 @@ import _ from "lodash"; -import React, { FC } from "react"; +import React, { FC, useMemo } from "react"; import AceEditor from "react-ace"; import "ace-builds/src-noconflict/mode-golang"; @@ -21,14 +21,15 @@ export const CodeEditor: FC = ({ showGutter = false, height, }) => { - let lineCount = value.split("\n").length; - let id = _.uniqueId(); + const lineCount = value.split("\n").length; + const id = useMemo(() => _.uniqueId(), []); + return ( (x: declaration) { - let 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 { - let range = getRange(x); - let min = range.floats ? range.floats.min : 0; - let max = range.floats ? range.floats.max : 10; - return { - start: min, - stop: max, - count: 20, - }; -} - -interface VariableBoxProps { - heading: string; - children: React.ReactNode; - showTypes: boolean; -} - -export const VariableBox: React.FC = ({ - heading = "Error", - children, - showTypes = false, -}) => { - if (showTypes) { - return ( -
-
-
{heading}
-
-
{children}
-
- ); - } else { - return
{children}
; - } -}; - -export interface SquiggleItemProps { - /** The input string for squiggle */ - expression: squiggleExpression; - width?: number; - height: number; - /** Whether to show a summary of statistics for distributions */ - showSummary: boolean; - /** Whether to show type information */ - showTypes: boolean; - /** Whether to show users graph controls (scale etc) */ - showControls: boolean; - /** Settings for displaying functions */ - chartSettings: FunctionChartSettings; - /** Environment for further function executions */ - environment: environment; -} - -const SquiggleItem: React.FC = ({ - expression, - width, - height, - showSummary, - showTypes = false, - showControls = false, - chartSettings, - environment, -}) => { - switch (expression.tag) { - case "number": - return ( - -
- -
-
- ); - case "distribution": { - let distType = expression.value.type(); - return ( - - {distType === "Symbolic" && showTypes ? ( -
{expression.value.toString()}
- ) : null} - -
- ); - } - case "string": - return ( - - " - - {expression.value} - - " - - ); - case "boolean": - return ( - - {expression.value.toString()} - - ); - case "symbol": - return ( - - Undefined Symbol: - {expression.value} - - ); - case "call": - return ( - - {expression.value} - - ); - case "array": - return ( - - {expression.value.map((r, i) => ( -
-
-
{i}
-
-
- -
-
- ))} -
- ); - case "record": - return ( - -
- {Object.entries(expression.value).map(([key, r]) => ( -
-
-
{key}:
-
-
- -
-
- ))} -
-
- ); - case "arraystring": - return ( - - {expression.value.map((r) => `"${r}"`).join(", ")} - - ); - case "date": - return ( - - {expression.value.toDateString()} - - ); - case "timeDuration": { - return ( - - - - ); - } - case "lambda": - return ( - -
{`function(${expression.value.parameters.join( - "," - )})`}
- -
- ); - case "lambdaDeclaration": { - return ( - - - - ); - } - default: { - return <>Should be unreachable; - } - } -}; +import { FunctionChartSettings } from "./FunctionChart"; +import { useSquiggle } from "../lib/hooks"; +import { SquiggleErrorAlert } from "./SquiggleErrorAlert"; +import { SquiggleItem } from "./SquiggleItem"; export interface SquiggleChartProps { /** The input string for squiggle */ @@ -266,8 +22,8 @@ export interface SquiggleChartProps { environment?: environment; /** If the result is a function, where the function starts, ends and the amount of stops */ chartSettings?: FunctionChartSettings; - /** When the environment changes */ - onChange?(expr: squiggleExpression): void; + /** When the squiggle code gets reevaluated */ + onChange?(expr: squiggleExpression | undefined): void; /** CSS width of the element */ width?: number; height?: number; @@ -275,7 +31,7 @@ export interface SquiggleChartProps { bindings?: bindings; /** JS imported parameters */ jsImports?: jsImports; - /** Whether to show a summary of the distirbution */ + /** Whether to show a summary of the distribution */ showSummary?: boolean; /** Whether to show type information about returns, default false */ showTypes?: boolean; @@ -283,12 +39,13 @@ export interface SquiggleChartProps { showControls?: boolean; } +const defaultOnChange = () => {}; const defaultChartSettings = { start: 0, stop: 10, count: 20 }; export const SquiggleChart: React.FC = ({ squiggleString = "", environment, - onChange = () => {}, + onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here height = 200, bindings = defaultBindings, jsImports = defaultImports, @@ -298,28 +55,28 @@ export const SquiggleChart: React.FC = ({ showControls = false, chartSettings = defaultChartSettings, }) => { - let expressionResult = run(squiggleString, bindings, environment, jsImports); - if (expressionResult.tag !== "Ok") { - return ( - - {errorValueToString(expressionResult.value)} - - ); + const { result } = useSquiggle({ + code: squiggleString, + bindings, + environment, + jsImports, + onChange, + }); + + if (result.tag !== "Ok") { + return ; } - let e = environment ?? defaultEnvironment; - let expression = expressionResult.value; - onChange(expression); return ( ); }; diff --git a/packages/components/src/components/SquiggleEditor.tsx b/packages/components/src/components/SquiggleEditor.tsx index a8db78af..279d6ec6 100644 --- a/packages/components/src/components/SquiggleEditor.tsx +++ b/packages/components/src/components/SquiggleEditor.tsx @@ -1,39 +1,51 @@ -import * as React from "react"; +import React, { useState } from "react"; import * as ReactDOM from "react-dom"; -import { SquiggleChart } from "./SquiggleChart"; import { CodeEditor } from "./CodeEditor"; -import type { +import { squiggleExpression, environment, bindings, jsImports, + defaultEnvironment, } from "@quri/squiggle-lang"; -import { - runPartial, - errorValueToString, - defaultImports, - defaultBindings, -} from "@quri/squiggle-lang"; -import { ErrorAlert } from "./Alert"; +import { defaultImports, defaultBindings } from "@quri/squiggle-lang"; import { SquiggleContainer } from "./SquiggleContainer"; +import { useSquiggle, useSquigglePartial } from "../lib/hooks"; +import { SquiggleErrorAlert } from "./SquiggleErrorAlert"; +import { SquiggleItem } from "./SquiggleItem"; + +const WrappedCodeEditor: React.FC<{ + code: string; + setCode: (code: string) => void; +}> = ({ code, setCode }) => ( +
+ +
+); export interface SquiggleEditorProps { /** The input string for squiggle */ initialSquiggleString?: string; - /** If the output requires monte carlo sampling, the amount of samples */ - environment?: environment; + /** The width of the element */ + width?: number; /** If the result is a function, where the function starts */ diagramStart?: number; /** If the result is a function, where the function ends */ diagramStop?: number; /** If the result is a function, how many points along the function it samples */ diagramCount?: number; - /** when the environment changes. Used again for notebook magic*/ - onChange?(expr: squiggleExpression): void; - /** The width of the element */ - width?: number; + /** When the environment changes. Used again for notebook magic */ + onChange?(expr: squiggleExpression | undefined): void; /** Previous variable declarations */ bindings?: bindings; + /** If the output requires monte carlo sampling, the amount of samples */ + environment?: environment; /** JS Imports */ jsImports?: jsImports; /** Whether to show detail about types of the returns, default false */ @@ -44,169 +56,109 @@ export interface SquiggleEditorProps { showSummary?: boolean; } -export let SquiggleEditor: React.FC = ({ +export const SquiggleEditor: React.FC = ({ initialSquiggleString = "", width, - environment, diagramStart = 0, diagramStop = 10, diagramCount = 20, onChange, bindings = defaultBindings, + environment, jsImports = defaultImports, showTypes = false, showControls = false, showSummary = false, }: SquiggleEditorProps) => { - const [expression, setExpression] = React.useState(initialSquiggleString); + const [code, setCode] = useState(initialSquiggleString); + + const { result, observableRef } = useSquiggle({ + code, + bindings, + environment, + jsImports, + onChange, + }); + const chartSettings = { start: diagramStart, stop: diagramStop, count: diagramCount, }; + return ( - -
-
- + + + {result.tag === "Ok" ? ( + -
- -
-
+ ) : ( + + )} + + ); }; export function renderSquiggleEditorToDom(props: SquiggleEditorProps) { - let parent = document.createElement("div"); - ReactDOM.render( - { - // Typescript complains on two levels here. - // - Div elements don't have a value property - // - Even if it did (like it was an input element), it would have to - // be a string - // - // Which are reasonable in most web contexts. - // - // However we're using observable, neither of those things have to be - // true there. div elements can contain the value property, and can have - // the value be any datatype they wish. - // - // This is here to get the 'viewof' part of: - // viewof env = cell('normal(0,1)') - // to work - // @ts-ignore - parent.value = expr; - - parent.dispatchEvent(new CustomEvent("input")); - if (props.onChange) props.onChange(expr); - }} - />, - parent - ); + const parent = document.createElement("div"); + ReactDOM.render(, parent); return parent; } export interface SquigglePartialProps { /** The input string for squiggle */ initialSquiggleString?: string; - /** If the output requires monte carlo sampling, the amount of samples */ - environment?: environment; - /** If the result is a function, where the function starts */ - diagramStart?: number; - /** If the result is a function, where the function ends */ - diagramStop?: number; - /** If the result is a function, how many points along the function it samples */ - diagramCount?: number; /** when the environment changes. Used again for notebook magic*/ - onChange?(expr: bindings): void; + onChange?(expr: bindings | undefined): void; /** Previously declared variables */ bindings?: bindings; + /** If the output requires monte carlo sampling, the amount of samples */ + environment?: environment; /** Variables imported from js */ jsImports?: jsImports; - /** Whether to give users access to graph controls */ - showControls?: boolean; } -export let SquigglePartial: React.FC = ({ +export const SquigglePartial: React.FC = ({ initialSquiggleString = "", onChange, bindings = defaultBindings, environment, jsImports = defaultImports, }: SquigglePartialProps) => { - const [expression, setExpression] = React.useState(initialSquiggleString); - const [error, setError] = React.useState(null); + const [code, setCode] = useState(initialSquiggleString); - const runSquiggleAndUpdateBindings = () => { - const squiggleResult = runPartial( - expression, - bindings, - environment, - jsImports - ); - if (squiggleResult.tag === "Ok") { - if (onChange) onChange(squiggleResult.value); - setError(null); - } else { - setError(errorValueToString(squiggleResult.value)); - } - }; - - React.useEffect(runSquiggleAndUpdateBindings, [expression]); + const { result, observableRef } = useSquigglePartial({ + code, + bindings, + environment, + jsImports, + onChange, + }); return ( - -
-
- -
- {error !== null ? ( - {error} +
+ + + {result.tag !== "Ok" ? ( + ) : null} -
- + +
); }; export function renderSquigglePartialToDom(props: SquigglePartialProps) { - let parent = document.createElement("div"); - ReactDOM.render( - { - // @ts-ignore - parent.value = bindings; - - parent.dispatchEvent(new CustomEvent("input")); - if (props.onChange) props.onChange(bindings); - }} - />, - parent - ); + const parent = document.createElement("div"); + ReactDOM.render(, parent); return parent; } diff --git a/packages/components/src/components/SquiggleErrorAlert.tsx b/packages/components/src/components/SquiggleErrorAlert.tsx new file mode 100644 index 00000000..31d7e352 --- /dev/null +++ b/packages/components/src/components/SquiggleErrorAlert.tsx @@ -0,0 +1,11 @@ +import { errorValue, errorValueToString } from "@quri/squiggle-lang"; +import React from "react"; +import { ErrorAlert } from "./Alert"; + +type Props = { + error: errorValue; +}; + +export const SquiggleErrorAlert: React.FC = ({ error }) => { + return {errorValueToString(error)}; +}; diff --git a/packages/components/src/components/SquiggleItem.tsx b/packages/components/src/components/SquiggleItem.tsx new file mode 100644 index 00000000..0e775f33 --- /dev/null +++ b/packages/components/src/components/SquiggleItem.tsx @@ -0,0 +1,250 @@ +import * as React from "react"; +import { + squiggleExpression, + environment, + declaration, +} from "@quri/squiggle-lang"; +import { NumberShower } from "./NumberShower"; +import { DistributionChart } from "./DistributionChart"; +import { FunctionChart, FunctionChartSettings } from "./FunctionChart"; + +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, + }; +} + +interface VariableBoxProps { + heading: string; + children: React.ReactNode; + showTypes: boolean; +} + +export const VariableBox: React.FC = ({ + heading = "Error", + children, + showTypes = false, +}) => { + if (showTypes) { + return ( +
+
+
{heading}
+
+
{children}
+
+ ); + } else { + return
{children}
; + } +}; + +export interface SquiggleItemProps { + /** The input string for squiggle */ + expression: squiggleExpression; + width?: number; + height: number; + /** Whether to show a summary of statistics for distributions */ + showSummary: boolean; + /** Whether to show type information */ + showTypes: boolean; + /** Whether to show users graph controls (scale etc) */ + showControls: boolean; + /** Settings for displaying functions */ + chartSettings: FunctionChartSettings; + /** Environment for further function executions */ + environment: environment; +} + +export const SquiggleItem: React.FC = ({ + expression, + width, + height, + showSummary, + showTypes = false, + showControls = false, + chartSettings, + environment, +}) => { + switch (expression.tag) { + case "number": + return ( + +
+ +
+
+ ); + case "distribution": { + const distType = expression.value.type(); + return ( + + {distType === "Symbolic" && showTypes ? ( +
{expression.value.toString()}
+ ) : null} + +
+ ); + } + case "string": + return ( + + " + + {expression.value} + + " + + ); + case "boolean": + return ( + + {expression.value.toString()} + + ); + case "symbol": + return ( + + Undefined Symbol: + {expression.value} + + ); + case "call": + return ( + + {expression.value} + + ); + case "array": + return ( + + {expression.value.map((r, i) => ( +
+
+
{i}
+
+
+ +
+
+ ))} +
+ ); + case "record": + return ( + +
+ {Object.entries(expression.value).map(([key, r]) => ( +
+
+
{key}:
+
+
+ +
+
+ ))} +
+
+ ); + case "arraystring": + return ( + + {expression.value.map((r) => `"${r}"`).join(", ")} + + ); + case "date": + return ( + + {expression.value.toDateString()} + + ); + case "timeDuration": { + return ( + + + + ); + } + case "lambda": + return ( + +
{`function(${expression.value.parameters.join( + "," + )})`}
+ +
+ ); + case "lambdaDeclaration": { + return ( + + + + ); + } + default: { + return <>Should be unreachable; + } + } +}; diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index a0615ca4..64b56358 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -190,7 +190,7 @@ function Checkbox({ ); } -const SquigglePlayground: FC = ({ +export const SquigglePlayground: FC = ({ initialSquiggleString = "", height = 500, showTypes = false, @@ -207,9 +207,9 @@ const SquigglePlayground: FC = ({ sampleCount: 1000, xyPointLength: 1000, chartHeight: 150, - showTypes: showTypes, - showControls: showControls, - showSummary: showSummary, + showTypes, + showControls, + showSummary, leftSizePercent: 50, showSettingsPage: false, diagramStart: 0, @@ -414,9 +414,9 @@ const SquigglePlayground: FC = ({ height={vars.chartHeight} showTypes={vars.showTypes} showControls={vars.showControls} + showSummary={vars.showSummary} bindings={defaultBindings} jsImports={imports} - showSummary={vars.showSummary} /> @@ -426,7 +426,6 @@ const SquigglePlayground: FC = ({ ); }; -export default SquigglePlayground; export function renderSquigglePlaygroundToDom(props: PlaygroundProps) { const parent = document.createElement("div"); ReactDOM.render(, parent); diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 7a7751a2..de0b6dff 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -6,7 +6,7 @@ export { renderSquigglePartialToDom, } from "./components/SquiggleEditor"; export { - default as SquigglePlayground, + SquigglePlayground, renderSquigglePlaygroundToDom, } from "./components/SquigglePlayground"; export { SquiggleContainer } from "./components/SquiggleContainer"; diff --git a/packages/components/src/lib/hooks.ts b/packages/components/src/lib/hooks.ts new file mode 100644 index 00000000..42db01ce --- /dev/null +++ b/packages/components/src/lib/hooks.ts @@ -0,0 +1,63 @@ +import { + bindings, + environment, + jsImports, + run, + runPartial, +} from "@quri/squiggle-lang"; +import { useEffect, useMemo, useRef } from "react"; + +type SquiggleArgs> = { + code: string; + bindings?: bindings; + jsImports?: jsImports; + environment?: environment; + onChange?: (expr: Extract["value"] | undefined) => void; +}; + +const useSquiggleAny = >( + args: SquiggleArgs, + f: (...args: Parameters) => T +) => { + // We're using observable, where div elements can have a `value` property: + // https://observablehq.com/@observablehq/introduction-to-views + // + // This is here to get the 'viewof' part of: + // viewof env = cell('normal(0,1)') + // to work + const ref = useRef< + HTMLDivElement & { value?: Extract["value"] } + >(null); + const result: T = useMemo( + () => f(args.code, args.bindings, args.environment, args.jsImports), + [f, args.code, args.bindings, args.environment, args.jsImports] + ); + + useEffect(() => { + if (!ref.current) return; + ref.current.value = result.tag === "Ok" ? result.value : undefined; + + ref.current.dispatchEvent(new CustomEvent("input")); + }, [result]); + + const { onChange } = args; + + useEffect(() => { + onChange?.(result.tag === "Ok" ? result.value : undefined); + }, [result, onChange]); + + return { + result, // squiggleExpression or externalBindings + observableRef: ref, // can be passed to outermost
if you want to use your component as an observablehq's view + }; +}; + +export const useSquigglePartial = ( + args: SquiggleArgs> +) => { + return useSquiggleAny(args, runPartial); +}; + +export const useSquiggle = (args: SquiggleArgs>) => { + return useSquiggleAny(args, run); +};