From 9f257490e75871a374bbc43eac1a9edb9bad8d10 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Fri, 17 Jun 2022 06:55:42 -0700 Subject: [PATCH] Revert "useSquiggle and useSquigglePartial hooks - components refactorings" --- .../components/src/components/CodeEditor.tsx | 9 +- .../src/components/SquiggleChart.tsx | 285 ++++++++++++++++-- .../src/components/SquiggleEditor.tsx | 214 ++++++++----- .../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, 406 insertions(+), 439 deletions(-) delete mode 100644 packages/components/src/components/SquiggleErrorAlert.tsx delete mode 100644 packages/components/src/components/SquiggleItem.tsx delete 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 c869b2ea..5d9340b8 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, useMemo } from "react"; +import React, { FC } from "react"; import AceEditor from "react-ace"; import "ace-builds/src-noconflict/mode-golang"; @@ -21,15 +21,14 @@ export const CodeEditor: FC = ({ showGutter = false, height, }) => { - const lineCount = value.split("\n").length; - const id = useMemo(() => _.uniqueId(), []); - + let lineCount = value.split("\n").length; + let id = _.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; + } + } +}; export interface SquiggleChartProps { /** The input string for squiggle */ @@ -22,8 +266,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 squiggle code gets reevaluated */ - onChange?(expr: squiggleExpression | undefined): void; + /** When the environment changes */ + onChange?(expr: squiggleExpression): void; /** CSS width of the element */ width?: number; height?: number; @@ -31,7 +275,7 @@ export interface SquiggleChartProps { bindings?: bindings; /** JS imported parameters */ jsImports?: jsImports; - /** Whether to show a summary of the distribution */ + /** Whether to show a summary of the distirbution */ showSummary?: boolean; /** Whether to show type information about returns, default false */ showTypes?: boolean; @@ -39,13 +283,12 @@ export interface SquiggleChartProps { showControls?: boolean; } -const defaultOnChange = () => {}; const defaultChartSettings = { start: 0, stop: 10, count: 20 }; export const SquiggleChart: React.FC = ({ squiggleString = "", environment, - onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here + onChange = () => {}, height = 200, bindings = defaultBindings, jsImports = defaultImports, @@ -55,28 +298,28 @@ export const SquiggleChart: React.FC = ({ showControls = false, chartSettings = defaultChartSettings, }) => { - const { result } = useSquiggle({ - code: squiggleString, - bindings, - environment, - jsImports, - onChange, - }); - - if (result.tag !== "Ok") { - return ; + let expressionResult = run(squiggleString, bindings, environment, jsImports); + if (expressionResult.tag !== "Ok") { + return ( + + {errorValueToString(expressionResult.value)} + + ); } + 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 279d6ec6..a8db78af 100644 --- a/packages/components/src/components/SquiggleEditor.tsx +++ b/packages/components/src/components/SquiggleEditor.tsx @@ -1,51 +1,39 @@ -import React, { useState } from "react"; +import * as React from "react"; import * as ReactDOM from "react-dom"; +import { SquiggleChart } from "./SquiggleChart"; import { CodeEditor } from "./CodeEditor"; -import { +import type { squiggleExpression, environment, bindings, jsImports, - defaultEnvironment, } from "@quri/squiggle-lang"; -import { defaultImports, defaultBindings } from "@quri/squiggle-lang"; +import { + runPartial, + errorValueToString, + defaultImports, + defaultBindings, +} from "@quri/squiggle-lang"; +import { ErrorAlert } from "./Alert"; 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; - /** The width of the element */ - width?: number; + /** 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: squiggleExpression | undefined): void; + /** when the environment changes. Used again for notebook magic*/ + onChange?(expr: squiggleExpression): void; + /** The width of the element */ + width?: number; /** 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 */ @@ -56,109 +44,169 @@ export interface SquiggleEditorProps { showSummary?: boolean; } -export const SquiggleEditor: React.FC = ({ +export let 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 [code, setCode] = useState(initialSquiggleString); - - const { result, observableRef } = useSquiggle({ - code, - bindings, - environment, - jsImports, - onChange, - }); - + const [expression, setExpression] = React.useState(initialSquiggleString); const chartSettings = { start: diagramStart, stop: diagramStop, count: diagramCount, }; - return ( -
- - - {result.tag === "Ok" ? ( - +
+
+ - ) : ( - - )} - -
+
+ +
+ ); }; export function renderSquiggleEditorToDom(props: SquiggleEditorProps) { - const parent = document.createElement("div"); - ReactDOM.render(, parent); + 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 + ); return parent; } export interface SquigglePartialProps { /** The input string for squiggle */ initialSquiggleString?: string; - /** when the environment changes. Used again for notebook magic*/ - onChange?(expr: bindings | undefined): void; - /** Previously declared variables */ - bindings?: bindings; /** 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; + /** Previously declared variables */ + bindings?: bindings; /** Variables imported from js */ jsImports?: jsImports; + /** Whether to give users access to graph controls */ + showControls?: boolean; } -export const SquigglePartial: React.FC = ({ +export let SquigglePartial: React.FC = ({ initialSquiggleString = "", onChange, bindings = defaultBindings, environment, jsImports = defaultImports, }: SquigglePartialProps) => { - const [code, setCode] = useState(initialSquiggleString); + const [expression, setExpression] = React.useState(initialSquiggleString); + const [error, setError] = React.useState(null); - const { result, observableRef } = useSquigglePartial({ - code, - bindings, - environment, - jsImports, - onChange, - }); + 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]); return ( -
- - - {result.tag !== "Ok" ? ( - + +
+
+ +
+ {error !== null ? ( + {error} ) : null} - -
+
+ ); }; export function renderSquigglePartialToDom(props: SquigglePartialProps) { - const parent = document.createElement("div"); - ReactDOM.render(, parent); + let parent = document.createElement("div"); + ReactDOM.render( + { + // @ts-ignore + parent.value = bindings; + + parent.dispatchEvent(new CustomEvent("input")); + if (props.onChange) props.onChange(bindings); + }} + />, + parent + ); return parent; } diff --git a/packages/components/src/components/SquiggleErrorAlert.tsx b/packages/components/src/components/SquiggleErrorAlert.tsx deleted file mode 100644 index 31d7e352..00000000 --- a/packages/components/src/components/SquiggleErrorAlert.tsx +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 0e775f33..00000000 --- a/packages/components/src/components/SquiggleItem.tsx +++ /dev/null @@ -1,250 +0,0 @@ -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 64b56358..a0615ca4 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -190,7 +190,7 @@ function Checkbox({ ); } -export const SquigglePlayground: FC = ({ +const SquigglePlayground: FC = ({ initialSquiggleString = "", height = 500, showTypes = false, @@ -207,9 +207,9 @@ export const SquigglePlayground: FC = ({ sampleCount: 1000, xyPointLength: 1000, chartHeight: 150, - showTypes, - showControls, - showSummary, + showTypes: showTypes, + showControls: showControls, + showSummary: showSummary, leftSizePercent: 50, showSettingsPage: false, diagramStart: 0, @@ -414,9 +414,9 @@ export const SquigglePlayground: FC = ({ height={vars.chartHeight} showTypes={vars.showTypes} showControls={vars.showControls} - showSummary={vars.showSummary} bindings={defaultBindings} jsImports={imports} + showSummary={vars.showSummary} /> @@ -426,6 +426,7 @@ export 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 de0b6dff..7a7751a2 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -6,7 +6,7 @@ export { renderSquigglePartialToDom, } from "./components/SquiggleEditor"; export { - SquigglePlayground, + default as 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 deleted file mode 100644 index 42db01ce..00000000 --- a/packages/components/src/lib/hooks.ts +++ /dev/null @@ -1,63 +0,0 @@ -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); -};