Merge pull request #702 from quantified-uncertainty/components-hooks

useSquiggle and useSquigglePartial hooks - components refactorings
This commit is contained in:
Ozzie Gooen 2022-06-17 06:49:32 -07:00 committed by GitHub
commit feda8997a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 438 additions and 405 deletions

View File

@ -1,5 +1,5 @@
import _ from "lodash"; import _ from "lodash";
import React, { FC } from "react"; import React, { FC, useMemo } from "react";
import AceEditor from "react-ace"; import AceEditor from "react-ace";
import "ace-builds/src-noconflict/mode-golang"; import "ace-builds/src-noconflict/mode-golang";
@ -21,14 +21,15 @@ export const CodeEditor: FC<CodeEditorProps> = ({
showGutter = false, showGutter = false,
height, height,
}) => { }) => {
let lineCount = value.split("\n").length; const lineCount = value.split("\n").length;
let id = _.uniqueId(); const id = useMemo(() => _.uniqueId(), []);
return ( return (
<AceEditor <AceEditor
value={value} value={value}
mode="golang" mode="golang"
theme="github" theme="github"
width={"100%"} width="100%"
fontSize={14} fontSize={14}
height={String(height) + "px"} height={String(height) + "px"}
minLines={oneLine ? lineCount : undefined} minLines={oneLine ? lineCount : undefined}

View File

@ -1,7 +1,5 @@
import * as React from "react"; import * as React from "react";
import { import {
run,
errorValueToString,
squiggleExpression, squiggleExpression,
bindings, bindings,
environment, environment,
@ -9,253 +7,11 @@ import {
defaultImports, defaultImports,
defaultBindings, defaultBindings,
defaultEnvironment, defaultEnvironment,
declaration,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { NumberShower } from "./NumberShower"; import { FunctionChartSettings } from "./FunctionChart";
import { DistributionChart } from "./DistributionChart"; import { useSquiggle } from "../lib/hooks";
import { ErrorAlert } from "./Alert"; import { SquiggleErrorAlert } from "./SquiggleErrorAlert";
import { FunctionChart, FunctionChartSettings } from "./FunctionChart"; import { SquiggleItem } from "./SquiggleItem";
function getRange<a>(x: declaration<a>) {
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<a>(x: declaration<a>): 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<VariableBoxProps> = ({
heading = "Error",
children,
showTypes = false,
}) => {
if (showTypes) {
return (
<div className="bg-white border border-grey-200 m-2">
<div className="border-b border-grey-200 p-3">
<header className="font-mono">{heading}</header>
</div>
<div className="p-3">{children}</div>
</div>
);
} else {
return <div>{children}</div>;
}
};
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<SquiggleItemProps> = ({
expression,
width,
height,
showSummary,
showTypes = false,
showControls = false,
chartSettings,
environment,
}) => {
switch (expression.tag) {
case "number":
return (
<VariableBox heading="Number" showTypes={showTypes}>
<div className="font-semibold text-slate-600">
<NumberShower precision={3} number={expression.value} />
</div>
</VariableBox>
);
case "distribution": {
let distType = expression.value.type();
return (
<VariableBox
heading={`Distribution (${distType})`}
showTypes={showTypes}
>
{distType === "Symbolic" && showTypes ? (
<div>{expression.value.toString()}</div>
) : null}
<DistributionChart
distribution={expression.value}
height={height}
width={width}
showSummary={showSummary}
showControls={showControls}
/>
</VariableBox>
);
}
case "string":
return (
<VariableBox heading="String" showTypes={showTypes}>
<span className="text-slate-400">"</span>
<span className="text-slate-600 font-semibold">
{expression.value}
</span>
<span className="text-slate-400">"</span>
</VariableBox>
);
case "boolean":
return (
<VariableBox heading="Boolean" showTypes={showTypes}>
{expression.value.toString()}
</VariableBox>
);
case "symbol":
return (
<VariableBox heading="Symbol" showTypes={showTypes}>
<span className="text-slate-500 mr-2">Undefined Symbol:</span>
<span className="text-slate-600">{expression.value}</span>
</VariableBox>
);
case "call":
return (
<VariableBox heading="Call" showTypes={showTypes}>
{expression.value}
</VariableBox>
);
case "array":
return (
<VariableBox heading="Array" showTypes={showTypes}>
{expression.value.map((r, i) => (
<div key={i} className="flex pt-1">
<div className="flex-none bg-slate-100 rounded-sm px-1">
<header className="text-slate-400 font-mono">{i}</header>
</div>
<div className="px-2 mb-2 grow">
<SquiggleItem
key={i}
expression={r}
width={width !== undefined ? width - 20 : width}
height={50}
showTypes={showTypes}
showControls={showControls}
chartSettings={chartSettings}
environment={environment}
showSummary={showSummary}
/>
</div>
</div>
))}
</VariableBox>
);
case "record":
return (
<VariableBox heading="Record" showTypes={showTypes}>
<div className="space-y-3">
{Object.entries(expression.value).map(([key, r]) => (
<div key={key} className="flex space-x-2">
<div className="flex-none">
<header className="text-slate-500 font-mono">{key}:</header>
</div>
<div className="px-2 grow bg-gray-50 border border-gray-100 rounded-sm">
<SquiggleItem
expression={r}
width={width !== undefined ? width - 20 : width}
height={height / 3}
showTypes={showTypes}
showSummary={showSummary}
showControls={showControls}
chartSettings={chartSettings}
environment={environment}
/>
</div>
</div>
))}
</div>
</VariableBox>
);
case "arraystring":
return (
<VariableBox heading="Array String" showTypes={showTypes}>
{expression.value.map((r) => `"${r}"`).join(", ")}
</VariableBox>
);
case "date":
return (
<VariableBox heading="Date" showTypes={showTypes}>
{expression.value.toDateString()}
</VariableBox>
);
case "timeDuration": {
return (
<VariableBox heading="Time Duration" showTypes={showTypes}>
<NumberShower precision={3} number={expression.value} />
</VariableBox>
);
}
case "lambda":
return (
<VariableBox heading="Function" showTypes={showTypes}>
<div className="text-amber-700 bg-amber-100 rounded-md font-mono p-1 pl-2 mb-3 mt-1 text-sm">{`function(${expression.value.parameters.join(
","
)})`}</div>
<FunctionChart
fn={expression.value}
chartSettings={chartSettings}
height={height}
environment={{
sampleCount: environment.sampleCount / 10,
xyPointLength: environment.xyPointLength / 10,
}}
/>
</VariableBox>
);
case "lambdaDeclaration": {
return (
<VariableBox heading="Function Declaration" showTypes={showTypes}>
<FunctionChart
fn={expression.value.fn}
chartSettings={getChartSettings(expression.value)}
height={height}
environment={{
sampleCount: environment.sampleCount / 10,
xyPointLength: environment.xyPointLength / 10,
}}
/>
</VariableBox>
);
}
default: {
return <>Should be unreachable</>;
}
}
};
export interface SquiggleChartProps { export interface SquiggleChartProps {
/** The input string for squiggle */ /** The input string for squiggle */
@ -266,8 +22,8 @@ export interface SquiggleChartProps {
environment?: environment; environment?: environment;
/** If the result is a function, where the function starts, ends and the amount of stops */ /** If the result is a function, where the function starts, ends and the amount of stops */
chartSettings?: FunctionChartSettings; chartSettings?: FunctionChartSettings;
/** When the environment changes */ /** When the squiggle code gets reevaluated */
onChange?(expr: squiggleExpression): void; onChange?(expr: squiggleExpression | undefined): void;
/** CSS width of the element */ /** CSS width of the element */
width?: number; width?: number;
height?: number; height?: number;
@ -275,7 +31,7 @@ export interface SquiggleChartProps {
bindings?: bindings; bindings?: bindings;
/** JS imported parameters */ /** JS imported parameters */
jsImports?: jsImports; jsImports?: jsImports;
/** Whether to show a summary of the distirbution */ /** Whether to show a summary of the distribution */
showSummary?: boolean; showSummary?: boolean;
/** Whether to show type information about returns, default false */ /** Whether to show type information about returns, default false */
showTypes?: boolean; showTypes?: boolean;
@ -283,12 +39,13 @@ export interface SquiggleChartProps {
showControls?: boolean; showControls?: boolean;
} }
const defaultOnChange = () => {};
const defaultChartSettings = { start: 0, stop: 10, count: 20 }; const defaultChartSettings = { start: 0, stop: 10, count: 20 };
export const SquiggleChart: React.FC<SquiggleChartProps> = ({ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
squiggleString = "", squiggleString = "",
environment, environment,
onChange = () => {}, onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
height = 200, height = 200,
bindings = defaultBindings, bindings = defaultBindings,
jsImports = defaultImports, jsImports = defaultImports,
@ -298,28 +55,28 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = ({
showControls = false, showControls = false,
chartSettings = defaultChartSettings, chartSettings = defaultChartSettings,
}) => { }) => {
let expressionResult = run(squiggleString, bindings, environment, jsImports); const { result } = useSquiggle({
if (expressionResult.tag !== "Ok") { code: squiggleString,
return ( bindings,
<ErrorAlert heading={"Parse Error"}> environment,
{errorValueToString(expressionResult.value)} jsImports,
</ErrorAlert> onChange,
); });
if (result.tag !== "Ok") {
return <SquiggleErrorAlert error={result.value} />;
} }
let e = environment ?? defaultEnvironment;
let expression = expressionResult.value;
onChange(expression);
return ( return (
<SquiggleItem <SquiggleItem
expression={expression} expression={result.value}
width={width} width={width}
height={height} height={height}
showSummary={showSummary} showSummary={showSummary}
showTypes={showTypes} showTypes={showTypes}
showControls={showControls} showControls={showControls}
chartSettings={chartSettings} chartSettings={chartSettings}
environment={e} environment={environment ?? defaultEnvironment}
/> />
); );
}; };

View File

@ -1,39 +1,51 @@
import * as React from "react"; import React, { useState } from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import { SquiggleChart } from "./SquiggleChart";
import { CodeEditor } from "./CodeEditor"; import { CodeEditor } from "./CodeEditor";
import type { import {
squiggleExpression, squiggleExpression,
environment, environment,
bindings, bindings,
jsImports, jsImports,
defaultEnvironment,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { import { defaultImports, defaultBindings } from "@quri/squiggle-lang";
runPartial,
errorValueToString,
defaultImports,
defaultBindings,
} from "@quri/squiggle-lang";
import { ErrorAlert } from "./Alert";
import { SquiggleContainer } from "./SquiggleContainer"; 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 }) => (
<div className="border border-grey-200 p-2 m-4">
<CodeEditor
value={code}
onChange={setCode}
oneLine={true}
showGutter={false}
height={20}
/>
</div>
);
export interface SquiggleEditorProps { export interface SquiggleEditorProps {
/** The input string for squiggle */ /** The input string for squiggle */
initialSquiggleString?: string; initialSquiggleString?: string;
/** If the output requires monte carlo sampling, the amount of samples */ /** The width of the element */
environment?: environment; width?: number;
/** If the result is a function, where the function starts */ /** If the result is a function, where the function starts */
diagramStart?: number; diagramStart?: number;
/** If the result is a function, where the function ends */ /** If the result is a function, where the function ends */
diagramStop?: number; diagramStop?: number;
/** If the result is a function, how many points along the function it samples */ /** If the result is a function, how many points along the function it samples */
diagramCount?: number; diagramCount?: number;
/** when the environment changes. Used again for notebook magic*/ /** When the environment changes. Used again for notebook magic */
onChange?(expr: squiggleExpression): void; onChange?(expr: squiggleExpression | undefined): void;
/** The width of the element */
width?: number;
/** Previous variable declarations */ /** Previous variable declarations */
bindings?: bindings; bindings?: bindings;
/** If the output requires monte carlo sampling, the amount of samples */
environment?: environment;
/** JS Imports */ /** JS Imports */
jsImports?: jsImports; jsImports?: jsImports;
/** Whether to show detail about types of the returns, default false */ /** Whether to show detail about types of the returns, default false */
@ -44,169 +56,109 @@ export interface SquiggleEditorProps {
showSummary?: boolean; showSummary?: boolean;
} }
export let SquiggleEditor: React.FC<SquiggleEditorProps> = ({ export const SquiggleEditor: React.FC<SquiggleEditorProps> = ({
initialSquiggleString = "", initialSquiggleString = "",
width, width,
environment,
diagramStart = 0, diagramStart = 0,
diagramStop = 10, diagramStop = 10,
diagramCount = 20, diagramCount = 20,
onChange, onChange,
bindings = defaultBindings, bindings = defaultBindings,
environment,
jsImports = defaultImports, jsImports = defaultImports,
showTypes = false, showTypes = false,
showControls = false, showControls = false,
showSummary = false, showSummary = false,
}: SquiggleEditorProps) => { }: SquiggleEditorProps) => {
const [expression, setExpression] = React.useState(initialSquiggleString); const [code, setCode] = useState(initialSquiggleString);
const { result, observableRef } = useSquiggle({
code,
bindings,
environment,
jsImports,
onChange,
});
const chartSettings = { const chartSettings = {
start: diagramStart, start: diagramStart,
stop: diagramStop, stop: diagramStop,
count: diagramCount, count: diagramCount,
}; };
return ( return (
<SquiggleContainer> <div ref={observableRef}>
<div> <SquiggleContainer>
<div className="border border-grey-200 p-2 m-4"> <WrappedCodeEditor code={code} setCode={setCode} />
<CodeEditor {result.tag === "Ok" ? (
value={expression} <SquiggleItem
onChange={setExpression} expression={result.value}
oneLine={true} width={width}
showGutter={false} height={200}
height={20} showSummary={showSummary}
showTypes={showTypes}
showControls={showControls}
chartSettings={chartSettings}
environment={environment ?? defaultEnvironment}
/> />
</div> ) : (
<SquiggleChart <SquiggleErrorAlert error={result.value} />
width={width} )}
environment={environment} </SquiggleContainer>
squiggleString={expression} </div>
chartSettings={chartSettings}
onChange={onChange}
bindings={bindings}
jsImports={jsImports}
showTypes={showTypes}
showControls={showControls}
showSummary={showSummary}
/>
</div>
</SquiggleContainer>
); );
}; };
export function renderSquiggleEditorToDom(props: SquiggleEditorProps) { export function renderSquiggleEditorToDom(props: SquiggleEditorProps) {
let parent = document.createElement("div"); const parent = document.createElement("div");
ReactDOM.render( ReactDOM.render(<SquiggleEditor {...props} />, parent);
<SquiggleEditor
{...props}
onChange={(expr) => {
// 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; return parent;
} }
export interface SquigglePartialProps { export interface SquigglePartialProps {
/** The input string for squiggle */ /** The input string for squiggle */
initialSquiggleString?: string; 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*/ /** when the environment changes. Used again for notebook magic*/
onChange?(expr: bindings): void; onChange?(expr: bindings | undefined): void;
/** Previously declared variables */ /** Previously declared variables */
bindings?: bindings; bindings?: bindings;
/** If the output requires monte carlo sampling, the amount of samples */
environment?: environment;
/** Variables imported from js */ /** Variables imported from js */
jsImports?: jsImports; jsImports?: jsImports;
/** Whether to give users access to graph controls */
showControls?: boolean;
} }
export let SquigglePartial: React.FC<SquigglePartialProps> = ({ export const SquigglePartial: React.FC<SquigglePartialProps> = ({
initialSquiggleString = "", initialSquiggleString = "",
onChange, onChange,
bindings = defaultBindings, bindings = defaultBindings,
environment, environment,
jsImports = defaultImports, jsImports = defaultImports,
}: SquigglePartialProps) => { }: SquigglePartialProps) => {
const [expression, setExpression] = React.useState(initialSquiggleString); const [code, setCode] = useState(initialSquiggleString);
const [error, setError] = React.useState<string | null>(null);
const runSquiggleAndUpdateBindings = () => { const { result, observableRef } = useSquigglePartial({
const squiggleResult = runPartial( code,
expression, bindings,
bindings, environment,
environment, jsImports,
jsImports onChange,
); });
if (squiggleResult.tag === "Ok") {
if (onChange) onChange(squiggleResult.value);
setError(null);
} else {
setError(errorValueToString(squiggleResult.value));
}
};
React.useEffect(runSquiggleAndUpdateBindings, [expression]);
return ( return (
<SquiggleContainer> <div ref={observableRef}>
<div> <SquiggleContainer>
<div className="border border-grey-200 p-2 m-4"> <WrappedCodeEditor code={code} setCode={setCode} />
<CodeEditor {result.tag !== "Ok" ? (
value={expression} <SquiggleErrorAlert error={result.value} />
onChange={setExpression}
oneLine={true}
showGutter={false}
height={20}
/>
</div>
{error !== null ? (
<ErrorAlert heading="Error">{error}</ErrorAlert>
) : null} ) : null}
</div> </SquiggleContainer>
</SquiggleContainer> </div>
); );
}; };
export function renderSquigglePartialToDom(props: SquigglePartialProps) { export function renderSquigglePartialToDom(props: SquigglePartialProps) {
let parent = document.createElement("div"); const parent = document.createElement("div");
ReactDOM.render( ReactDOM.render(<SquigglePartial {...props} />, parent);
<SquigglePartial
{...props}
onChange={(bindings) => {
// @ts-ignore
parent.value = bindings;
parent.dispatchEvent(new CustomEvent("input"));
if (props.onChange) props.onChange(bindings);
}}
/>,
parent
);
return parent; return parent;
} }

View File

@ -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<Props> = ({ error }) => {
return <ErrorAlert heading="Error">{errorValueToString(error)}</ErrorAlert>;
};

View File

@ -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<a>(x: declaration<a>) {
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<a>(x: declaration<a>): 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<VariableBoxProps> = ({
heading = "Error",
children,
showTypes = false,
}) => {
if (showTypes) {
return (
<div className="bg-white border border-grey-200 m-2">
<div className="border-b border-grey-200 p-3">
<header className="font-mono">{heading}</header>
</div>
<div className="p-3">{children}</div>
</div>
);
} else {
return <div>{children}</div>;
}
};
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<SquiggleItemProps> = ({
expression,
width,
height,
showSummary,
showTypes = false,
showControls = false,
chartSettings,
environment,
}) => {
switch (expression.tag) {
case "number":
return (
<VariableBox heading="Number" showTypes={showTypes}>
<div className="font-semibold text-slate-600">
<NumberShower precision={3} number={expression.value} />
</div>
</VariableBox>
);
case "distribution": {
const distType = expression.value.type();
return (
<VariableBox
heading={`Distribution (${distType})`}
showTypes={showTypes}
>
{distType === "Symbolic" && showTypes ? (
<div>{expression.value.toString()}</div>
) : null}
<DistributionChart
distribution={expression.value}
height={height}
width={width}
showSummary={showSummary}
showControls={showControls}
/>
</VariableBox>
);
}
case "string":
return (
<VariableBox heading="String" showTypes={showTypes}>
<span className="text-slate-400">"</span>
<span className="text-slate-600 font-semibold">
{expression.value}
</span>
<span className="text-slate-400">"</span>
</VariableBox>
);
case "boolean":
return (
<VariableBox heading="Boolean" showTypes={showTypes}>
{expression.value.toString()}
</VariableBox>
);
case "symbol":
return (
<VariableBox heading="Symbol" showTypes={showTypes}>
<span className="text-slate-500 mr-2">Undefined Symbol:</span>
<span className="text-slate-600">{expression.value}</span>
</VariableBox>
);
case "call":
return (
<VariableBox heading="Call" showTypes={showTypes}>
{expression.value}
</VariableBox>
);
case "array":
return (
<VariableBox heading="Array" showTypes={showTypes}>
{expression.value.map((r, i) => (
<div key={i} className="flex pt-1">
<div className="flex-none bg-slate-100 rounded-sm px-1">
<header className="text-slate-400 font-mono">{i}</header>
</div>
<div className="px-2 mb-2 grow">
<SquiggleItem
key={i}
expression={r}
width={width !== undefined ? width - 20 : width}
height={50}
showTypes={showTypes}
showControls={showControls}
chartSettings={chartSettings}
environment={environment}
showSummary={showSummary}
/>
</div>
</div>
))}
</VariableBox>
);
case "record":
return (
<VariableBox heading="Record" showTypes={showTypes}>
<div className="space-y-3">
{Object.entries(expression.value).map(([key, r]) => (
<div key={key} className="flex space-x-2">
<div className="flex-none">
<header className="text-slate-500 font-mono">{key}:</header>
</div>
<div className="px-2 grow bg-gray-50 border border-gray-100 rounded-sm">
<SquiggleItem
expression={r}
width={width !== undefined ? width - 20 : width}
height={height / 3}
showTypes={showTypes}
showSummary={showSummary}
showControls={showControls}
chartSettings={chartSettings}
environment={environment}
/>
</div>
</div>
))}
</div>
</VariableBox>
);
case "arraystring":
return (
<VariableBox heading="Array String" showTypes={showTypes}>
{expression.value.map((r) => `"${r}"`).join(", ")}
</VariableBox>
);
case "date":
return (
<VariableBox heading="Date" showTypes={showTypes}>
{expression.value.toDateString()}
</VariableBox>
);
case "timeDuration": {
return (
<VariableBox heading="Time Duration" showTypes={showTypes}>
<NumberShower precision={3} number={expression.value} />
</VariableBox>
);
}
case "lambda":
return (
<VariableBox heading="Function" showTypes={showTypes}>
<div className="text-amber-700 bg-amber-100 rounded-md font-mono p-1 pl-2 mb-3 mt-1 text-sm">{`function(${expression.value.parameters.join(
","
)})`}</div>
<FunctionChart
fn={expression.value}
chartSettings={chartSettings}
height={height}
environment={{
sampleCount: environment.sampleCount / 10,
xyPointLength: environment.xyPointLength / 10,
}}
/>
</VariableBox>
);
case "lambdaDeclaration": {
return (
<VariableBox heading="Function Declaration" showTypes={showTypes}>
<FunctionChart
fn={expression.value.fn}
chartSettings={getChartSettings(expression.value)}
height={height}
environment={{
sampleCount: environment.sampleCount / 10,
xyPointLength: environment.xyPointLength / 10,
}}
/>
</VariableBox>
);
}
default: {
return <>Should be unreachable</>;
}
}
};

View File

@ -190,7 +190,7 @@ function Checkbox<T>({
); );
} }
const SquigglePlayground: FC<PlaygroundProps> = ({ export const SquigglePlayground: FC<PlaygroundProps> = ({
initialSquiggleString = "", initialSquiggleString = "",
height = 500, height = 500,
showTypes = false, showTypes = false,
@ -207,9 +207,9 @@ const SquigglePlayground: FC<PlaygroundProps> = ({
sampleCount: 1000, sampleCount: 1000,
xyPointLength: 1000, xyPointLength: 1000,
chartHeight: 150, chartHeight: 150,
showTypes: showTypes, showTypes,
showControls: showControls, showControls,
showSummary: showSummary, showSummary,
leftSizePercent: 50, leftSizePercent: 50,
showSettingsPage: false, showSettingsPage: false,
diagramStart: 0, diagramStart: 0,
@ -414,9 +414,9 @@ const SquigglePlayground: FC<PlaygroundProps> = ({
height={vars.chartHeight} height={vars.chartHeight}
showTypes={vars.showTypes} showTypes={vars.showTypes}
showControls={vars.showControls} showControls={vars.showControls}
showSummary={vars.showSummary}
bindings={defaultBindings} bindings={defaultBindings}
jsImports={imports} jsImports={imports}
showSummary={vars.showSummary}
/> />
</div> </div>
</div> </div>
@ -426,7 +426,6 @@ const SquigglePlayground: FC<PlaygroundProps> = ({
); );
}; };
export default SquigglePlayground;
export function renderSquigglePlaygroundToDom(props: PlaygroundProps) { export function renderSquigglePlaygroundToDom(props: PlaygroundProps) {
const parent = document.createElement("div"); const parent = document.createElement("div");
ReactDOM.render(<SquigglePlayground {...props} />, parent); ReactDOM.render(<SquigglePlayground {...props} />, parent);

View File

@ -6,7 +6,7 @@ export {
renderSquigglePartialToDom, renderSquigglePartialToDom,
} from "./components/SquiggleEditor"; } from "./components/SquiggleEditor";
export { export {
default as SquigglePlayground, SquigglePlayground,
renderSquigglePlaygroundToDom, renderSquigglePlaygroundToDom,
} from "./components/SquigglePlayground"; } from "./components/SquigglePlayground";
export { SquiggleContainer } from "./components/SquiggleContainer"; export { SquiggleContainer } from "./components/SquiggleContainer";

View File

@ -0,0 +1,63 @@
import {
bindings,
environment,
jsImports,
run,
runPartial,
} from "@quri/squiggle-lang";
import { useEffect, useMemo, useRef } from "react";
type SquiggleArgs<T extends ReturnType<typeof run | typeof runPartial>> = {
code: string;
bindings?: bindings;
jsImports?: jsImports;
environment?: environment;
onChange?: (expr: Extract<T, { tag: "Ok" }>["value"] | undefined) => void;
};
const useSquiggleAny = <T extends ReturnType<typeof run | typeof runPartial>>(
args: SquiggleArgs<T>,
f: (...args: Parameters<typeof run>) => 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<T, { tag: "Ok" }>["value"] }
>(null);
const result: T = useMemo<T>(
() => 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 <div> if you want to use your component as an observablehq's view
};
};
export const useSquigglePartial = (
args: SquiggleArgs<ReturnType<typeof runPartial>>
) => {
return useSquiggleAny(args, runPartial);
};
export const useSquiggle = (args: SquiggleArgs<ReturnType<typeof run>>) => {
return useSquiggleAny(args, run);
};