SquiggleViewer refactoring; persistent collapsed settings via context

This commit is contained in:
Vyacheslav Matyukhin 2022-07-08 18:28:12 +04:00
parent 3ca80daee8
commit 12eb63c789
No known key found for this signature in database
GPG Key ID: 3D2A774C5489F96C
6 changed files with 200 additions and 93 deletions

View File

@ -10,7 +10,7 @@ import {
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { useSquiggle } from "../lib/hooks"; import { useSquiggle } from "../lib/hooks";
import { SquiggleErrorAlert } from "./SquiggleErrorAlert"; import { SquiggleErrorAlert } from "./SquiggleErrorAlert";
import { SquiggleItem } from "./SquiggleItem"; import { SquiggleViewer } from "./SquiggleViewer";
export interface SquiggleChartProps { export interface SquiggleChartProps {
/** The input string for squiggle */ /** The input string for squiggle */
@ -71,26 +71,22 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
onChange, onChange,
}); });
if (result.tag !== "Ok") { const distributionPlotSettings = {
return <SquiggleErrorAlert error={result.value} />;
}
let distributionPlotSettings = {
showControls, showControls,
showSummary, showSummary,
logX, logX,
expY, expY,
}; };
let chartSettings = { const chartSettings = {
start: diagramStart, start: diagramStart,
stop: diagramStop, stop: diagramStop,
count: diagramCount, count: diagramCount,
}; };
return ( return (
<SquiggleItem <SquiggleViewer
expression={result.value} result={result}
width={width} width={width}
height={height} height={height}
distributionPlotSettings={distributionPlotSettings} distributionPlotSettings={distributionPlotSettings}

View File

@ -1,17 +1,17 @@
import React, { useState } from "react"; import React from "react";
import { import {
squiggleExpression, squiggleExpression,
environment, environment,
declaration, declaration,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { NumberShower } from "./NumberShower"; import { NumberShower } from "../NumberShower";
import { import {
DistributionChart, DistributionChart,
DistributionPlottingSettings, DistributionPlottingSettings,
} from "./DistributionChart"; } from "../DistributionChart";
import { FunctionChart, FunctionChartSettings } from "./FunctionChart"; import { FunctionChart, FunctionChartSettings } from "../FunctionChart";
import clsx from "clsx"; import clsx from "clsx";
import { Tooltip } from "./ui/Tooltip"; import { VariableBox } from "./VariableBox";
function getRange<a>(x: declaration<a>) { function getRange<a>(x: declaration<a>) {
const first = x.args[0]; const first = x.args[0];
@ -36,73 +36,23 @@ function getChartSettings<a>(x: declaration<a>): FunctionChartSettings {
}; };
} }
interface VariableBoxProps {
name?: string;
heading: string;
children: React.ReactNode;
}
const VariableBox: React.FC<VariableBoxProps> = ({
name,
heading = "Error",
children,
}) => {
const [isCollapsed, setIsCollapsed] = useState(false);
const toggleCollapsed = () => {
setIsCollapsed(!isCollapsed);
};
return (
<div>
<div>
{name ? (
<header
className="inline-flex space-x-1 text-slate-500 font-mono text-sm cursor-pointer"
onClick={toggleCollapsed}
>
{name ? (
<Tooltip text={heading}>
<span>{name}:</span>
</Tooltip>
) : null}
{isCollapsed ? (
<span className="bg-slate-200 rounded p-0.5 font-xs">...</span>
) : null}
</header>
) : null}
{isCollapsed ? null : (
<div className="flex w-full">
{name ? (
<div
className="border-l-2 border-slate-200 hover:border-green-600 w-4 cursor-pointer"
onClick={toggleCollapsed}
></div>
) : null}
<div className="grow">{children}</div>
</div>
)}
</div>
</div>
);
};
const VariableList: React.FC<{ const VariableList: React.FC<{
name?: string; path: string[];
heading: string; heading: string;
children: React.ReactNode; children: React.ReactNode;
}> = ({ name, heading, children }) => ( }> = ({ path, heading, children }) => (
<VariableBox name={name} heading={heading}> <VariableBox path={path} heading={heading}>
<div className={clsx("space-y-3", name ? "pt-1 mt-1" : null)}> <div className={clsx("space-y-3", path.length ? "pt-1 mt-1" : null)}>
{children} {children}
</div> </div>
</VariableBox> </VariableBox>
); );
export interface SquiggleItemProps { export interface Props {
/** The output of squiggle's run */ /** The output of squiggle's run */
expression: squiggleExpression; expression: squiggleExpression;
name?: string; /** 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; width?: number;
height: number; height: number;
distributionPlotSettings: DistributionPlottingSettings; distributionPlotSettings: DistributionPlottingSettings;
@ -112,8 +62,8 @@ export interface SquiggleItemProps {
environment: environment; environment: environment;
} }
export const SquiggleItem: React.FC<SquiggleItemProps> = ({ export const ExpressionViewer: React.FC<Props> = ({
name, path,
expression, expression,
width, width,
height, height,
@ -124,7 +74,7 @@ export const SquiggleItem: React.FC<SquiggleItemProps> = ({
switch (expression.tag) { switch (expression.tag) {
case "number": case "number":
return ( return (
<VariableBox name={name} heading="Number"> <VariableBox path={path} heading="Number">
<div className="font-semibold text-slate-600"> <div className="font-semibold text-slate-600">
<NumberShower precision={3} number={expression.value} /> <NumberShower precision={3} number={expression.value} />
</div> </div>
@ -134,7 +84,7 @@ export const SquiggleItem: React.FC<SquiggleItemProps> = ({
const distType = expression.value.type(); const distType = expression.value.type();
return ( return (
<VariableBox <VariableBox
name={name} path={path}
heading={`Distribution (${distType})\n${ heading={`Distribution (${distType})\n${
distType === "Symbolic" ? expression.value.toString() : "" distType === "Symbolic" ? expression.value.toString() : ""
}`} }`}
@ -150,7 +100,7 @@ export const SquiggleItem: React.FC<SquiggleItemProps> = ({
} }
case "string": case "string":
return ( return (
<VariableBox name={name} heading="String"> <VariableBox path={path} heading="String">
<span className="text-slate-400">"</span> <span className="text-slate-400">"</span>
<span className="text-slate-600 font-semibold font-mono"> <span className="text-slate-600 font-semibold font-mono">
{expression.value} {expression.value}
@ -160,45 +110,45 @@ export const SquiggleItem: React.FC<SquiggleItemProps> = ({
); );
case "boolean": case "boolean":
return ( return (
<VariableBox name={name} heading="Boolean"> <VariableBox path={path} heading="Boolean">
{expression.value.toString()} {expression.value.toString()}
</VariableBox> </VariableBox>
); );
case "symbol": case "symbol":
return ( return (
<VariableBox name={name} heading="Symbol"> <VariableBox path={path} heading="Symbol">
<span className="text-slate-500 mr-2">Undefined Symbol:</span> <span className="text-slate-500 mr-2">Undefined Symbol:</span>
<span className="text-slate-600">{expression.value}</span> <span className="text-slate-600">{expression.value}</span>
</VariableBox> </VariableBox>
); );
case "call": case "call":
return ( return (
<VariableBox name={name} heading="Call"> <VariableBox path={path} heading="Call">
{expression.value} {expression.value}
</VariableBox> </VariableBox>
); );
case "arraystring": case "arraystring":
return ( return (
<VariableBox name={name} heading="Array String"> <VariableBox path={path} heading="Array String">
{expression.value.map((r) => `"${r}"`).join(", ")} {expression.value.map((r) => `"${r}"`).join(", ")}
</VariableBox> </VariableBox>
); );
case "date": case "date":
return ( return (
<VariableBox name={name} heading="Date"> <VariableBox path={path} heading="Date">
{expression.value.toDateString()} {expression.value.toDateString()}
</VariableBox> </VariableBox>
); );
case "timeDuration": { case "timeDuration": {
return ( return (
<VariableBox name={name} heading="Time Duration"> <VariableBox path={path} heading="Time Duration">
<NumberShower precision={3} number={expression.value} /> <NumberShower precision={3} number={expression.value} />
</VariableBox> </VariableBox>
); );
} }
case "lambda": case "lambda":
return ( return (
<VariableBox name={name} heading="Function"> <VariableBox path={path} heading="Function">
<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 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> )})`}</div>
@ -216,7 +166,7 @@ export const SquiggleItem: React.FC<SquiggleItemProps> = ({
); );
case "lambdaDeclaration": { case "lambdaDeclaration": {
return ( return (
<VariableBox name={name} heading="Function Declaration"> <VariableBox path={path} heading="Function Declaration">
<FunctionChart <FunctionChart
fn={expression.value.fn} fn={expression.value.fn}
chartSettings={getChartSettings(expression.value)} chartSettings={getChartSettings(expression.value)}
@ -232,13 +182,13 @@ export const SquiggleItem: React.FC<SquiggleItemProps> = ({
} }
case "module": { case "module": {
return ( return (
<VariableList name={name} heading="Module"> <VariableList path={path} heading="Module">
{Object.entries(expression.value) {Object.entries(expression.value)
.filter(([key, r]) => key !== "Math") .filter(([key, r]) => key !== "Math")
.map(([key, r]) => ( .map(([key, r]) => (
<SquiggleItem <ExpressionViewer
key={key} key={key}
name={key} path={[...path, key]}
expression={r} expression={r}
width={width !== undefined ? width - 20 : width} width={width !== undefined ? width - 20 : width}
height={height / 3} height={height / 3}
@ -252,11 +202,11 @@ export const SquiggleItem: React.FC<SquiggleItemProps> = ({
} }
case "record": case "record":
return ( return (
<VariableList name={name} heading="Record"> <VariableList path={path} heading="Record">
{Object.entries(expression.value).map(([key, r]) => ( {Object.entries(expression.value).map(([key, r]) => (
<SquiggleItem <ExpressionViewer
key={key} key={key}
name={key} path={[...path, key]}
expression={r} expression={r}
width={width !== undefined ? width - 20 : width} width={width !== undefined ? width - 20 : width}
height={height / 3} height={height / 3}
@ -269,11 +219,11 @@ export const SquiggleItem: React.FC<SquiggleItemProps> = ({
); );
case "array": case "array":
return ( return (
<VariableList name={name} heading="Array"> <VariableList path={path} heading="Array">
{expression.value.map((r, i) => ( {expression.value.map((r, i) => (
<SquiggleItem <ExpressionViewer
key={i} key={i}
name={String(i)} path={[...path, String(i)]}
expression={r} expression={r}
width={width !== undefined ? width - 20 : width} width={width !== undefined ? width - 20 : width}
height={50} height={50}

View File

@ -0,0 +1,61 @@
import React, { useContext, useState } from "react";
import { Tooltip } from "../ui/Tooltip";
import { ViewerContext } from "./ViewerContext";
interface VariableBoxProps {
path: string[];
heading: string;
children: React.ReactNode;
}
export const VariableBox: React.FC<VariableBoxProps> = ({
path,
heading = "Error",
children,
}) => {
const { setSettings, getSettings } = useContext(ViewerContext);
const [isCollapsed, setIsCollapsed] = useState(
() => getSettings(path).collapsed
);
const toggleCollapsed = () => {
setSettings(path, {
collapsed: !isCollapsed,
});
setIsCollapsed(!isCollapsed);
};
const isTopLevel = path.length === 0;
const name = isTopLevel ? "" : path[path.length - 1];
return (
<div>
<div>
{isTopLevel ? null : (
<header
className="inline-flex space-x-1 text-slate-500 font-mono text-sm cursor-pointer"
onClick={toggleCollapsed}
>
<Tooltip text={heading}>
<span>{name}:</span>
</Tooltip>
{isCollapsed ? (
<span className="bg-slate-200 rounded p-0.5 font-xs">...</span>
) : null}
</header>
)}
{isCollapsed ? null : (
<div className="flex w-full">
{path.length ? (
<div
className="border-l-2 border-slate-200 hover:border-green-600 w-4 cursor-pointer"
onClick={toggleCollapsed}
></div>
) : null}
<div className="grow">{children}</div>
</div>
)}
</div>
</div>
);
};

View File

@ -0,0 +1,15 @@
import React from "react";
import { ItemSettings, 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): ItemSettings;
setSettings(path: Path, value: ItemSettings): void;
};
export const ViewerContext = React.createContext<ViewerContextShape>({
getSettings: () => ({ collapsed: false }),
setSettings() {},
});

View File

@ -0,0 +1,79 @@
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 { Path, pathAsString } from "./utils";
import { useSquiggle } from "../../lib/hooks";
import { SquiggleErrorAlert } from "../SquiggleErrorAlert";
type Props = {
/** The output of squiggle's run */
result: ReturnType<typeof useSquiggle>;
width?: number;
height: number;
distributionPlotSettings: DistributionPlottingSettings;
/** Settings for displaying functions */
chartSettings: FunctionChartSettings;
/** Environment for further function executions */
environment: environment;
};
type ItemSettings = {
collapsed: boolean;
};
type Settings = {
[k: string]: ItemSettings;
};
const defaultSettings: ItemSettings = { collapsed: false };
export const SquiggleViewer: React.FC<Props> = ({
result,
width,
height,
distributionPlotSettings,
chartSettings,
environment,
}) => {
const settingsRef = useRef<Settings>({});
const getSettings = useCallback(
(path: Path) => {
return settingsRef.current[pathAsString(path)] || defaultSettings;
},
[settingsRef]
);
const setSettings = useCallback(
(path: Path, value: ItemSettings) => {
settingsRef.current[pathAsString(path)] = value;
},
[settingsRef]
);
return (
<ViewerContext.Provider
value={{
getSettings,
setSettings,
}}
>
{result.tag === "Ok" ? (
<ExpressionViewer
path={[]}
expression={result.value}
width={width}
height={height}
distributionPlotSettings={distributionPlotSettings}
chartSettings={chartSettings}
environment={environment}
/>
) : (
<SquiggleErrorAlert error={result.value} />
)}
</ViewerContext.Provider>
);
};

View File

@ -0,0 +1,6 @@
export type ItemSettings = {
collapsed: boolean;
};
export type Path = string[];
export const pathAsString = (path: Path) => path.join(".");