SquiggleViewer refactoring; persistent collapsed settings via context
This commit is contained in:
parent
3ca80daee8
commit
12eb63c789
|
@ -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}
|
||||||
|
|
|
@ -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}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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() {},
|
||||||
|
});
|
79
packages/components/src/components/SquiggleViewer/index.tsx
Normal file
79
packages/components/src/components/SquiggleViewer/index.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,6 @@
|
||||||
|
export type ItemSettings = {
|
||||||
|
collapsed: boolean;
|
||||||
|
};
|
||||||
|
export type Path = string[];
|
||||||
|
|
||||||
|
export const pathAsString = (path: Path) => path.join(".");
|
Loading…
Reference in New Issue
Block a user