local chart settings via dropdown menu
This commit is contained in:
parent
12eb63c789
commit
8d390c4433
|
@ -9,7 +9,6 @@ import {
|
||||||
defaultEnvironment,
|
defaultEnvironment,
|
||||||
} from "@quri/squiggle-lang";
|
} from "@quri/squiggle-lang";
|
||||||
import { useSquiggle } from "../lib/hooks";
|
import { useSquiggle } from "../lib/hooks";
|
||||||
import { SquiggleErrorAlert } from "./SquiggleErrorAlert";
|
|
||||||
import { SquiggleViewer } from "./SquiggleViewer";
|
import { SquiggleViewer } from "./SquiggleViewer";
|
||||||
|
|
||||||
export interface SquiggleChartProps {
|
export interface SquiggleChartProps {
|
||||||
|
|
|
@ -1,17 +1,11 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {
|
import { squiggleExpression, declaration } from "@quri/squiggle-lang";
|
||||||
squiggleExpression,
|
|
||||||
environment,
|
|
||||||
declaration,
|
|
||||||
} from "@quri/squiggle-lang";
|
|
||||||
import { NumberShower } from "../NumberShower";
|
import { NumberShower } from "../NumberShower";
|
||||||
import {
|
import { DistributionChart } from "../DistributionChart";
|
||||||
DistributionChart,
|
|
||||||
DistributionPlottingSettings,
|
|
||||||
} from "../DistributionChart";
|
|
||||||
import { FunctionChart, FunctionChartSettings } from "../FunctionChart";
|
import { FunctionChart, FunctionChartSettings } from "../FunctionChart";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { VariableBox } from "./VariableBox";
|
import { VariableBox } from "./VariableBox";
|
||||||
|
import { ItemSettingsMenu } from "./ItemSettingsMenu";
|
||||||
|
|
||||||
function getRange<a>(x: declaration<a>) {
|
function getRange<a>(x: declaration<a>) {
|
||||||
const first = x.args[0];
|
const first = x.args[0];
|
||||||
|
@ -42,9 +36,11 @@ const VariableList: React.FC<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}> = ({ path, heading, children }) => (
|
}> = ({ path, heading, children }) => (
|
||||||
<VariableBox path={path} heading={heading}>
|
<VariableBox path={path} heading={heading}>
|
||||||
|
{() => (
|
||||||
<div className={clsx("space-y-3", path.length ? "pt-1 mt-1" : null)}>
|
<div className={clsx("space-y-3", path.length ? "pt-1 mt-1" : null)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</VariableBox>
|
</VariableBox>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -55,11 +51,6 @@ export interface Props {
|
||||||
path: string[];
|
path: string[];
|
||||||
width?: number;
|
width?: number;
|
||||||
height: number;
|
height: number;
|
||||||
distributionPlotSettings: DistributionPlottingSettings;
|
|
||||||
/** Settings for displaying functions */
|
|
||||||
chartSettings: FunctionChartSettings;
|
|
||||||
/** Environment for further function executions */
|
|
||||||
environment: environment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ExpressionViewer: React.FC<Props> = ({
|
export const ExpressionViewer: React.FC<Props> = ({
|
||||||
|
@ -67,17 +58,16 @@ export const ExpressionViewer: React.FC<Props> = ({
|
||||||
expression,
|
expression,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
distributionPlotSettings,
|
|
||||||
chartSettings,
|
|
||||||
environment,
|
|
||||||
}) => {
|
}) => {
|
||||||
switch (expression.tag) {
|
switch (expression.tag) {
|
||||||
case "number":
|
case "number":
|
||||||
return (
|
return (
|
||||||
<VariableBox path={path} 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>
|
||||||
|
)}
|
||||||
</VariableBox>
|
</VariableBox>
|
||||||
);
|
);
|
||||||
case "distribution": {
|
case "distribution": {
|
||||||
|
@ -88,95 +78,134 @@ export const ExpressionViewer: React.FC<Props> = ({
|
||||||
heading={`Distribution (${distType})\n${
|
heading={`Distribution (${distType})\n${
|
||||||
distType === "Symbolic" ? expression.value.toString() : ""
|
distType === "Symbolic" ? expression.value.toString() : ""
|
||||||
}`}
|
}`}
|
||||||
|
dropdownMenu={({ settings, setSettings }) => {
|
||||||
|
return (
|
||||||
|
<ItemSettingsMenu settings={settings} setSettings={setSettings} />
|
||||||
|
);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
{(settings) => {
|
||||||
|
return (
|
||||||
<DistributionChart
|
<DistributionChart
|
||||||
distribution={expression.value}
|
distribution={expression.value}
|
||||||
{...distributionPlotSettings}
|
{...settings.distributionPlotSettings}
|
||||||
height={height}
|
height={height}
|
||||||
width={width}
|
width={width}
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
</VariableBox>
|
</VariableBox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case "string":
|
case "string":
|
||||||
return (
|
return (
|
||||||
<VariableBox path={path} 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}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-slate-400">"</span>
|
<span className="text-slate-400">"</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</VariableBox>
|
</VariableBox>
|
||||||
);
|
);
|
||||||
case "boolean":
|
case "boolean":
|
||||||
return (
|
return (
|
||||||
<VariableBox path={path} heading="Boolean">
|
<VariableBox path={path} heading="Boolean">
|
||||||
{expression.value.toString()}
|
{() => expression.value.toString()}
|
||||||
</VariableBox>
|
</VariableBox>
|
||||||
);
|
);
|
||||||
case "symbol":
|
case "symbol":
|
||||||
return (
|
return (
|
||||||
<VariableBox path={path} 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 path={path} heading="Call">
|
<VariableBox path={path} heading="Call">
|
||||||
{expression.value}
|
{() => expression.value}
|
||||||
</VariableBox>
|
</VariableBox>
|
||||||
);
|
);
|
||||||
case "arraystring":
|
case "arraystring":
|
||||||
return (
|
return (
|
||||||
<VariableBox path={path} 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 path={path} heading="Date">
|
<VariableBox path={path} heading="Date">
|
||||||
{expression.value.toDateString()}
|
{() => expression.value.toDateString()}
|
||||||
</VariableBox>
|
</VariableBox>
|
||||||
);
|
);
|
||||||
case "timeDuration": {
|
case "timeDuration": {
|
||||||
return (
|
return (
|
||||||
<VariableBox path={path} 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 path={path} heading="Function">
|
<VariableBox
|
||||||
|
path={path}
|
||||||
|
heading="Function"
|
||||||
|
dropdownMenu={({ settings, setSettings }) => {
|
||||||
|
return (
|
||||||
|
<ItemSettingsMenu settings={settings} setSettings={setSettings} />
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(settings) => (
|
||||||
|
<>
|
||||||
<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>
|
||||||
<FunctionChart
|
<FunctionChart
|
||||||
fn={expression.value}
|
fn={expression.value}
|
||||||
chartSettings={chartSettings}
|
chartSettings={settings.chartSettings}
|
||||||
distributionPlotSettings={distributionPlotSettings}
|
distributionPlotSettings={settings.distributionPlotSettings}
|
||||||
height={height}
|
height={height}
|
||||||
environment={{
|
environment={{
|
||||||
sampleCount: environment.sampleCount / 10,
|
sampleCount: settings.environment.sampleCount / 10,
|
||||||
xyPointLength: environment.xyPointLength / 10,
|
xyPointLength: settings.environment.xyPointLength / 10,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</VariableBox>
|
</VariableBox>
|
||||||
);
|
);
|
||||||
case "lambdaDeclaration": {
|
case "lambdaDeclaration": {
|
||||||
return (
|
return (
|
||||||
<VariableBox path={path} heading="Function Declaration">
|
<VariableBox
|
||||||
|
path={path}
|
||||||
|
heading="Function Declaration"
|
||||||
|
dropdownMenu={({ settings, setSettings }) => {
|
||||||
|
return (
|
||||||
|
<ItemSettingsMenu settings={settings} setSettings={setSettings} />
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(settings) => (
|
||||||
<FunctionChart
|
<FunctionChart
|
||||||
fn={expression.value.fn}
|
fn={expression.value.fn}
|
||||||
chartSettings={getChartSettings(expression.value)}
|
chartSettings={getChartSettings(expression.value)}
|
||||||
distributionPlotSettings={distributionPlotSettings}
|
distributionPlotSettings={settings.distributionPlotSettings}
|
||||||
height={height}
|
height={height}
|
||||||
environment={{
|
environment={{
|
||||||
sampleCount: environment.sampleCount / 10,
|
sampleCount: settings.environment.sampleCount / 10,
|
||||||
xyPointLength: environment.xyPointLength / 10,
|
xyPointLength: settings.environment.xyPointLength / 10,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</VariableBox>
|
</VariableBox>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -192,9 +221,6 @@ export const ExpressionViewer: React.FC<Props> = ({
|
||||||
expression={r}
|
expression={r}
|
||||||
width={width !== undefined ? width - 20 : width}
|
width={width !== undefined ? width - 20 : width}
|
||||||
height={height / 3}
|
height={height / 3}
|
||||||
distributionPlotSettings={distributionPlotSettings}
|
|
||||||
chartSettings={chartSettings}
|
|
||||||
environment={environment}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</VariableList>
|
</VariableList>
|
||||||
|
@ -210,9 +236,6 @@ export const ExpressionViewer: React.FC<Props> = ({
|
||||||
expression={r}
|
expression={r}
|
||||||
width={width !== undefined ? width - 20 : width}
|
width={width !== undefined ? width - 20 : width}
|
||||||
height={height / 3}
|
height={height / 3}
|
||||||
distributionPlotSettings={distributionPlotSettings}
|
|
||||||
chartSettings={chartSettings}
|
|
||||||
environment={environment}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</VariableList>
|
</VariableList>
|
||||||
|
@ -227,9 +250,6 @@ export const ExpressionViewer: React.FC<Props> = ({
|
||||||
expression={r}
|
expression={r}
|
||||||
width={width !== undefined ? width - 20 : width}
|
width={width !== undefined ? width - 20 : width}
|
||||||
height={50}
|
height={50}
|
||||||
distributionPlotSettings={distributionPlotSettings}
|
|
||||||
chartSettings={chartSettings}
|
|
||||||
environment={environment}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</VariableList>
|
</VariableList>
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import React from "react";
|
||||||
|
import { DropdownMenu } from "../ui/DropdownMenu";
|
||||||
|
import { LocalItemSettings } from "./utils";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
settings: LocalItemSettings;
|
||||||
|
setSettings: (value: LocalItemSettings) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ItemSettingsMenu: React.FC<Props> = ({
|
||||||
|
settings,
|
||||||
|
setSettings,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-1 items-center">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
label="Log X scale"
|
||||||
|
value={settings.distributionPlotSettings?.logX ?? false}
|
||||||
|
toggle={() =>
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
distributionPlotSettings: {
|
||||||
|
...settings.distributionPlotSettings,
|
||||||
|
logX: !settings.distributionPlotSettings?.logX,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
label="Exp Y scale"
|
||||||
|
value={settings.distributionPlotSettings?.expY ?? false}
|
||||||
|
toggle={() =>
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
distributionPlotSettings: {
|
||||||
|
...settings.distributionPlotSettings,
|
||||||
|
expY: !settings.distributionPlotSettings?.expY,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DropdownMenu.CheckboxItem
|
||||||
|
label="Show summary"
|
||||||
|
value={settings.distributionPlotSettings?.showSummary ?? false}
|
||||||
|
toggle={() =>
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
distributionPlotSettings: {
|
||||||
|
...settings.distributionPlotSettings,
|
||||||
|
showSummary: !settings.distributionPlotSettings?.showSummary,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</DropdownMenu>
|
||||||
|
{settings.distributionPlotSettings || settings.chartSettings ? (
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setSettings({
|
||||||
|
...settings,
|
||||||
|
distributionPlotSettings: undefined,
|
||||||
|
chartSettings: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="text-xs px-1 py-0.5 rounded bg-slate-300"
|
||||||
|
>
|
||||||
|
Reset settings
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,50 +1,67 @@
|
||||||
import React, { useContext, useState } from "react";
|
import React, { useContext, useReducer } from "react";
|
||||||
import { Tooltip } from "../ui/Tooltip";
|
import { Tooltip } from "../ui/Tooltip";
|
||||||
|
import { LocalItemSettings, MergedItemSettings } from "./utils";
|
||||||
import { ViewerContext } from "./ViewerContext";
|
import { ViewerContext } from "./ViewerContext";
|
||||||
|
|
||||||
interface VariableBoxProps {
|
type DropdownMenuParams = {
|
||||||
|
settings: LocalItemSettings;
|
||||||
|
setSettings: (value: LocalItemSettings) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VariableBoxProps = {
|
||||||
path: string[];
|
path: string[];
|
||||||
heading: string;
|
heading: string;
|
||||||
children: React.ReactNode;
|
dropdownMenu?: (params: DropdownMenuParams) => React.ReactNode;
|
||||||
}
|
children: (settings: MergedItemSettings) => React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
export const VariableBox: React.FC<VariableBoxProps> = ({
|
export const VariableBox: React.FC<VariableBoxProps> = ({
|
||||||
path,
|
path,
|
||||||
heading = "Error",
|
heading = "Error",
|
||||||
|
dropdownMenu,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const { setSettings, getSettings } = useContext(ViewerContext);
|
const { setSettings, getSettings, getMergedSettings } =
|
||||||
const [isCollapsed, setIsCollapsed] = useState(
|
useContext(ViewerContext);
|
||||||
() => getSettings(path).collapsed
|
const [_, forceUpdate] = useReducer((x) => x + 1, 0);
|
||||||
);
|
|
||||||
|
const settings = getSettings(path);
|
||||||
|
|
||||||
|
const setSettingsAndUpdate = (newSettings: LocalItemSettings) => {
|
||||||
|
setSettings(path, newSettings);
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
const toggleCollapsed = () => {
|
const toggleCollapsed = () => {
|
||||||
setSettings(path, {
|
setSettingsAndUpdate({ ...settings, collapsed: !settings.collapsed });
|
||||||
collapsed: !isCollapsed,
|
|
||||||
});
|
|
||||||
setIsCollapsed(!isCollapsed);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isTopLevel = path.length === 0;
|
const isTopLevel = path.length === 0;
|
||||||
const name = isTopLevel ? "" : path[path.length - 1];
|
const name = isTopLevel ? "Result" : path[path.length - 1];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<header className="inline-flex space-x-1">
|
||||||
{isTopLevel ? null : (
|
<Tooltip text={heading}>
|
||||||
<header
|
<span
|
||||||
className="inline-flex space-x-1 text-slate-500 font-mono text-sm cursor-pointer"
|
className="text-slate-500 font-mono text-sm cursor-pointer"
|
||||||
onClick={toggleCollapsed}
|
onClick={toggleCollapsed}
|
||||||
>
|
>
|
||||||
<Tooltip text={heading}>
|
{name}:
|
||||||
<span>{name}:</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{isCollapsed ? (
|
{settings.collapsed ? (
|
||||||
<span className="bg-slate-200 rounded p-0.5 font-xs">...</span>
|
<span
|
||||||
|
className="rounded p-0.5 bg-slate-200 text-slate-500 font-mono text-xs cursor-pointer"
|
||||||
|
onClick={toggleCollapsed}
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</span>
|
||||||
|
) : dropdownMenu ? (
|
||||||
|
dropdownMenu({ settings, setSettings: setSettingsAndUpdate })
|
||||||
) : null}
|
) : null}
|
||||||
</header>
|
</header>
|
||||||
)}
|
{settings.collapsed ? null : (
|
||||||
{isCollapsed ? null : (
|
|
||||||
<div className="flex w-full">
|
<div className="flex w-full">
|
||||||
{path.length ? (
|
{path.length ? (
|
||||||
<div
|
<div
|
||||||
|
@ -52,10 +69,9 @@ export const VariableBox: React.FC<VariableBoxProps> = ({
|
||||||
onClick={toggleCollapsed}
|
onClick={toggleCollapsed}
|
||||||
></div>
|
></div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="grow">{children}</div>
|
<div className="grow">{children(getMergedSettings(path))}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,15 +1,33 @@
|
||||||
|
import { defaultEnvironment } from "@quri/squiggle-lang";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ItemSettings, Path } from "./utils";
|
import { LocalItemSettings, MergedItemSettings, Path } from "./utils";
|
||||||
|
|
||||||
type ViewerContextShape = {
|
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).
|
// 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.
|
// 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.
|
// See ./SquiggleViewer.tsx and ./VariableBox.tsx for other implementation details on this.
|
||||||
getSettings(path: Path): ItemSettings;
|
getSettings(path: Path): LocalItemSettings;
|
||||||
setSettings(path: Path, value: ItemSettings): void;
|
getMergedSettings(path: Path): MergedItemSettings;
|
||||||
|
setSettings(path: Path, value: LocalItemSettings): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewerContext = React.createContext<ViewerContextShape>({
|
export const ViewerContext = React.createContext<ViewerContextShape>({
|
||||||
getSettings: () => ({ collapsed: false }),
|
getSettings: () => ({ collapsed: false }),
|
||||||
|
getMergedSettings: () => ({
|
||||||
|
collapsed: false,
|
||||||
|
// copy-pasted from SquiggleChart
|
||||||
|
chartSettings: {
|
||||||
|
start: 0,
|
||||||
|
stop: 10,
|
||||||
|
count: 100,
|
||||||
|
},
|
||||||
|
distributionPlotSettings: {
|
||||||
|
showSummary: false,
|
||||||
|
showControls: false,
|
||||||
|
logX: false,
|
||||||
|
expY: false,
|
||||||
|
},
|
||||||
|
environment: defaultEnvironment,
|
||||||
|
}),
|
||||||
setSettings() {},
|
setSettings() {},
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,12 @@ import { DistributionPlottingSettings } from "../DistributionChart";
|
||||||
import { FunctionChartSettings } from "../FunctionChart";
|
import { FunctionChartSettings } from "../FunctionChart";
|
||||||
import { ExpressionViewer } from "./ExpressionViewer";
|
import { ExpressionViewer } from "./ExpressionViewer";
|
||||||
import { ViewerContext } from "./ViewerContext";
|
import { ViewerContext } from "./ViewerContext";
|
||||||
import { Path, pathAsString } from "./utils";
|
import {
|
||||||
|
LocalItemSettings,
|
||||||
|
MergedItemSettings,
|
||||||
|
Path,
|
||||||
|
pathAsString,
|
||||||
|
} from "./utils";
|
||||||
import { useSquiggle } from "../../lib/hooks";
|
import { useSquiggle } from "../../lib/hooks";
|
||||||
import { SquiggleErrorAlert } from "../SquiggleErrorAlert";
|
import { SquiggleErrorAlert } from "../SquiggleErrorAlert";
|
||||||
|
|
||||||
|
@ -20,15 +25,11 @@ type Props = {
|
||||||
environment: environment;
|
environment: environment;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ItemSettings = {
|
|
||||||
collapsed: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Settings = {
|
type Settings = {
|
||||||
[k: string]: ItemSettings;
|
[k: string]: LocalItemSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultSettings: ItemSettings = { collapsed: false };
|
const defaultSettings: LocalItemSettings = { collapsed: false };
|
||||||
|
|
||||||
export const SquiggleViewer: React.FC<Props> = ({
|
export const SquiggleViewer: React.FC<Props> = ({
|
||||||
result,
|
result,
|
||||||
|
@ -38,6 +39,7 @@ export const SquiggleViewer: React.FC<Props> = ({
|
||||||
chartSettings,
|
chartSettings,
|
||||||
environment,
|
environment,
|
||||||
}) => {
|
}) => {
|
||||||
|
// can't store settings in the state because we don't want to rerender the entire tree on every change
|
||||||
const settingsRef = useRef<Settings>({});
|
const settingsRef = useRef<Settings>({});
|
||||||
|
|
||||||
const getSettings = useCallback(
|
const getSettings = useCallback(
|
||||||
|
@ -48,17 +50,40 @@ export const SquiggleViewer: React.FC<Props> = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
const setSettings = useCallback(
|
const setSettings = useCallback(
|
||||||
(path: Path, value: ItemSettings) => {
|
(path: Path, value: LocalItemSettings) => {
|
||||||
settingsRef.current[pathAsString(path)] = value;
|
settingsRef.current[pathAsString(path)] = value;
|
||||||
},
|
},
|
||||||
[settingsRef]
|
[settingsRef]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getMergedSettings = useCallback(
|
||||||
|
(path: Path) => {
|
||||||
|
const localSettings = getSettings(path);
|
||||||
|
const result: MergedItemSettings = {
|
||||||
|
distributionPlotSettings: {
|
||||||
|
...distributionPlotSettings,
|
||||||
|
...(localSettings.distributionPlotSettings || {}),
|
||||||
|
},
|
||||||
|
chartSettings: {
|
||||||
|
...chartSettings,
|
||||||
|
...(localSettings.chartSettings || {}),
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
...environment,
|
||||||
|
...(localSettings.environment || {}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
[distributionPlotSettings, chartSettings, environment, getSettings]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ViewerContext.Provider
|
<ViewerContext.Provider
|
||||||
value={{
|
value={{
|
||||||
getSettings,
|
getSettings,
|
||||||
setSettings,
|
setSettings,
|
||||||
|
getMergedSettings,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{result.tag === "Ok" ? (
|
{result.tag === "Ok" ? (
|
||||||
|
@ -67,9 +92,6 @@ export const SquiggleViewer: React.FC<Props> = ({
|
||||||
expression={result.value}
|
expression={result.value}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
distributionPlotSettings={distributionPlotSettings}
|
|
||||||
chartSettings={chartSettings}
|
|
||||||
environment={environment}
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SquiggleErrorAlert error={result.value} />
|
<SquiggleErrorAlert error={result.value} />
|
||||||
|
|
|
@ -1,6 +1,20 @@
|
||||||
export type ItemSettings = {
|
import { DistributionPlottingSettings } from "../DistributionChart";
|
||||||
|
import { FunctionChartSettings } from "../FunctionChart";
|
||||||
|
import { environment } from "@quri/squiggle-lang";
|
||||||
|
|
||||||
|
export type LocalItemSettings = {
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
|
distributionPlotSettings?: Partial<DistributionPlottingSettings>;
|
||||||
|
chartSettings?: Partial<FunctionChartSettings>;
|
||||||
|
environment?: Partial<environment>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MergedItemSettings = {
|
||||||
|
distributionPlotSettings: DistributionPlottingSettings;
|
||||||
|
chartSettings: FunctionChartSettings;
|
||||||
|
environment: environment;
|
||||||
|
};
|
||||||
|
|
||||||
export type Path = string[];
|
export type Path = string[];
|
||||||
|
|
||||||
export const pathAsString = (path: Path) => path.join(".");
|
export const pathAsString = (path: Path) => path.join(".");
|
||||||
|
|
75
packages/components/src/components/ui/DropdownMenu.tsx
Normal file
75
packages/components/src/components/ui/DropdownMenu.tsx
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
import { CheckIcon, CogIcon } from "@heroicons/react/solid";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
import {
|
||||||
|
shift,
|
||||||
|
useClick,
|
||||||
|
useDismiss,
|
||||||
|
useFloating,
|
||||||
|
useInteractions,
|
||||||
|
useRole,
|
||||||
|
} from "@floating-ui/react-dom-interactions";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DropdownMenuType = React.FC<Props> & {
|
||||||
|
CheckboxItem: React.FC<{ label: string; value: boolean; toggle: () => void }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DropdownMenu: DropdownMenuType = ({ children }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { x, y, reference, floating, strategy, context } = useFloating({
|
||||||
|
placement: "bottom-start",
|
||||||
|
open: isOpen,
|
||||||
|
onOpenChange: setIsOpen,
|
||||||
|
middleware: [shift()],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getReferenceProps, getFloatingProps } = useInteractions([
|
||||||
|
useClick(context),
|
||||||
|
useRole(context, { role: "menu" }),
|
||||||
|
useDismiss(context),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CogIcon
|
||||||
|
className="h-5 w-5 cursor-pointer text-slate-400 hover:text-slate-500"
|
||||||
|
onClick={() => setIsOpen(!isOpen)}
|
||||||
|
{...getReferenceProps({ ref: reference })}
|
||||||
|
/>
|
||||||
|
{isOpen ? (
|
||||||
|
<div
|
||||||
|
{...getFloatingProps({
|
||||||
|
className: "rounded shadow z-10 bg-white",
|
||||||
|
ref: floating,
|
||||||
|
style: {
|
||||||
|
position: strategy,
|
||||||
|
top: y ?? 0,
|
||||||
|
left: x ?? 0,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
DropdownMenu.CheckboxItem = ({ label, value, toggle }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="px-4 py-2 cursor-pointer flex space-x-2 hover:bg-gray-100"
|
||||||
|
onClick={toggle}
|
||||||
|
>
|
||||||
|
{value ? (
|
||||||
|
<CheckIcon className="w-4 h-4 text-gray-700" />
|
||||||
|
) : (
|
||||||
|
<div className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span>{label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -15,12 +15,12 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Tooltip: React.FC<Props> = ({ text, children }) => {
|
export const Tooltip: React.FC<Props> = ({ text, children }) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const { x, y, reference, floating, strategy, context } = useFloating({
|
const { x, y, reference, floating, strategy, context } = useFloating({
|
||||||
placement: "top",
|
placement: "top",
|
||||||
open,
|
open: isOpen,
|
||||||
onOpenChange: setOpen,
|
onOpenChange: setIsOpen,
|
||||||
middleware: [shift()],
|
middleware: [shift()],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ export const Tooltip: React.FC<Props> = ({ text, children }) => {
|
||||||
getReferenceProps({ ref: reference, ...children.props })
|
getReferenceProps({ ref: reference, ...children.props })
|
||||||
)}
|
)}
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{open && (
|
{isOpen && (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user