modal window for local settings
This commit is contained in:
parent
8f4259cef3
commit
eefdfbb2fe
|
@ -8,26 +8,24 @@ import {
|
|||
import { Vega } from "react-vega";
|
||||
import { ErrorAlert } from "./Alert";
|
||||
import { useSize } from "react-use";
|
||||
import clsx from "clsx";
|
||||
|
||||
import {
|
||||
buildVegaSpec,
|
||||
DistributionChartSpecOptions,
|
||||
} from "../lib/distributionSpecBuilder";
|
||||
import { NumberShower } from "./NumberShower";
|
||||
import { hasMassBelowZero } from "../lib/distributionUtils";
|
||||
|
||||
export type DistributionPlottingSettings = {
|
||||
/** Whether to show a summary of means, stdev, percentiles etc */
|
||||
showSummary: boolean;
|
||||
/** Whether to show the user graph controls (scale etc) */
|
||||
showControls: boolean;
|
||||
actions?: boolean;
|
||||
} & DistributionChartSpecOptions;
|
||||
|
||||
export type DistributionChartProps = {
|
||||
distribution: Distribution;
|
||||
width?: number;
|
||||
height: number;
|
||||
actions?: boolean;
|
||||
} & DistributionPlottingSettings;
|
||||
|
||||
export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
|
||||
|
@ -36,17 +34,9 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
|
|||
height,
|
||||
showSummary,
|
||||
width,
|
||||
showControls,
|
||||
logX,
|
||||
expY,
|
||||
actions = false,
|
||||
} = 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 [sized] = useSize((size) => {
|
||||
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);
|
||||
|
||||
let widthProp = width ? width : size.width;
|
||||
|
@ -72,7 +59,11 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
|
|||
|
||||
return (
|
||||
<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
|
||||
spec={spec}
|
||||
data={{ con: shape.value.continuous, dis: shape.value.discrete }}
|
||||
|
@ -80,67 +71,16 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
|
|||
height={height}
|
||||
actions={actions}
|
||||
/>
|
||||
) : (
|
||||
<ErrorAlert heading="Log Domain Error">
|
||||
Cannot graph distribution with negative values on logarithmic scale.
|
||||
</ErrorAlert>
|
||||
)}
|
||||
<div className="flex justify-center">
|
||||
{showSummary && <SummaryTable distribution={distribution} />}
|
||||
</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>
|
||||
);
|
||||
});
|
||||
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 }> = ({
|
||||
children,
|
||||
}) => (
|
||||
|
|
|
@ -35,8 +35,6 @@ export interface SquiggleChartProps {
|
|||
jsImports?: jsImports;
|
||||
/** Whether to show a summary of the distribution */
|
||||
showSummary?: boolean;
|
||||
/** Whether to show graph controls (scale etc)*/
|
||||
showControls?: boolean;
|
||||
/** Set the x scale to be logarithmic by deault */
|
||||
logX?: boolean;
|
||||
/** Set the y scale to be exponential by deault */
|
||||
|
@ -67,7 +65,6 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
|
|||
jsImports = defaultImports,
|
||||
showSummary = false,
|
||||
width,
|
||||
showControls = false,
|
||||
logX = false,
|
||||
expY = false,
|
||||
diagramStart = 0,
|
||||
|
@ -89,7 +86,6 @@ export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
|
|||
});
|
||||
|
||||
const distributionPlotSettings = {
|
||||
showControls,
|
||||
showSummary,
|
||||
logX,
|
||||
expY,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 { useMaybeControlledValue } from "../lib/hooks";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
|
@ -24,8 +24,15 @@ import { JsonEditor } from "./JsonEditor";
|
|||
import { ErrorAlert, SuccessAlert } from "./Alert";
|
||||
import { SquiggleContainer } from "./SquiggleContainer";
|
||||
import { Toggle } from "./ui/Toggle";
|
||||
import { Checkbox } from "./ui/Checkbox";
|
||||
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 & {
|
||||
/** The initial squiggle string to put in the playground */
|
||||
|
@ -37,90 +44,30 @@ type PlaygroundProps = SquiggleChartProps & {
|
|||
showEditor?: boolean;
|
||||
};
|
||||
|
||||
const schema = yup.object({}).shape({
|
||||
sampleCount: yup
|
||||
.number()
|
||||
.required()
|
||||
.positive()
|
||||
.integer()
|
||||
.default(1000)
|
||||
.min(10)
|
||||
.max(1000000),
|
||||
xyPointLength: yup
|
||||
.number()
|
||||
.required()
|
||||
.positive()
|
||||
.integer()
|
||||
.default(1000)
|
||||
.min(10)
|
||||
.max(10000),
|
||||
chartHeight: yup.number().required().positive().integer().default(350),
|
||||
leftSizePercent: yup
|
||||
.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),
|
||||
});
|
||||
const schema = yup
|
||||
.object({})
|
||||
.shape({
|
||||
sampleCount: yup
|
||||
.number()
|
||||
.required()
|
||||
.positive()
|
||||
.integer()
|
||||
.default(1000)
|
||||
.min(10)
|
||||
.max(1000000),
|
||||
xyPointLength: yup
|
||||
.number()
|
||||
.required()
|
||||
.positive()
|
||||
.integer()
|
||||
.default(1000)
|
||||
.min(10)
|
||||
.max(10000),
|
||||
})
|
||||
.concat(viewSettingsSchema);
|
||||
|
||||
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> }> = ({
|
||||
register,
|
||||
}) => (
|
||||
|
@ -156,123 +103,6 @@ const SamplingSettings: React.FC<{ register: UseFormRegister<FormFields> }> = ({
|
|||
</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<{
|
||||
initialImports: any; // TODO - any json type
|
||||
setImports: (imports: any) => void;
|
||||
|
@ -402,15 +232,14 @@ const useRunnerState = (code: string) => {
|
|||
export const SquigglePlayground: FC<PlaygroundProps> = ({
|
||||
defaultCode = "",
|
||||
height = 500,
|
||||
showControls = false,
|
||||
showSummary = false,
|
||||
logX = false,
|
||||
expY = false,
|
||||
title,
|
||||
minX,
|
||||
maxX,
|
||||
color = "#739ECC",
|
||||
tickFormat = ".9~s",
|
||||
color = defaultColor,
|
||||
tickFormat = defaultTickFormat,
|
||||
distributionChartActions,
|
||||
code: controlledCode,
|
||||
onCodeChange,
|
||||
|
@ -431,7 +260,6 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
|
|||
sampleCount: 1000,
|
||||
xyPointLength: 1000,
|
||||
chartHeight: 150,
|
||||
showControls,
|
||||
logX,
|
||||
expY,
|
||||
title,
|
||||
|
@ -442,8 +270,6 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
|
|||
distributionChartActions,
|
||||
showSummary,
|
||||
showEditor,
|
||||
leftSizePercent: 50,
|
||||
showSettingsPage: false,
|
||||
diagramStart: 0,
|
||||
diagramStop: 10,
|
||||
diagramCount: 20,
|
||||
|
@ -500,7 +326,13 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
|
|||
<SamplingSettings register={register} />
|
||||
</StyledTab.Panel>
|
||||
<StyledTab.Panel>
|
||||
<ViewSettings register={register} />
|
||||
<ViewSettings
|
||||
register={
|
||||
register as unknown as UseFormRegister<
|
||||
yup.InferType<typeof viewSettingsSchema>
|
||||
>
|
||||
}
|
||||
/>
|
||||
</StyledTab.Panel>
|
||||
<StyledTab.Panel>
|
||||
<InputVariablesSettings
|
||||
|
|
|
@ -6,6 +6,8 @@ import { FunctionChart, FunctionChartSettings } from "../FunctionChart";
|
|||
import clsx from "clsx";
|
||||
import { VariableBox } from "./VariableBox";
|
||||
import { ItemSettingsMenu } from "./ItemSettingsMenu";
|
||||
import { hasMassBelowZero } from "../../lib/distributionUtils";
|
||||
import { MergedItemSettings } from "./utils";
|
||||
|
||||
function getRange<a>(x: declaration<a>) {
|
||||
const first = x.args[0];
|
||||
|
@ -33,12 +35,12 @@ function getChartSettings<a>(x: declaration<a>): FunctionChartSettings {
|
|||
const VariableList: React.FC<{
|
||||
path: string[];
|
||||
heading: string;
|
||||
children: React.ReactNode;
|
||||
children: (settings: MergedItemSettings) => React.ReactNode;
|
||||
}> = ({ path, heading, children }) => (
|
||||
<VariableBox path={path} heading={heading}>
|
||||
{() => (
|
||||
{(settings) => (
|
||||
<div className={clsx("space-y-3", path.length ? "pt-1 mt-1" : null)}>
|
||||
{children}
|
||||
{children(settings)}
|
||||
</div>
|
||||
)}
|
||||
</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: string[];
|
||||
width?: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const ExpressionViewer: React.FC<Props> = ({
|
||||
path,
|
||||
expression,
|
||||
width,
|
||||
height,
|
||||
}) => {
|
||||
switch (expression.tag) {
|
||||
case "number":
|
||||
|
@ -78,9 +78,17 @@ export const ExpressionViewer: React.FC<Props> = ({
|
|||
heading={`Distribution (${distType})\n${
|
||||
distType === "Symbolic" ? expression.value.toString() : ""
|
||||
}`}
|
||||
dropdownMenu={({ settings, setSettings }) => {
|
||||
renderSettingsMenu={({ onChange }) => {
|
||||
const shape = expression.value.pointSet();
|
||||
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
|
||||
distribution={expression.value}
|
||||
{...settings.distributionPlotSettings}
|
||||
height={height}
|
||||
height={settings.height}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
|
@ -158,9 +166,13 @@ export const ExpressionViewer: React.FC<Props> = ({
|
|||
<VariableBox
|
||||
path={path}
|
||||
heading="Function"
|
||||
dropdownMenu={({ settings, setSettings }) => {
|
||||
renderSettingsMenu={({ onChange }) => {
|
||||
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}
|
||||
chartSettings={settings.chartSettings}
|
||||
distributionPlotSettings={settings.distributionPlotSettings}
|
||||
height={height}
|
||||
height={settings.height}
|
||||
environment={{
|
||||
sampleCount: settings.environment.sampleCount / 10,
|
||||
xyPointLength: settings.environment.xyPointLength / 10,
|
||||
|
@ -188,9 +200,13 @@ export const ExpressionViewer: React.FC<Props> = ({
|
|||
<VariableBox
|
||||
path={path}
|
||||
heading="Function Declaration"
|
||||
dropdownMenu={({ settings, setSettings }) => {
|
||||
renderSettingsMenu={({ onChange }) => {
|
||||
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}
|
||||
chartSettings={getChartSettings(expression.value)}
|
||||
distributionPlotSettings={settings.distributionPlotSettings}
|
||||
height={height}
|
||||
height={settings.height}
|
||||
environment={{
|
||||
sampleCount: settings.environment.sampleCount / 10,
|
||||
xyPointLength: settings.environment.xyPointLength / 10,
|
||||
|
@ -212,46 +228,49 @@ export const ExpressionViewer: React.FC<Props> = ({
|
|||
case "module": {
|
||||
return (
|
||||
<VariableList path={path} heading="Module">
|
||||
{Object.entries(expression.value)
|
||||
.filter(([key, r]) => key !== "Math")
|
||||
.map(([key, r]) => (
|
||||
<ExpressionViewer
|
||||
key={key}
|
||||
path={[...path, key]}
|
||||
expression={r}
|
||||
width={width !== undefined ? width - 20 : width}
|
||||
height={height / 3}
|
||||
/>
|
||||
))}
|
||||
{(settings) =>
|
||||
Object.entries(expression.value)
|
||||
.filter(([key, r]) => key !== "Math")
|
||||
.map(([key, r]) => (
|
||||
<ExpressionViewer
|
||||
key={key}
|
||||
path={[...path, key]}
|
||||
expression={r}
|
||||
width={width !== undefined ? width - 20 : width}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</VariableList>
|
||||
);
|
||||
}
|
||||
case "record":
|
||||
return (
|
||||
<VariableList path={path} heading="Record">
|
||||
{Object.entries(expression.value).map(([key, r]) => (
|
||||
<ExpressionViewer
|
||||
key={key}
|
||||
path={[...path, key]}
|
||||
expression={r}
|
||||
width={width !== undefined ? width - 20 : width}
|
||||
height={height / 3}
|
||||
/>
|
||||
))}
|
||||
{(settings) =>
|
||||
Object.entries(expression.value).map(([key, r]) => (
|
||||
<ExpressionViewer
|
||||
key={key}
|
||||
path={[...path, key]}
|
||||
expression={r}
|
||||
width={width !== undefined ? width - 20 : width}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</VariableList>
|
||||
);
|
||||
case "array":
|
||||
return (
|
||||
<VariableList path={path} heading="Array">
|
||||
{expression.value.map((r, i) => (
|
||||
<ExpressionViewer
|
||||
key={i}
|
||||
path={[...path, String(i)]}
|
||||
expression={r}
|
||||
width={width !== undefined ? width - 20 : width}
|
||||
height={50}
|
||||
/>
|
||||
))}
|
||||
{(settings) =>
|
||||
expression.value.map((r, i) => (
|
||||
<ExpressionViewer
|
||||
key={i}
|
||||
path={[...path, String(i)]}
|
||||
expression={r}
|
||||
width={width !== undefined ? width - 20 : width}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</VariableList>
|
||||
);
|
||||
default: {
|
||||
|
|
|
@ -1,73 +1,127 @@
|
|||
import React from "react";
|
||||
import { DropdownMenu } from "../ui/DropdownMenu";
|
||||
import { LocalItemSettings } from "./utils";
|
||||
import { CogIcon } from "@heroicons/react/solid";
|
||||
import React, { useContext, useState } from "react";
|
||||
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 = {
|
||||
settings: LocalItemSettings;
|
||||
setSettings: (value: LocalItemSettings) => void;
|
||||
path: Path;
|
||||
onChange: () => void;
|
||||
disableLogX?: boolean;
|
||||
withFunctionSettings: boolean;
|
||||
};
|
||||
|
||||
export const ItemSettingsMenu: React.FC<Props> = ({
|
||||
settings,
|
||||
setSettings,
|
||||
const ItemSettingsModal: React.FC<Props & { close: () => void }> = ({
|
||||
path,
|
||||
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 (
|
||||
<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,
|
||||
},
|
||||
})
|
||||
}
|
||||
<Modal>
|
||||
<Modal.Header close={close}>
|
||||
Chart settings{path.length ? " for " + pathAsString(path) : ""}
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<ViewSettings
|
||||
register={register}
|
||||
withShowEditorSetting={false}
|
||||
withFunctionSettings={withFunctionSettings}
|
||||
/>
|
||||
<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>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export const ItemSettingsMenu: React.FC<Props> = (props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { setSettings, getSettings } = useContext(ViewerContext);
|
||||
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)}
|
||||
/>
|
||||
{settings.distributionPlotSettings || settings.chartSettings ? (
|
||||
<button
|
||||
onClick={() =>
|
||||
setSettings({
|
||||
onClick={() => {
|
||||
setSettings(props.path, {
|
||||
...settings,
|
||||
distributionPlotSettings: undefined,
|
||||
chartSettings: undefined,
|
||||
})
|
||||
}
|
||||
});
|
||||
props.onChange();
|
||||
}}
|
||||
className="text-xs px-1 py-0.5 rounded bg-slate-300"
|
||||
>
|
||||
Reset settings
|
||||
</button>
|
||||
) : null}
|
||||
{isOpen ? (
|
||||
<ItemSettingsModal {...props} close={() => setIsOpen(false)} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -3,22 +3,21 @@ import { Tooltip } from "../ui/Tooltip";
|
|||
import { LocalItemSettings, MergedItemSettings } from "./utils";
|
||||
import { ViewerContext } from "./ViewerContext";
|
||||
|
||||
type DropdownMenuParams = {
|
||||
settings: LocalItemSettings;
|
||||
setSettings: (value: LocalItemSettings) => void;
|
||||
type SettingsMenuParams = {
|
||||
onChange: () => void; // used to notify VariableBox that settings have changed, so that VariableBox could re-render itself
|
||||
};
|
||||
|
||||
type VariableBoxProps = {
|
||||
path: string[];
|
||||
heading: string;
|
||||
dropdownMenu?: (params: DropdownMenuParams) => React.ReactNode;
|
||||
renderSettingsMenu?: (params: SettingsMenuParams) => React.ReactNode;
|
||||
children: (settings: MergedItemSettings) => React.ReactNode;
|
||||
};
|
||||
|
||||
export const VariableBox: React.FC<VariableBoxProps> = ({
|
||||
path,
|
||||
heading = "Error",
|
||||
dropdownMenu,
|
||||
renderSettingsMenu,
|
||||
children,
|
||||
}) => {
|
||||
const { setSettings, getSettings, getMergedSettings } =
|
||||
|
@ -57,8 +56,8 @@ export const VariableBox: React.FC<VariableBoxProps> = ({
|
|||
>
|
||||
...
|
||||
</span>
|
||||
) : dropdownMenu ? (
|
||||
dropdownMenu({ settings, setSettings: setSettingsAndUpdate })
|
||||
) : renderSettingsMenu ? (
|
||||
renderSettingsMenu({ onChange: forceUpdate })
|
||||
) : null}
|
||||
</header>
|
||||
{settings.collapsed ? null : (
|
||||
|
|
|
@ -23,11 +23,11 @@ export const ViewerContext = React.createContext<ViewerContextShape>({
|
|||
},
|
||||
distributionPlotSettings: {
|
||||
showSummary: false,
|
||||
showControls: false,
|
||||
logX: false,
|
||||
expY: false,
|
||||
},
|
||||
environment: defaultEnvironment,
|
||||
height: 150,
|
||||
}),
|
||||
setSettings() {},
|
||||
});
|
||||
|
|
|
@ -72,10 +72,11 @@ export const SquiggleViewer: React.FC<Props> = ({
|
|||
...environment,
|
||||
...(localSettings.environment || {}),
|
||||
},
|
||||
height: localSettings.height || height,
|
||||
};
|
||||
return result;
|
||||
},
|
||||
[distributionPlotSettings, chartSettings, environment, getSettings]
|
||||
[distributionPlotSettings, chartSettings, environment, height, getSettings]
|
||||
);
|
||||
|
||||
return (
|
||||
|
@ -87,12 +88,7 @@ export const SquiggleViewer: React.FC<Props> = ({
|
|||
}}
|
||||
>
|
||||
{result.tag === "Ok" ? (
|
||||
<ExpressionViewer
|
||||
path={[]}
|
||||
expression={result.value}
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
<ExpressionViewer path={[]} expression={result.value} width={width} />
|
||||
) : (
|
||||
<SquiggleErrorAlert error={result.value} />
|
||||
)}
|
||||
|
|
|
@ -6,12 +6,14 @@ export type LocalItemSettings = {
|
|||
collapsed: boolean;
|
||||
distributionPlotSettings?: Partial<DistributionPlottingSettings>;
|
||||
chartSettings?: Partial<FunctionChartSettings>;
|
||||
height?: number;
|
||||
environment?: Partial<environment>;
|
||||
};
|
||||
|
||||
export type MergedItemSettings = {
|
||||
distributionPlotSettings: DistributionPlottingSettings;
|
||||
chartSettings: FunctionChartSettings;
|
||||
height: number;
|
||||
environment: environment;
|
||||
};
|
||||
|
||||
|
|
155
packages/components/src/components/ViewSettings.tsx
Normal file
155
packages/components/src/components/ViewSettings.tsx
Normal 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>
|
||||
);
|
||||
};
|
13
packages/components/src/components/ui/HeadedSection.tsx
Normal file
13
packages/components/src/components/ui/HeadedSection.tsx
Normal 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>
|
||||
);
|
25
packages/components/src/components/ui/InputItem.tsx
Normal file
25
packages/components/src/components/ui/InputItem.tsx
Normal 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>
|
||||
);
|
||||
}
|
84
packages/components/src/components/ui/Modal.tsx
Normal file
84
packages/components/src/components/ui/Modal.tsx
Normal 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;
|
5
packages/components/src/components/ui/Text.tsx
Normal file
5
packages/components/src/components/ui/Text.tsx
Normal 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>
|
||||
);
|
|
@ -100,12 +100,15 @@ export let expYScale: PowScale = {
|
|||
},
|
||||
};
|
||||
|
||||
export const defaultTickFormat = ".9~s";
|
||||
export const defaultColor = "#739ECC";
|
||||
|
||||
export function buildVegaSpec(
|
||||
specOptions: DistributionChartSpecOptions
|
||||
): VisualizationSpec {
|
||||
let {
|
||||
format = ".9~s",
|
||||
color = "#739ECC",
|
||||
format = defaultTickFormat,
|
||||
color = defaultColor,
|
||||
title,
|
||||
minX,
|
||||
maxX,
|
||||
|
|
5
packages/components/src/lib/distributionUtils.ts
Normal file
5
packages/components/src/lib/distributionUtils.ts
Normal 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);
|
Loading…
Reference in New Issue
Block a user