Refactor component settings,

Also fix #1258
This commit is contained in:
Sam Nolan 2022-10-13 16:43:53 +11:00
parent 1ea3c975d5
commit 56c34de18a
14 changed files with 402 additions and 498 deletions

View File

@ -49,10 +49,10 @@ export function makePlot(record: SqRecord): Plot | void {
export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
const {
plot,
environment,
height,
showSummary,
width,
environment,
logX,
actions = false,
} = props;
@ -89,8 +89,12 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
const spec = buildVegaSpec({
...props,
minX: props.minX ?? Math.min(...domain.map((x) => x.x)),
maxX: props.minX ?? Math.max(...domain.map((x) => x.x)),
minX: Number.isFinite(props.minX)
? props.minX
: Math.min(...domain.map((x) => x.x)),
maxX: Number.isFinite(props.maxX)
? props.maxX
: Math.max(...domain.map((x) => x.x)),
maxY: Math.max(...domain.map((x) => x.y)),
});

View File

@ -1,161 +1,22 @@
import * as React from "react";
import { SqValue, environment, SqProject } from "@quri/squiggle-lang";
import { useSquiggle } from "../lib/hooks";
import { SquiggleViewer } from "./SquiggleViewer";
import { JsImports } from "../lib/jsImports";
import { useSquiggle, SquiggleArgs } from "../lib/hooks/useSquiggle";
import {
SquiggleViewer,
FlattenedViewSettings,
createViewSettings,
} from "./SquiggleViewer";
import { getValueToRender } from "../lib/utility";
export type SquiggleChartProps = {
/** The input string for squiggle */
code: string;
/** Allows to re-run the code if code hasn't changed */
executionId?: number;
/** If the output requires monte carlo sampling, the amount of samples */
sampleCount?: number;
/** If the result is a function, where the function domain starts */
diagramStart?: number;
/** If the result is a function, where the function domain ends */
diagramStop?: number;
/** If the result is a function, the amount of stops sampled */
diagramCount?: number;
/** When the squiggle code gets reevaluated */
onChange?(expr: SqValue | undefined, sourceName: string): void;
/** CSS width of the element */
width?: number;
height?: number;
/** JS imported parameters */
jsImports?: JsImports;
/** Whether to show a summary of the distribution */
showSummary?: boolean;
/** Set the x scale to be logarithmic by deault */
logX?: boolean;
/** Set the y scale to be exponential by deault */
expY?: boolean;
/** How to format numbers on the x axis */
tickFormat?: string;
/** Title of the graphed distribution */
title?: string;
/** Color of the graphed distribution */
color?: string;
/** Specify the lower bound of the x scale */
minX?: number;
/** Specify the upper bound of the x scale */
maxX?: number;
/** Whether the x-axis should be dates or numbers */
xAxisType?: "number" | "dateTime";
/** Whether to show vega actions to the user, so they can copy the chart spec */
distributionChartActions?: boolean;
enableLocalSettings?: boolean;
} & (StandaloneExecutionProps | ProjectExecutionProps);
// Props needed for a standalone execution
type StandaloneExecutionProps = {
project?: undefined;
continues?: undefined;
/** The amount of points returned to draw the distribution, not needed if using a project */
environment?: environment;
};
// Props needed when executing inside a project.
type ProjectExecutionProps = {
environment?: undefined;
/** The project that this execution is part of */
project: SqProject;
/** What other squiggle sources from the project to continue. Default [] */
continues?: string[];
};
const defaultOnChange = () => {};
const defaultImports: JsImports = {};
const defaultContinues: string[] = [];
export const splitSquiggleChartSettings = (props: SquiggleChartProps) => {
const {
showSummary = false,
logX = false,
expY = false,
diagramStart = 0,
diagramStop = 10,
diagramCount = 20,
tickFormat,
minX,
maxX,
color,
title,
xAxisType = "number",
distributionChartActions,
} = props;
const distributionPlotSettings = {
showSummary,
logX,
expY,
format: tickFormat,
minX,
maxX,
color,
title,
xAxisType,
actions: distributionChartActions,
};
const chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
return { distributionPlotSettings, chartSettings };
};
export type SquiggleChartProps = SquiggleArgs & FlattenedViewSettings;
export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
(props) => {
const { distributionPlotSettings, chartSettings } =
splitSquiggleChartSettings(props);
const {
code,
jsImports = defaultImports,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
executionId = 0,
width,
height = 200,
enableLocalSettings = false,
continues = defaultContinues,
} = props;
const p = React.useMemo(() => {
if (props.project) {
return props.project;
} else {
const p = SqProject.create();
if (props.environment) {
p.setEnvironment(props.environment);
}
return p;
}
}, [props.project, props.environment]);
const resultAndBindings = useSquiggle({
continues,
project: p,
code,
jsImports,
onChange,
executionId,
});
const resultAndBindings = useSquiggle(props);
const valueToRender = getValueToRender(resultAndBindings);
return (
<SquiggleViewer
result={valueToRender}
width={width}
height={height}
distributionPlotSettings={distributionPlotSettings}
chartSettings={chartSettings}
environment={p.getEnvironment()}
enableLocalSettings={enableLocalSettings}
/>
<SquiggleViewer {...createViewSettings(props)} result={valueToRender} />
);
}
);

