modal window for local settings

This commit is contained in:
Vyacheslav Matyukhin 2022-07-22 23:24:49 +04:00
parent 8f4259cef3
commit eefdfbb2fe
No known key found for this signature in database
GPG Key ID: 3D2A774C5489F96C
16 changed files with 519 additions and 391 deletions

View File

@ -8,26 +8,24 @@ import {
import { Vega } from "react-vega"; import { Vega } from "react-vega";
import { ErrorAlert } from "./Alert"; import { ErrorAlert } from "./Alert";
import { useSize } from "react-use"; import { useSize } from "react-use";
import clsx from "clsx";
import { import {
buildVegaSpec, buildVegaSpec,
DistributionChartSpecOptions, DistributionChartSpecOptions,
} from "../lib/distributionSpecBuilder"; } from "../lib/distributionSpecBuilder";
import { NumberShower } from "./NumberShower"; import { NumberShower } from "./NumberShower";
import { hasMassBelowZero } from "../lib/distributionUtils";
export type DistributionPlottingSettings = { export type DistributionPlottingSettings = {
/** Whether to show a summary of means, stdev, percentiles etc */ /** Whether to show a summary of means, stdev, percentiles etc */
showSummary: boolean; showSummary: boolean;
/** Whether to show the user graph controls (scale etc) */ actions?: boolean;
showControls: boolean;
} & DistributionChartSpecOptions; } & DistributionChartSpecOptions;
export type DistributionChartProps = { export type DistributionChartProps = {
distribution: Distribution; distribution: Distribution;
width?: number; width?: number;
height: number; height: number;
actions?: boolean;
} & DistributionPlottingSettings; } & DistributionPlottingSettings;
export const DistributionChart: React.FC<DistributionChartProps> = (props) => { export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
@ -36,17 +34,9 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
height, height,
showSummary, showSummary,
width, width,
showControls,
logX, logX,
expY,
actions = false, actions = false,
} = props; } = props;
const [isLogX, setLogX] = React.useState(logX);
const [isExpY, setExpY] = React.useState(expY);
React.useEffect(() => setLogX(logX), [logX]);
React.useEffect(() => setExpY(expY), [expY]);
const shape = distribution.pointSet(); const shape = distribution.pointSet();
const [sized] = useSize((size) => { const [sized] = useSize((size) => {
if (shape.tag === "Error") { if (shape.tag === "Error") {
@ -57,9 +47,6 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
); );
} }
const massBelow0 =
shape.value.continuous.some((x) => x.x <= 0) ||
shape.value.discrete.some((x) => x.x <= 0);
const spec = buildVegaSpec(props); const spec = buildVegaSpec(props);
let widthProp = width ? width : size.width; let widthProp = width ? width : size.width;
@ -72,7 +59,11 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
return ( return (
<div style={{ width: widthProp }}> <div style={{ width: widthProp }}>
{!(isLogX && massBelow0) ? ( {logX && hasMassBelowZero(shape.value) ? (
<ErrorAlert heading="Log Domain Error">
Cannot graph distribution with negative values on logarithmic scale.
</ErrorAlert>
) : (
<Vega <Vega
spec={spec} spec={spec}
data={{ con: shape.value.continuous, dis: shape.value.discrete }} data={{ con: shape.value.continuous, dis: shape.value.discrete }}
@ -80,67 +71,16 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
height={height} height={height}
actions={actions} actions={actions}
/> />
) : (
<ErrorAlert heading="Log Domain Error">
Cannot graph distribution with negative values on logarithmic scale.
</ErrorAlert>
)} )}
<div className="flex justify-center"> <div className="flex justify-center">
{showSummary && <SummaryTable distribution={distribution} />} {showSummary && <SummaryTable distribution={distribution} />}
</div> </div>
{showControls && (
<div>
<CheckBox
label="Log X scale"
value={isLogX}
onChange={setLogX}
// Check whether we should disable the checkbox
{...(massBelow0
? {
disabled: true,
tooltip:
"Your distribution has mass lower than or equal to 0. Log only works on strictly positive values.",
}
: {})}
/>
<CheckBox label="Exp Y scale" value={isExpY} onChange={setExpY} />
</div>
)}
</div> </div>
); );
}); });
return sized; return sized;
}; };
interface CheckBoxProps {
label: string;
onChange: (x: boolean) => void;
value: boolean;
disabled?: boolean;
tooltip?: string;
}
export const CheckBox: React.FC<CheckBoxProps> = ({
label,
onChange,
value,
disabled = false,
tooltip,
}) => {
return (
<span title={tooltip}>
<input
type="checkbox"
checked={value}
onChange={() => onChange(!value)}
disabled={disabled}
className="form-checkbox"
/>
<label className={clsx(disabled && "text-slate-400")}> {label}</label>
</span>
);
};
const TableHeadCell: React.FC<{ children: React.ReactNode }> = ({ const TableHeadCell: React.FC<{ children: React.ReactNode }> = ({
children, children,
}) => ( }) => (

View File

@ -35,8 +35,6 @@ export interface SquiggleChartProps {
jsImports?: jsImports; jsImports?: jsImports;
/** Whether to show a summary of the distribution */ /** Whether to show a summary of the distribution */
showSummary?: boolean; showSummary?: boolean;
/** Whether to show graph controls (scale etc)*/
showControls?: boolean;
/** Set the x scale to be logarithmic by deault */ /** Set the x scale to be logarithmic by deault */
logX?: boolean; logX?: boolean;
/** Set the y scale to be exponential by deault */ /** Set the y scale to be exponential by deault */
@ -67,7 +65,6 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
jsImports = defaultImports, jsImports = defaultImports,
showSummary = false, showSummary = false,
width, width,
showControls = false,
logX = false, logX = false,
expY = false, expY = false,
diagramStart = 0, diagramStart = 0,
@ -89,7 +86,6 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
}); });
const distributionPlotSettings = { const distributionPlotSettings = {
showControls,
showSummary, showSummary,
logX, logX,
expY, expY,

View File

@ -1,5 +1,5 @@
import React, { FC, useState, useEffect, useMemo } from "react"; import React, { FC, useState, useEffect, useMemo } from "react";
import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form"; import { useForm, UseFormRegister, useWatch } from "react-hook-form";
import * as yup from "yup"; import * as yup from "yup";
import { useMaybeControlledValue } from "../lib/hooks"; import { useMaybeControlledValue } from "../lib/hooks";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
@ -24,8 +24,15 @@ import { JsonEditor } from "./JsonEditor";
import { ErrorAlert, SuccessAlert } from "./Alert"; import { ErrorAlert, SuccessAlert } from "./Alert";
import { SquiggleContainer } from "./SquiggleContainer"; import { SquiggleContainer } from "./SquiggleContainer";
import { Toggle } from "./ui/Toggle"; import { Toggle } from "./ui/Toggle";
import { Checkbox } from "./ui/Checkbox";
import { StyledTab } from "./ui/StyledTab"; import { StyledTab } from "./ui/StyledTab";
import { InputItem } from "./ui/InputItem";
import { Text } from "./ui/Text";
import { ViewSettings, viewSettingsSchema } from "./ViewSettings";
import { HeadedSection } from "./ui/HeadedSection";
import {
defaultColor,
defaultTickFormat,
} from "../lib/distributionSpecBuilder";
type PlaygroundProps = SquiggleChartProps & { type PlaygroundProps = SquiggleChartProps & {
/** The initial squiggle string to put in the playground */ /** The initial squiggle string to put in the playground */
@ -37,7 +44,9 @@ type PlaygroundProps = SquiggleChartProps & {
showEditor?: boolean; showEditor?: boolean;
}; };
const schema = yup.object({}).shape({ const schema = yup
.object({})
.shape({
sampleCount: yup sampleCount: yup
.number() .number()
.required() .required()
@ -54,73 +63,11 @@ const schema = yup.object({}).shape({
.default(1000) .default(1000)
.min(10) .min(10)
.max(10000), .max(10000),
chartHeight: yup.number().required().positive().integer().default(350), })
leftSizePercent: yup .concat(viewSettingsSchema);
.number()
.required()
.positive()
.integer()
.min(10)
.max(100)
.default(50),
showControls: yup.boolean().required(),
showSummary: yup.boolean().required(),
showEditor: yup.boolean().required(),
logX: yup.boolean().required(),
expY: yup.boolean().required(),
tickFormat: yup.string().default(".9~s"),
title: yup.string(),
color: yup.string().default("#739ECC").required(),
minX: yup.number(),
maxX: yup.number(),
distributionChartActions: yup.boolean(),
showSettingsPage: yup.boolean().default(false),
diagramStart: yup.number().required().positive().integer().default(0).min(0),
diagramStop: yup.number().required().positive().integer().default(10).min(0),
diagramCount: yup.number().required().positive().integer().default(20).min(2),
});
type FormFields = yup.InferType<typeof schema>; type FormFields = yup.InferType<typeof schema>;
const HeadedSection: FC<{ title: string; children: React.ReactNode }> = ({
title,
children,
}) => (
<div>
<header className="text-lg leading-6 font-medium text-gray-900">
{title}
</header>
<div className="mt-4">{children}</div>
</div>
);
const Text: FC<{ children: React.ReactNode }> = ({ children }) => (
<p className="text-sm text-gray-500">{children}</p>
);
function InputItem<T>({
name,
label,
type,
register,
}: {
name: Path<T>;
label: string;
type: "number" | "text" | "color";
register: UseFormRegister<T>;
}) {
return (
<label className="block">
<div className="text-sm font-medium text-gray-600 mb-1">{label}</div>
<input
type={type}
{...register(name, { valueAsNumber: type === "number" })}
className="form-input max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
/>
</label>
);
}
const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({ const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
register, register,
}) => ( }) => (
@ -156,123 +103,6 @@ const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
</div> </div>
); );
const ViewSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
register,
}) => (
<div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl">
<HeadedSection title="General Display Settings">
<div className="space-y-4">
<Checkbox
name="showEditor"
register={register}
label="Show code editor on left"
/>
<InputItem
name="chartHeight"
type="number"
register={register}
label="Chart Height (in pixels)"
/>
</div>
</HeadedSection>
<div className="pt-8">
<HeadedSection title="Distribution Display Settings">
<div className="space-y-2">
<Checkbox
register={register}
name="logX"
label="Show x scale logarithmically"
/>
<Checkbox
register={register}
name="expY"
label="Show y scale exponentially"
/>
<Checkbox
register={register}
name="distributionChartActions"
label="Show vega chart controls"
/>
<Checkbox
register={register}
name="showControls"
label="Show toggles to adjust scale of x and y axes"
/>
<Checkbox
register={register}
name="showSummary"
label="Show summary statistics"
/>
<InputItem
name="minX"
type="number"
register={register}
label="Min X Value"
/>
<InputItem
name="maxX"
type="number"
register={register}
label="Max X Value"
/>
<InputItem
name="title"
type="text"
register={register}
label="Title"
/>
<InputItem
name="tickFormat"
type="text"
register={register}
label="Tick Format"
/>
<InputItem
name="color"
type="color"
register={register}
label="Color"
/>
</div>
</HeadedSection>
</div>
<div className="pt-8">
<HeadedSection title="Function Display Settings">
<div className="space-y-6">
<Text>
When displaying functions of single variables that return numbers or
distributions, we need to use defaults for the x-axis. We need to
select a minimum and maximum value of x to sample, and a number n of
the number of points to sample.
</Text>
<div className="space-y-4">
<InputItem
type="number"
name="diagramStart"
register={register}
label="Min X Value"
/>
<InputItem
type="number"
name="diagramStop"
register={register}
label="Max X Value"
/>
<InputItem
type="number"
name="diagramCount"
register={register}
label="Points between X min and X max to sample"
/>
</div>
</div>
</HeadedSection>
</div>
</div>
);
const InputVariablesSettings: React.FC<{ const InputVariablesSettings: React.FC<{
initialImports: any; // TODO - any json type initialImports: any; // TODO - any json type
setImports: (imports: any) => void; setImports: (imports: any) => void;
@ -402,15 +232,14 @@ const useRunnerState = (code: string) => {
export const SquigglePlayground: FC<PlaygroundProps> = ({ export const SquigglePlayground: FC<PlaygroundProps> = ({
defaultCode = "", defaultCode = "",
height = 500, height = 500,
showControls = false,
showSummary = false, showSummary = false,
logX = false, logX = false,
expY = false, expY = false,
title, title,
minX, minX,
maxX, maxX,
color = "#739ECC", color = defaultColor,
tickFormat = ".9~s", tickFormat = defaultTickFormat,
distributionChartActions, distributionChartActions,
code: controlledCode, code: controlledCode,
onCodeChange, onCodeChange,
@ -431,7 +260,6 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
sampleCount: 1000, sampleCount: 1000,
xyPointLength: 1000, xyPointLength: 1000,
chartHeight: 150, chartHeight: 150,
showControls,
logX, logX,
expY, expY,
title, title,
@ -442,8 +270,6 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
distributionChartActions, distributionChartActions,
showSummary, showSummary,
showEditor, showEditor,
leftSizePercent: 50,
showSettingsPage: false,
diagramStart: 0, diagramStart: 0,
diagramStop: 10, diagramStop: 10,
diagramCount: 20, diagramCount: 20,
@ -500,7 +326,13 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
<SamplingSettings register={register} /> <SamplingSettings register={register} />
</StyledTab.Panel> </StyledTab.Panel>
<StyledTab.Panel> <StyledTab.Panel>
<ViewSettings register={register} /> <ViewSettings
register={
register as unknown as UseFormRegister<
yup.InferType<typeof viewSettingsSchema>
>
}
/>
</StyledTab.Panel> </StyledTab.Panel>
<StyledTab.Panel> <StyledTab.Panel>
<InputVariablesSettings <InputVariablesSettings

View File

@ -6,6 +6,8 @@ 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"; import { ItemSettingsMenu } from "./ItemSettingsMenu";
import { hasMassBelowZero } from "../../lib/distributionUtils";
import { MergedItemSettings } from "./utils";
function getRange<a>(x: declaration<a>) { function getRange<a>(x: declaration<a>) {
const first = x.args[0]; const first = x.args[0];
@ -33,12 +35,12 @@ function getChartSettings<a>(x: declaration<a>): FunctionChartSettings {
const VariableList: React.FC<{ const VariableList: React.FC<{
path: string[]; path: string[];
heading: string; heading: string;
children: React.ReactNode; children: (settings: MergedItemSettings) => React.ReactNode;
}> = ({ path, heading, children }) => ( }> = ({ path, heading, children }) => (
<VariableBox path={path} heading={heading}> <VariableBox path={path} heading={heading}>
{() => ( {(settings) => (
<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(settings)}
</div> </div>
)} )}
</VariableBox> </VariableBox>
@ -50,14 +52,12 @@ export interface Props {
/** Path to the current item, e.g. `['foo', 'bar', '3']` for `foo.bar[3]`; can be empty on the top-level item. */ /** Path to the current item, e.g. `['foo', 'bar', '3']` for `foo.bar[3]`; can be empty on the top-level item. */
path: string[]; path: string[];
width?: number; width?: number;
height: number;
} }
export const ExpressionViewer: React.FC<Props> = ({ export const ExpressionViewer: React.FC<Props> = ({
path, path,
expression, expression,
width, width,
height,
}) => { }) => {
switch (expression.tag) { switch (expression.tag) {
case "number": case "number":
@ -78,9 +78,17 @@ 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 }) => { renderSettingsMenu={({ onChange }) => {
const shape = expression.value.pointSet();
return ( return (
<ItemSettingsMenu settings={settings} setSettings={setSettings} /> <ItemSettingsMenu
path={path}
onChange={onChange}
disableLogX={
shape.tag === "Ok" && hasMassBelowZero(shape.value)
}
withFunctionSettings={false}
/>
); );
}} }}
> >
@ -89,7 +97,7 @@ export const ExpressionViewer: React.FC<Props> = ({
<DistributionChart <DistributionChart
distribution={expression.value} distribution={expression.value}
{...settings.distributionPlotSettings} {...settings.distributionPlotSettings}
height={height} height={settings.height}
width={width} width={width}
/> />
); );
@ -158,9 +166,13 @@ export const ExpressionViewer: React.FC<Props> = ({
<VariableBox <VariableBox
path={path} path={path}
heading="Function" heading="Function"
dropdownMenu={({ settings, setSettings }) => { renderSettingsMenu={({ onChange }) => {
return ( return (
<ItemSettingsMenu settings={settings} setSettings={setSettings} /> <ItemSettingsMenu
path={path}
onChange={onChange}
withFunctionSettings={true}
/>
); );
}} }}
> >
@ -173,7 +185,7 @@ export const ExpressionViewer: React.FC<Props> = ({
fn={expression.value} fn={expression.value}
chartSettings={settings.chartSettings} chartSettings={settings.chartSettings}
distributionPlotSettings={settings.distributionPlotSettings} distributionPlotSettings={settings.distributionPlotSettings}
height={height} height={settings.height}
environment={{ environment={{
sampleCount: settings.environment.sampleCount / 10, sampleCount: settings.environment.sampleCount / 10,
xyPointLength: settings.environment.xyPointLength / 10, xyPointLength: settings.environment.xyPointLength / 10,
@ -188,9 +200,13 @@ export const ExpressionViewer: React.FC<Props> = ({
<VariableBox <VariableBox
path={path} path={path}
heading="Function Declaration" heading="Function Declaration"
dropdownMenu={({ settings, setSettings }) => { renderSettingsMenu={({ onChange }) => {
return ( return (
<ItemSettingsMenu settings={settings} setSettings={setSettings} /> <ItemSettingsMenu
onChange={onChange}
path={path}
withFunctionSettings={true}
/>
); );
}} }}
> >
@ -199,7 +215,7 @@ export const ExpressionViewer: React.FC<Props> = ({
fn={expression.value.fn} fn={expression.value.fn}
chartSettings={getChartSettings(expression.value)} chartSettings={getChartSettings(expression.value)}
distributionPlotSettings={settings.distributionPlotSettings} distributionPlotSettings={settings.distributionPlotSettings}
height={height} height={settings.height}
environment={{ environment={{
sampleCount: settings.environment.sampleCount / 10, sampleCount: settings.environment.sampleCount / 10,
xyPointLength: settings.environment.xyPointLength / 10, xyPointLength: settings.environment.xyPointLength / 10,
@ -212,7 +228,8 @@ export const ExpressionViewer: React.FC<Props> = ({
case "module": { case "module": {
return ( return (
<VariableList path={path} heading="Module"> <VariableList path={path} heading="Module">
{Object.entries(expression.value) {(settings) =>
Object.entries(expression.value)
.filter(([key, r]) => key !== "Math") .filter(([key, r]) => key !== "Math")
.map(([key, r]) => ( .map(([key, r]) => (
<ExpressionViewer <ExpressionViewer
@ -220,38 +237,40 @@ export const ExpressionViewer: React.FC<Props> = ({
path={[...path, key]} path={[...path, key]}
expression={r} expression={r}
width={width !== undefined ? width - 20 : width} width={width !== undefined ? width - 20 : width}
height={height / 3}
/> />
))} ))
}
</VariableList> </VariableList>
); );
} }
case "record": case "record":
return ( return (
<VariableList path={path} heading="Record"> <VariableList path={path} heading="Record">
{Object.entries(expression.value).map(([key, r]) => ( {(settings) =>
Object.entries(expression.value).map(([key, r]) => (
<ExpressionViewer <ExpressionViewer
key={key} key={key}
path={[...path, key]} path={[...path, key]}
expression={r} expression={r}
width={width !== undefined ? width - 20 : width} width={width !== undefined ? width - 20 : width}
height={height / 3}
/> />
))} ))
}
</VariableList> </VariableList>
); );
case "array": case "array":
return ( return (
<VariableList path={path} heading="Array"> <VariableList path={path} heading="Array">
{expression.value.map((r, i) => ( {(settings) =>
expression.value.map((r, i) => (
<ExpressionViewer <ExpressionViewer
key={i} key={i}
path={[...path, String(i)]} path={[...path, String(i)]}
expression={r} expression={r}
width={width !== undefined ? width - 20 : width} width={width !== undefined ? width - 20 : width}
height={50}
/> />
))} ))
}
</VariableList> </VariableList>
); );
default: { default: {

View File

@ -1,73 +1,127 @@
import React from "react"; import { CogIcon } from "@heroicons/react/solid";
import { DropdownMenu } from "../ui/DropdownMenu"; import React, { useContext, useState } from "react";
import { LocalItemSettings } from "./utils"; import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { Modal } from "../ui/Modal";
import { ViewSettings, viewSettingsSchema } from "../ViewSettings";
import { Path, pathAsString } from "./utils";
import { ViewerContext } from "./ViewerContext";
import {
defaultColor,
defaultTickFormat,
} from "../../lib/distributionSpecBuilder";
type Props = { type Props = {
settings: LocalItemSettings; path: Path;
setSettings: (value: LocalItemSettings) => void; onChange: () => void;
disableLogX?: boolean;
withFunctionSettings: boolean;
}; };
export const ItemSettingsMenu: React.FC<Props> = ({ const ItemSettingsModal: React.FC<Props & { close: () => void }> = ({
settings, path,
setSettings, onChange,
disableLogX,
withFunctionSettings,
close,
}) => { }) => {
const { setSettings, getSettings, getMergedSettings } =
useContext(ViewerContext);
const mergedSettings = getMergedSettings(path);
const { register, watch } = useForm({
resolver: yupResolver(viewSettingsSchema),
defaultValues: {
showEditor: true, // doesn't matter
chartHeight: mergedSettings.height,
showSummary: mergedSettings.distributionPlotSettings.showSummary,
logX: mergedSettings.distributionPlotSettings.logX,
expY: mergedSettings.distributionPlotSettings.expY,
tickFormat:
mergedSettings.distributionPlotSettings.format || defaultTickFormat,
title: mergedSettings.distributionPlotSettings.title,
color: mergedSettings.distributionPlotSettings.color || defaultColor,
minX: mergedSettings.distributionPlotSettings.minX,
maxX: mergedSettings.distributionPlotSettings.maxX,
distributionChartActions: mergedSettings.distributionPlotSettings.actions,
diagramStart: mergedSettings.chartSettings.start,
diagramStop: mergedSettings.chartSettings.stop,
diagramCount: mergedSettings.chartSettings.count,
},
});
React.useEffect(() => {
const subscription = watch((vars) => {
const settings = getSettings(path); // get the latest version
setSettings(path, {
...settings,
distributionPlotSettings: {
showSummary: vars.showSummary,
logX: vars.logX,
expY: vars.expY,
format: vars.tickFormat,
title: vars.title,
color: vars.color,
minX: vars.minX,
maxX: vars.maxX,
actions: vars.distributionChartActions,
},
chartSettings: {
start: vars.diagramStart,
stop: vars.diagramStop,
count: vars.diagramCount,
},
});
onChange();
});
return () => subscription.unsubscribe();
}, [getSettings, setSettings, onChange, path, watch]);
return ( return (
<div className="flex gap-1 items-center"> <Modal>
<DropdownMenu> <Modal.Header close={close}>
<DropdownMenu.CheckboxItem Chart settings{path.length ? " for " + pathAsString(path) : ""}
label="Log X scale" </Modal.Header>
value={settings.distributionPlotSettings?.logX ?? false} <Modal.Body>
toggle={() => <ViewSettings
setSettings({ register={register}
...settings, withShowEditorSetting={false}
distributionPlotSettings: { withFunctionSettings={withFunctionSettings}
...settings.distributionPlotSettings,
logX: !settings.distributionPlotSettings?.logX,
},
})
}
/> />
<DropdownMenu.CheckboxItem </Modal.Body>
label="Exp Y scale" </Modal>
value={settings.distributionPlotSettings?.expY ?? false} );
toggle={() => };
setSettings({
...settings, export const ItemSettingsMenu: React.FC<Props> = (props) => {
distributionPlotSettings: { const [isOpen, setIsOpen] = useState(false);
...settings.distributionPlotSettings, const { setSettings, getSettings } = useContext(ViewerContext);
expY: !settings.distributionPlotSettings?.expY, const settings = getSettings(props.path);
},
}) return (
} <div className="flex gap-2">
<CogIcon
className="h-5 w-5 cursor-pointer text-slate-400 hover:text-slate-500"
onClick={() => setIsOpen(!isOpen)}
/> />
<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 ? ( {settings.distributionPlotSettings || settings.chartSettings ? (
<button <button
onClick={() => onClick={() => {
setSettings({ setSettings(props.path, {
...settings, ...settings,
distributionPlotSettings: undefined, distributionPlotSettings: undefined,
chartSettings: undefined, chartSettings: undefined,
}) });
} props.onChange();
}}
className="text-xs px-1 py-0.5 rounded bg-slate-300" className="text-xs px-1 py-0.5 rounded bg-slate-300"
> >
Reset settings Reset settings
</button> </button>
) : null} ) : null}
{isOpen ? (
<ItemSettingsModal {...props} close={() => setIsOpen(false)} />
) : null}
</div> </div>
); );
}; };

View File

@ -3,22 +3,21 @@ import { Tooltip } from "../ui/Tooltip";
import { LocalItemSettings, MergedItemSettings } from "./utils"; import { LocalItemSettings, MergedItemSettings } from "./utils";
import { ViewerContext } from "./ViewerContext"; import { ViewerContext } from "./ViewerContext";
type DropdownMenuParams = { type SettingsMenuParams = {
settings: LocalItemSettings; onChange: () => void; // used to notify VariableBox that settings have changed, so that VariableBox could re-render itself
setSettings: (value: LocalItemSettings) => void;
}; };
type VariableBoxProps = { type VariableBoxProps = {
path: string[]; path: string[];
heading: string; heading: string;
dropdownMenu?: (params: DropdownMenuParams) => React.ReactNode; renderSettingsMenu?: (params: SettingsMenuParams) => React.ReactNode;
children: (settings: MergedItemSettings) => 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, renderSettingsMenu,
children, children,
}) => { }) => {
const { setSettings, getSettings, getMergedSettings } = const { setSettings, getSettings, getMergedSettings } =
@ -57,8 +56,8 @@ export const VariableBox: React.FC<VariableBoxProps> = ({
> >
... ...
</span> </span>
) : dropdownMenu ? ( ) : renderSettingsMenu ? (
dropdownMenu({ settings, setSettings: setSettingsAndUpdate }) renderSettingsMenu({ onChange: forceUpdate })
) : null} ) : null}
</header> </header>
{settings.collapsed ? null : ( {settings.collapsed ? null : (

View File

@ -23,11 +23,11 @@ export const ViewerContext = React.createContext<ViewerContextShape>({
}, },
distributionPlotSettings: { distributionPlotSettings: {
showSummary: false, showSummary: false,
showControls: false,
logX: false, logX: false,
expY: false, expY: false,
}, },
environment: defaultEnvironment, environment: defaultEnvironment,
height: 150,
}), }),
setSettings() {}, setSettings() {},
}); });

View File

@ -72,10 +72,11 @@ export const SquiggleViewer: React.FC<Props> = ({
...environment, ...environment,
...(localSettings.environment || {}), ...(localSettings.environment || {}),
}, },
height: localSettings.height || height,
}; };
return result; return result;
}, },
[distributionPlotSettings, chartSettings, environment, getSettings] [distributionPlotSettings, chartSettings, environment, height, getSettings]
); );
return ( return (
@ -87,12 +88,7 @@ export const SquiggleViewer: React.FC<Props> = ({
}} }}
> >
{result.tag === "Ok" ? ( {result.tag === "Ok" ? (
<ExpressionViewer <ExpressionViewer path={[]} expression={result.value} width={width} />
path={[]}
expression={result.value}
width={width}
height={height}
/>
) : ( ) : (
<SquiggleErrorAlert error={result.value} /> <SquiggleErrorAlert error={result.value} />
)} )}

View File

@ -6,12 +6,14 @@ export type LocalItemSettings = {
collapsed: boolean; collapsed: boolean;
distributionPlotSettings?: Partial<DistributionPlottingSettings>; distributionPlotSettings?: Partial<DistributionPlottingSettings>;
chartSettings?: Partial<FunctionChartSettings>; chartSettings?: Partial<FunctionChartSettings>;
height?: number;
environment?: Partial<environment>; environment?: Partial<environment>;
}; };
export type MergedItemSettings = { export type MergedItemSettings = {
distributionPlotSettings: DistributionPlottingSettings; distributionPlotSettings: DistributionPlottingSettings;
chartSettings: FunctionChartSettings; chartSettings: FunctionChartSettings;
height: number;
environment: environment; environment: environment;
}; };

View File

@ -0,0 +1,155 @@
import React from "react";
import * as yup from "yup";
import { UseFormRegister } from "react-hook-form";
import { InputItem } from "./ui/InputItem";
import { Checkbox } from "./ui/Checkbox";
import { HeadedSection } from "./ui/HeadedSection";
import { Text } from "./ui/Text";
import {
defaultColor,
defaultTickFormat,
} from "../lib/distributionSpecBuilder";
export const viewSettingsSchema = yup.object({}).shape({
chartHeight: yup.number().required().positive().integer().default(350),
showSummary: yup.boolean().required(),
showEditor: yup.boolean().required(),
logX: yup.boolean().required(),
expY: yup.boolean().required(),
tickFormat: yup.string().default(defaultTickFormat),
title: yup.string(),
color: yup.string().default(defaultColor).required(),
minX: yup.number(),
maxX: yup.number(),
distributionChartActions: yup.boolean(),
diagramStart: yup.number().required().positive().integer().default(0).min(0),
diagramStop: yup.number().required().positive().integer().default(10).min(0),
diagramCount: yup.number().required().positive().integer().default(20).min(2),
});
type FormFields = yup.InferType<typeof viewSettingsSchema>;
// This component is used in two places: for global settings in SquigglePlayground, and for item-specific settings in modal dialogs.
export const ViewSettings: React.FC<{
withShowEditorSetting?: boolean;
withFunctionSettings?: boolean;
register: UseFormRegister<FormFields>;
}> = ({
withShowEditorSetting = true,
withFunctionSettings = true,
register,
}) => {
return (
<div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl">
<HeadedSection title="General Display Settings">
<div className="space-y-4">
{withShowEditorSetting ? (
<Checkbox
name="showEditor"
register={register}
label="Show code editor on left"
/>
) : null}
<InputItem
name="chartHeight"
type="number"
register={register}
label="Chart Height (in pixels)"
/>
</div>
</HeadedSection>
<div className="pt-8">
<HeadedSection title="Distribution Display Settings">
<div className="space-y-2">
<Checkbox
register={register}
name="logX"
label="Show x scale logarithmically"
/>
<Checkbox
register={register}
name="expY"
label="Show y scale exponentially"
/>
<Checkbox
register={register}
name="distributionChartActions"
label="Show vega chart controls"
/>
<Checkbox
register={register}
name="showSummary"
label="Show summary statistics"
/>
<InputItem
name="minX"
type="number"
register={register}
label="Min X Value"
/>
<InputItem
name="maxX"
type="number"
register={register}
label="Max X Value"
/>
<InputItem
name="title"
type="text"
register={register}
label="Title"
/>
<InputItem
name="tickFormat"
type="text"
register={register}
label="Tick Format"
/>
<InputItem
name="color"
type="color"
register={register}
label="Color"
/>
</div>
</HeadedSection>
</div>
{withFunctionSettings ? (
<div className="pt-8">
<HeadedSection title="Function Display Settings">
<div className="space-y-6">
<Text>
When displaying functions of single variables that return
numbers or distributions, we need to use defaults for the
x-axis. We need to select a minimum and maximum value of x to
sample, and a number n of the number of points to sample.
</Text>
<div className="space-y-4">
<InputItem
type="number"
name="diagramStart"
register={register}
label="Min X Value"
/>
<InputItem
type="number"
name="diagramStop"
register={register}
label="Max X Value"
/>
<InputItem
type="number"
name="diagramCount"
register={register}
label="Points between X min and X max to sample"
/>
</div>
</div>
</HeadedSection>
</div>
) : null}
</div>
);
};

View File

@ -0,0 +1,13 @@
import React from "react";
export const HeadedSection: React.FC<{
title: string;
children: React.ReactNode;
}> = ({ title, children }) => (
<div>
<header className="text-lg leading-6 font-medium text-gray-900">
{title}
</header>
<div className="mt-4">{children}</div>
</div>
);

View File

@ -0,0 +1,25 @@
import React from "react";
import { Path, UseFormRegister } from "react-hook-form";
export function InputItem<T>({
name,
label,
type,
register,
}: {
name: Path<T>;
label: string;
type: "number" | "text" | "color";
register: UseFormRegister<T>;
}) {
return (
<label className="block">
<div className="text-sm font-medium text-gray-600 mb-1">{label}</div>
<input
type={type}
{...register(name, { valueAsNumber: type === "number" })}
className="form-input max-w-lg block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:max-w-xs sm:text-sm border-gray-300 rounded-md"
/>
</label>
);
}

View File

@ -0,0 +1,84 @@
import { motion } from "framer-motion";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { XIcon } from "@heroicons/react/solid";
const Overlay: React.FC = () => (
<motion.div
className="absolute inset-0 -z-10 bg-black"
initial={{ opacity: 0 }}
animate={{ opacity: 0.3 }}
/>
);
const ModalHeader: React.FC<{
close: () => void;
children: React.ReactNode;
}> = ({ children, close }) => {
return (
<header className="px-5 py-3 border-b border-gray-200 font-bold flex items-center justify-between">
<div>{children}</div>
<button
className="px-1 bg-transparent cursor-pointer text-gray-700 hover:text-accent-500"
type="button"
onClick={close}
>
<XIcon className="h-5 w-5 cursor-pointer text-slate-400 hover:text-slate-500" />
</button>
</header>
);
};
// TODO - get rid of forwardRef, support `focus` and `{...hotkeys}` via smart props
const ModalBody = React.forwardRef<
HTMLDivElement,
JSX.IntrinsicElements["div"]
>(function ModalBody(props, ref) {
return <div ref={ref} className="px-5 py-3 overflow-auto" {...props} />;
});
const ModalFooter: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div className="px-5 py-3 border-t border-gray-200">{children}</div>
);
const ModalWindow: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div
className="bg-white rounded shadow-toast overflow-auto flex flex-col mx-2 w-96"
style={{ maxHeight: "calc(100% - 20px)", maxWidth: "calc(100% - 20px)" }}
>
{children}
</div>
);
type ModalType = React.FC<{ children: React.ReactNode }> & {
Body: typeof ModalBody;
Footer: typeof ModalFooter;
Header: typeof ModalHeader;
};
export const Modal: ModalType = ({ children }) => {
const [el] = React.useState(() => document.createElement("div"));
React.useEffect(() => {
document.body.appendChild(el);
return () => {
document.body.removeChild(el);
};
}, [el]);
const modal = (
<div className="squiggle">
<div className="fixed inset-0 z-40 flex justify-center items-center">
<Overlay />
<ModalWindow>{children}</ModalWindow>
</div>
</div>
);
return ReactDOM.createPortal(modal, el);
};
Modal.Body = ModalBody;
Modal.Footer = ModalFooter;
Modal.Header = ModalHeader;

View File

@ -0,0 +1,5 @@
import React from "react";
export const Text: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<p className="text-sm text-gray-500">{children}</p>
);

View File

@ -100,12 +100,15 @@ export let expYScale: PowScale = {
}, },
}; };
export const defaultTickFormat = ".9~s";
export const defaultColor = "#739ECC";
export function buildVegaSpec( export function buildVegaSpec(
specOptions: DistributionChartSpecOptions specOptions: DistributionChartSpecOptions
): VisualizationSpec { ): VisualizationSpec {
let { let {
format = ".9~s", format = defaultTickFormat,
color = "#739ECC", color = defaultColor,
title, title,
minX, minX,
maxX, maxX,

View File

@ -0,0 +1,5 @@
import { shape } from "@quri/squiggle-lang";
export const hasMassBelowZero = (shape: shape) =>
shape.continuous.some((x) => x.x <= 0) ||
shape.discrete.some((x) => x.x <= 0);