View File

@ -1,14 +1,14 @@
import React from "react";
import { CodeEditor } from "./CodeEditor";
import { SquiggleContainer } from "./SquiggleContainer";
import { useMaybeControlledValue } from "../lib/hooks";
import { useSquiggle, SquiggleArgs } from "../lib/hooks/useSquiggle";
import { SqLocation } from "@quri/squiggle-lang";
import {
splitSquiggleChartSettings,
SquiggleChartProps,
} from "./SquiggleChart";
import { useMaybeControlledValue, useSquiggle } from "../lib/hooks";
import { JsImports } from "../lib/jsImports";
import { defaultEnvironment, SqLocation, SqProject } from "@quri/squiggle-lang";
import { SquiggleViewer } from "./SquiggleViewer";
SquiggleViewer,
createViewSettings,
FlattenedViewSettings,
} from "./SquiggleViewer";
import { getErrorLocations, getValueToRender } from "../lib/utility";
const WrappedCodeEditor: React.FC<{
@ -28,13 +28,11 @@ const WrappedCodeEditor: React.FC<{
</div>
);
export type SquiggleEditorProps = SquiggleChartProps & {
defaultCode?: string;
onCodeChange?: (code: string) => void;
};
const defaultOnChange = () => {};
const defaultImports: JsImports = {};
export type SquiggleEditorProps = SquiggleArgs &
FlattenedViewSettings & {
defaultCode?: string;
onCodeChange?: (code: string) => void;
};
export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
const [code, setCode] = useMaybeControlledValue({
@ -43,34 +41,7 @@ export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
onChange: props.onCodeChange,
});
const { distributionPlotSettings, chartSettings } =
splitSquiggleChartSettings(props);
const {
environment,
jsImports = defaultImports,
onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here
executionId = 0,
width,
height = 200,
enableLocalSettings = false,
} = props;
const project = React.useMemo(() => {
const p = SqProject.create();
if (environment) {
p.setEnvironment(environment);
}
return p;
}, [environment]);
const resultAndBindings = useSquiggle({
code,
project,
jsImports,
onChange,
executionId,
});
const resultAndBindings = useSquiggle({ ...props, code });
const valueToRender = getValueToRender(resultAndBindings);
const errorLocations = getErrorLocations(resultAndBindings.result);
@ -82,15 +53,7 @@ export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
setCode={setCode}
errorLocations={errorLocations}
/>
<SquiggleViewer
result={valueToRender}
width={width}
height={height}
distributionPlotSettings={distributionPlotSettings}
chartSettings={chartSettings}
environment={environment ?? defaultEnvironment}
enableLocalSettings={enableLocalSettings}
/>
<SquiggleViewer result={valueToRender} {...createViewSettings(props)} />
</SquiggleContainer>
);
};

View File

@ -13,6 +13,7 @@ import {
useRunnerState,
useSquiggle,
} from "../lib/hooks";
import { SquiggleArgs } from "../lib/hooks/useSquiggle";
import { yupResolver } from "@hookform/resolvers/yup";
import {
ChartSquareBarIcon,
@ -28,9 +29,8 @@ import {
} from "@heroicons/react/solid";
import clsx from "clsx";
import { environment, SqProject } from "@quri/squiggle-lang";
import { environment } from "@quri/squiggle-lang";
import { SquiggleChartProps } from "./SquiggleChart";
import { CodeEditor } from "./CodeEditor";
import { JsonEditor } from "./JsonEditor";
import { ErrorAlert, SuccessAlert } from "./Alert";
@ -41,23 +41,27 @@ import { InputItem } from "./ui/InputItem";
import { Text } from "./ui/Text";
import { ViewSettings, viewSettingsSchema } from "./ViewSettings";
import { HeadedSection } from "./ui/HeadedSection";
import { defaultTickFormat } from "../lib/distributionSpecBuilder";
import { Button } from "./ui/Button";
import { JsImports } from "../lib/jsImports";
import { getErrorLocations, getValueToRender } from "../lib/utility";
import { SquiggleViewer } from "./SquiggleViewer";
import {
SquiggleViewer,
FlattenedViewSettings,
createViewSettings,
} from "./SquiggleViewer";
type PlaygroundProps = SquiggleChartProps & {
/** The initial squiggle string to put in the playground */
defaultCode?: string;
onCodeChange?(expr: string): void;
/* When settings change */
onSettingsChange?(settings: any): void;
/** Should we show the editor? */
showEditor?: boolean;
/** Useful for playground on squiggle website, where we update the anchor link based on current code and settings */
showShareButton?: boolean;
};
type PlaygroundProps = SquiggleArgs &
FlattenedViewSettings & {
/** The initial squiggle string to put in the playground */
defaultCode?: string;
onCodeChange?(expr: string): void;
/* When settings change */
onSettingsChange?(settings: any): void;
/** Should we show the editor? */
showEditor?: boolean;
/** Useful for playground on squiggle website, where we update the anchor link based on current code and settings */
showShareButton?: boolean;
};
const schema = yup
.object({})
@ -78,6 +82,7 @@ const schema = yup
.default(1000)
.min(10)
.max(10000),
showEditor: yup.boolean().required().default(true),
})
.concat(viewSettingsSchema);
@ -235,23 +240,14 @@ export const PlaygroundContext = React.createContext<PlaygroundContextShape>({
getLeftPanelElement: () => undefined,
});
export const SquigglePlayground: FC<PlaygroundProps> = ({
defaultCode = "",
height = 500,
showSummary = true,
logX = false,
expY = false,
title,
minX,
maxX,
tickFormat = defaultTickFormat,
distributionChartActions,
code: controlledCode,
onCodeChange,
onSettingsChange,
showEditor = true,
showShareButton = false,
}) => {
export const SquigglePlayground: FC<PlaygroundProps> = (props) => {
const {
defaultCode = "",
code: controlledCode,
onCodeChange,
onSettingsChange,
showShareButton = false,
} = props;
const [code, setCode] = useMaybeControlledValue({
value: controlledCode,
defaultValue: defaultCode,
@ -260,29 +256,19 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
const [imports, setImports] = useState<JsImports>({});
let defaultValues: FormFields = {
...schema.getDefault(),
...props,
};
const { register, control } = useForm({
resolver: yupResolver(schema),
defaultValues: {
sampleCount: 1000,
xyPointLength: 1000,
chartHeight: 150,
logX,
expY,
title,
minX,
maxX,
tickFormat,
distributionChartActions,
showSummary,
showEditor,
diagramStart: 0,
diagramStop: 10,
diagramCount: 20,
},
defaultValues: defaultValues,
});
const vars = useWatch({
const rawVars = useWatch({
control,
});
let vars = useMemo(() => ({ ...schema.getDefault(), ...rawVars }), [rawVars]);
useEffect(() => {
onSettingsChange?.(vars);
@ -305,20 +291,12 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
executionId,
} = useRunnerState(code);
const project = React.useMemo(() => {
const p = SqProject.create();
if (environment) {
p.setEnvironment(environment);
}
return p;
}, [environment]);
const resultAndBindings = useSquiggle({
code,
project,
jsImports: imports,
executionId,
});
let args: SquiggleArgs = props;
args = { ...args, code, jsImports: imports, executionId };
if (!args.project) {
args = { ...args, environment };
}
const resultAndBindings = useSquiggle(args);
const valueToRender = getValueToRender(resultAndBindings);
@ -328,27 +306,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
{isRunning ? (
<div className="absolute inset-0 bg-white opacity-0 animate-semi-appear" />
) : null}
<SquiggleViewer
result={valueToRender}
environment={environment}
height={vars.chartHeight || 150}
distributionPlotSettings={{
showSummary: vars.showSummary ?? false,
logX: vars.logX ?? false,
expY: vars.expY ?? false,
format: vars.tickFormat,
minX: vars.minX,
maxX: vars.maxX,
title: vars.title,
actions: vars.distributionChartActions,
}}
chartSettings={{
start: vars.diagramStart ?? 0,
stop: vars.diagramStop ?? 10,
count: vars.diagramCount ?? 20,
}}
enableLocalSettings={true}
/>
<SquiggleViewer {...createViewSettings(vars)} result={valueToRender} />
</div>
);
@ -363,7 +321,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
onSubmit={run}
oneLine={false}
showGutter={true}
height={height - 1}
height={(props.chartHeight ?? 200) - 1}
/>
</div>
) : (
@ -402,7 +360,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
<div className="flex mt-2">
<div
className="w-1/2 relative"
style={{ minHeight: height }}
style={{ minHeight: props.chartHeight }}
ref={leftPanelRef}
>
{tabs}

View File

@ -105,9 +105,9 @@ export const ExpressionViewer: React.FC<Props> = ({ value, width }) => {
return (
<DistributionChart
plot={defaultPlot(value.value)}
environment={settings.environment}
{...settings.distributionPlotSettings}
height={settings.height}
height={settings.chartHeight}
environment={settings.environment}
width={width}
/>
);
@ -178,7 +178,7 @@ export const ExpressionViewer: React.FC<Props> = ({ value, width }) => {
fn={value.value}
chartSettings={settings.chartSettings}
distributionPlotSettings={settings.distributionPlotSettings}
height={settings.height}
height={settings.chartHeight}
environment={{
sampleCount: settings.environment.sampleCount / 10,
xyPointLength: settings.environment.xyPointLength / 10,
@ -203,7 +203,7 @@ export const ExpressionViewer: React.FC<Props> = ({ value, width }) => {
);
}}
>
{(settings) => (
{(_) => (
<div>NOT IMPLEMENTED IN 0.4 YET</div>
// <FunctionChart
// fn={expression.value.fn}
@ -252,7 +252,7 @@ export const ExpressionViewer: React.FC<Props> = ({ value, width }) => {
plot={plot}
environment={settings.environment}
{...settings.distributionPlotSettings}
height={settings.height}
height={settings.chartHeight}
width={width}
/>
);

View File

@ -3,9 +3,13 @@ import React, { useContext, useRef, useState, useEffect } 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 {
ViewSettings,
viewSettingsSchema,
mergedToViewSettings,
viewSettingsToLocal,
} from "../ViewSettings";
import { ViewerContext } from "./ViewerContext";
import { defaultTickFormat } from "../../lib/distributionSpecBuilder";
import { PlaygroundContext } from "../SquigglePlayground";
import { SqValue } from "@quri/squiggle-lang";
import { locationAsString } from "./utils";
@ -34,44 +38,14 @@ const ItemSettingsModal: React.FC<
const { register, watch } = useForm({
resolver: yupResolver(viewSettingsSchema),
defaultValues: {
// this is a mess and should be fixed
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,
minX: mergedSettings.distributionPlotSettings.minX,
maxX: mergedSettings.distributionPlotSettings.maxX,
distributionChartActions: mergedSettings.distributionPlotSettings.actions,
diagramStart: mergedSettings.chartSettings.start,
diagramStop: mergedSettings.chartSettings.stop,
diagramCount: mergedSettings.chartSettings.count,
},
defaultValues: mergedToViewSettings(mergedSettings),
});
useEffect(() => {
const subscription = watch((vars) => {
const settings = getSettings(value.location); // get the latest version
setSettings(value.location, {
...settings,
distributionPlotSettings: {
showSummary: vars.showSummary,
logX: vars.logX,
expY: vars.expY,
format: vars.tickFormat,
title: vars.title,
minX: vars.minX,
maxX: vars.maxX,
actions: vars.distributionChartActions,
},
chartSettings: {
start: vars.diagramStart,
stop: vars.diagramStop,
count: vars.diagramCount,
},
...viewSettingsToLocal(vars),
});
onChange();
});
@ -102,7 +76,6 @@ const ItemSettingsModal: React.FC<
<Modal.Body>
<ViewSettings
register={register}
withShowEditorSetting={false}
withFunctionSettings={withFunctionSettings}
disableLogXSetting={disableLogX}
/>

View File

@ -1,6 +1,7 @@
import { defaultEnvironment, SqValueLocation } from "@quri/squiggle-lang";
import React from "react";
import { LocalItemSettings, MergedItemSettings } from "./utils";
import { viewSettingsSchema, viewSettingsToMerged } from "../ViewSettings";
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).
@ -16,19 +17,8 @@ export const ViewerContext = React.createContext<ViewerContextShape>({
getSettings: () => ({ collapsed: false }),
getMergedSettings: () => ({
collapsed: false,
// copy-pasted from SquiggleChart
chartSettings: {
start: 0,
stop: 10,
count: 100,
},
distributionPlotSettings: {
showSummary: false,
logX: false,
expY: false,
},
environment: defaultEnvironment,
height: 150,
...viewSettingsToMerged(viewSettingsSchema.getDefault()),
}),
setSettings() {},
enableLocalSettings: false,

View File

@ -1,7 +1,5 @@
import React, { useCallback, useRef } from "react";
import { environment, SqValueLocation } from "@quri/squiggle-lang";
import { DistributionPlottingSettings } from "../DistributionChart";
import { FunctionChartSettings } from "../FunctionChart";
import { SqValueLocation } from "@quri/squiggle-lang";
import { ExpressionViewer } from "./ExpressionViewer";
import { ViewerContext } from "./ViewerContext";
import {
@ -10,20 +8,40 @@ import {
MergedItemSettings,
} from "./utils";
import { useSquiggle } from "../../lib/hooks";
import {
EditableViewSettings,
viewSettingsSchema,
viewSettingsToMerged,
} from "../ViewSettings";
import { SquiggleErrorAlert } from "../SquiggleErrorAlert";
// Flattened view settings, gets turned into props for SquiggleChart and SquiggleEditor
export type FlattenedViewSettings = Partial<
EditableViewSettings & {
width?: number;
enableLocalSettings?: boolean;
}
>;
type ViewSettings = {
width?: number;
enableLocalSettings?: boolean;
} & Omit<MergedItemSettings, "environment">;
export const createViewSettings = (
props: FlattenedViewSettings
): ViewSettings => {
const propsWithDefaults = { ...viewSettingsSchema.getDefault(), ...props };
let merged = viewSettingsToMerged(propsWithDefaults);
const { width, enableLocalSettings } = propsWithDefaults;
return { ...merged, width, enableLocalSettings };
};
type Props = {
/** The output of squiggle's run */
result: ReturnType<typeof useSquiggle>["result"];
width?: number;
height: number;
distributionPlotSettings: DistributionPlottingSettings;
/** Settings for displaying functions */
chartSettings: FunctionChartSettings;
/** Environment for further function executions */
environment: environment;
enableLocalSettings?: boolean;
};
} & ViewSettings;
type Settings = {
[k: string]: LocalItemSettings;
@ -34,10 +52,9 @@ const defaultSettings: LocalItemSettings = { collapsed: false };
export const SquiggleViewer: React.FC<Props> = ({
result,
width,
height,
chartHeight,
distributionPlotSettings,
chartSettings,
environment,
enableLocalSettings = false,
}) => {
// can't store settings in the state because we don't want to rerender the entire tree on every change
@ -59,6 +76,7 @@ export const SquiggleViewer: React.FC<Props> = ({
const getMergedSettings = useCallback(
(location: SqValueLocation) => {
const env = location.project.getEnvironment();
const localSettings = getSettings(location);
const result: MergedItemSettings = {
distributionPlotSettings: {
@ -70,14 +88,14 @@ export const SquiggleViewer: React.FC<Props> = ({
...(localSettings.chartSettings || {}),
},
environment: {
...environment,
...(localSettings.environment || {}),
...env,
...localSettings.environment,
},
height: localSettings.height || height,
chartHeight: localSettings.chartHeight || chartHeight,
};
return result;
},
[distributionPlotSettings, chartSettings, environment, height, getSettings]
[distributionPlotSettings, chartSettings, chartHeight, getSettings]
);
return (

View File

@ -1,19 +1,19 @@
import { DistributionPlottingSettings } from "../DistributionChart";
import { FunctionChartSettings } from "../FunctionChart";
import { environment, SqValueLocation } from "@quri/squiggle-lang";
import { SqValueLocation, environment } from "@quri/squiggle-lang";
export type LocalItemSettings = {
collapsed: boolean;
distributionPlotSettings?: Partial<DistributionPlottingSettings>;
chartSettings?: Partial<FunctionChartSettings>;
height?: number;
environment?: Partial<environment>;
chartHeight?: number;
environment?: environment;
};
export type MergedItemSettings = {
distributionPlotSettings: DistributionPlottingSettings;
chartSettings: FunctionChartSettings;
height: number;
chartHeight: number;
environment: environment;
};

View File

@ -5,49 +5,137 @@ import { InputItem } from "./ui/InputItem";
import { Checkbox } from "./ui/Checkbox";
import { HeadedSection } from "./ui/HeadedSection";
import { Text } from "./ui/Text";
import { MergedItemSettings, LocalItemSettings } from "./SquiggleViewer/utils";
import { 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),
showSummary: yup.boolean().required().default(false),
logX: yup.boolean().required().default(false),
expY: yup.boolean().required().default(false),
tickFormat: yup.string().required().default(defaultTickFormat),
title: yup.string(),
minX: yup.number(),
maxX: yup.number(),
xAxisType: yup
.mixed<"number" | "dateTime">()
.oneOf(["number", "dateTime"])
.default("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>;
export type EditableViewSettings = yup.InferType<typeof viewSettingsSchema>;
export const viewSettingsToMerged = (
settings: EditableViewSettings
): Omit<MergedItemSettings, "environment"> => {
const {
showSummary,
logX,
expY,
diagramStart,
diagramStop,
diagramCount,
tickFormat,
minX,
maxX,
title,
xAxisType,
distributionChartActions,
chartHeight,
} = settings;
const distributionPlotSettings = {
showSummary,
logX,
expY,
format: tickFormat,
minX,
maxX,
title,
xAxisType,
actions: distributionChartActions,
};
const chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
return { distributionPlotSettings, chartSettings, chartHeight };
};
export const viewSettingsToLocal = (
settings: Partial<EditableViewSettings>
): Omit<LocalItemSettings, "collapsed" | "environment"> => {
const {
showSummary,
logX,
expY,
diagramStart,
diagramStop,
diagramCount,
tickFormat,
minX,
maxX,
title,
xAxisType,
distributionChartActions,
chartHeight,
} = settings;
const distributionPlotSettings = {
showSummary,
logX,
expY,
format: tickFormat,
minX,
maxX,
title,
xAxisType,
actions: distributionChartActions,
};
const chartSettings = {
start: diagramStart,
stop: diagramStop,
count: diagramCount,
};
return { distributionPlotSettings, chartSettings, chartHeight };
};
export const mergedToViewSettings = (
mergedSettings: MergedItemSettings
): EditableViewSettings => ({
chartHeight: mergedSettings.chartHeight,
showSummary: mergedSettings.distributionPlotSettings.showSummary,
logX: mergedSettings.distributionPlotSettings.logX,
expY: mergedSettings.distributionPlotSettings.expY,
tickFormat: mergedSettings.distributionPlotSettings.format,
title: mergedSettings.distributionPlotSettings.title,
minX: mergedSettings.distributionPlotSettings.minX,
maxX: mergedSettings.distributionPlotSettings.maxX,
distributionChartActions: mergedSettings.distributionPlotSettings.actions,
xAxisType: mergedSettings.distributionPlotSettings.xAxisType,
diagramStart: mergedSettings.chartSettings.start,
diagramStop: mergedSettings.chartSettings.stop,
diagramCount: mergedSettings.chartSettings.count,
});
// 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;
disableLogXSetting?: boolean;
register: UseFormRegister<FormFields>;
}> = ({
withShowEditorSetting = true,
withFunctionSettings = true,
disableLogXSetting,
register,
}) => {
register: UseFormRegister<EditableViewSettings>;
}> = ({ withFunctionSettings = true, disableLogXSetting, 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"
@ -57,97 +145,115 @@ export const ViewSettings: React.FC<{
</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"
disabled={disableLogXSetting}
tooltip={
disableLogXSetting
? "Your distribution has mass lower than or equal to 0. Log only works on strictly positive values."
: undefined
}
/>
<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"
/>
</div>
</HeadedSection>
</div>
<DistributionViewSettings
disableLogXSetting={disableLogXSetting}
register={register}
/>
{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>
<FunctionViewSettings register={register} />
) : null}
</div>
);
};
export const DistributionViewSettings: React.FC<{
disableLogXSetting?: boolean;
register: UseFormRegister<EditableViewSettings>;
}> = ({ disableLogXSetting, register }) => {
return (
<div className="pt-8">
<HeadedSection title="Distribution Display Settings">
<div className="space-y-2">
<Checkbox
register={register}
name="logX"
label="Show x scale logarithmically"
disabled={disableLogXSetting}
tooltip={
disableLogXSetting
? "Your distribution has mass lower than or equal to 0. Log only works on strictly positive values."
: undefined
}
/>
<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"
/>
</div>
</HeadedSection>
</div>
);
};
export const FunctionViewSettings: React.FC<{
register: UseFormRegister<EditableViewSettings>;
}> = ({ register }) => (
<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>
);

View File

@ -13,9 +13,9 @@ export type DistributionChartSpecOptions = {
/** The title of the chart */
title?: string;
/** The formatting of the ticks */
format?: string;
format: string;
/** Whether the x-axis should be dates or numbers */
xAxisType?: "number" | "dateTime";
xAxisType: "number" | "dateTime";
};
/** X Scales */
@ -70,15 +70,7 @@ const width = 500;
export function buildVegaSpec(
specOptions: DistributionChartSpecOptions & { maxY: number }
): VisualizationSpec {
const {
title,
minX,
maxX,
logX,
expY,
xAxisType = "number",
maxY,
} = specOptions;
const { title, minX, maxX, logX, expY, xAxisType, maxY } = specOptions;
const dateTime = xAxisType === "dateTime";

View File

@ -1,6 +1,7 @@
import {
result,
SqError,
environment,
SqProject,
SqRecord,
SqValue,
@ -9,13 +10,31 @@ import { useEffect, useMemo } from "react";
import { JsImports, jsImportsToSquiggleCode } from "../jsImports";
import * as uuid from "uuid";
type SquiggleArgs = {
export type SquiggleArgs = {
code: string;
executionId?: number;
jsImports?: JsImports;
onChange?: (
expr: result<SqValue, SqError> | undefined,
sourceName: string
) => void;
} & (StandaloneExecutionProps | ProjectExecutionProps);
// Props needed for a standalone execution
type StandaloneExecutionProps = {
project?: undefined;
continues?: undefined;
/** The amount of points returned to draw the distribution, not needed if using a project */
environment?: environment;
};
// Props needed when executing inside a project.
type ProjectExecutionProps = {
environment?: undefined;
/** The project that this execution is part of */
project: SqProject;
/** What other squiggle sources from the project to continue. Default [] */
continues?: string[];
onChange?: (expr: SqValue | undefined, sourceName: string) => void;
};
export type ResultAndBindings = {
@ -29,12 +48,24 @@ const defaultContinues = [];
export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => {
const sourceName = useMemo(() => uuid.v4(), []);
const env = args.project.getEnvironment();
const p = useMemo(() => {
if (args.project) {
return args.project;
} else {
const p = SqProject.create();
if (args.environment) {
p.setEnvironment(args.environment);
}
return p;
}
}, [args.project, args.environment]);
const env = p.getEnvironment();
const continues = args.continues || defaultContinues;
const result = useMemo(
() => {
const project = args.project;
const project = p;
project.setSource(sourceName, args.code);
let fullContinues = continues;
@ -61,25 +92,23 @@ export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => {
continues,
args.project,
env,
p,
]
);
const { onChange } = args;
useEffect(() => {
onChange?.(
result.result.tag === "Ok" ? result.result.value : undefined,
sourceName
);
onChange?.(result.result, sourceName);
}, [result, onChange, sourceName]);
useEffect(() => {
return () => {
args.project.removeSource(sourceName);
if (args.project.getSource(importSourceName(sourceName)))
args.project.removeSource(importSourceName(sourceName));
p.removeSource(sourceName);
if (p.getSource(importSourceName(sourceName)))
p.removeSource(importSourceName(sourceName));
};
}, [args.project, sourceName]);
}, [p, sourceName]);
return result;
};

View File

@ -250,5 +250,3 @@ to allow large and small numbers being printed cleanly.
{Template.bind({})}
</Story>
</Canvas>
<Props of={SquiggleChart} />

View File

@ -0,0 +1,12 @@
import { render } from "@testing-library/react";
import React from "react";
import "@testing-library/jest-dom";
import { SquiggleChart } from "../src/index";
test("showSummary prop shows table", async () => {
const { container } = render(
<SquiggleChart code={"normal(5, 1)"} showSummary={true} />
);
expect(container).toHaveTextContent("Mean");
expect(container).toHaveTextContent("5");
});