Compare commits

...

2 Commits

Author SHA1 Message Date
Sam Nolan
2b89c2f228 Merge branch 'develop' into refactor-component-settings 2022-10-13 16:48:41 +11:00
Sam Nolan
56c34de18a Refactor component settings,
Also fix #1258
2022-10-13 16:43:53 +11:00
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) => { export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
const { const {
plot, plot,
environment,
height, height,
showSummary, showSummary,
width, width,
environment,
logX, logX,
actions = false, actions = false,
} = props; } = props;
@ -89,8 +89,12 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
const spec = buildVegaSpec({ const spec = buildVegaSpec({
...props, ...props,
minX: props.minX ?? Math.min(...domain.map((x) => x.x)), minX: Number.isFinite(props.minX)
maxX: props.minX ?? Math.max(...domain.map((x) => x.x)), ? 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)), maxY: Math.max(...domain.map((x) => x.y)),
}); });

View File

@ -1,161 +1,22 @@
import * as React from "react"; import * as React from "react";
import { SqValue, environment, SqProject } from "@quri/squiggle-lang"; import { useSquiggle, SquiggleArgs } from "../lib/hooks/useSquiggle";
import { useSquiggle } from "../lib/hooks"; import {
import { SquiggleViewer } from "./SquiggleViewer"; SquiggleViewer,
import { JsImports } from "../lib/jsImports"; FlattenedViewSettings,
createViewSettings,
} from "./SquiggleViewer";
import { getValueToRender } from "../lib/utility"; import { getValueToRender } from "../lib/utility";
export type SquiggleChartProps = { export type SquiggleChartProps = SquiggleArgs & FlattenedViewSettings;
/** 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 const SquiggleChart: React.FC<SquiggleChartProps> = React.memo( export const SquiggleChart: React.FC<SquiggleChartProps> = React.memo(
(props) => { (props) => {
const { distributionPlotSettings, chartSettings } = const resultAndBindings = useSquiggle(props);
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 valueToRender = getValueToRender(resultAndBindings); const valueToRender = getValueToRender(resultAndBindings);
return ( return (
<SquiggleViewer <SquiggleViewer {...createViewSettings(props)} result={valueToRender} />
result={valueToRender}
width={width}
height={height}
distributionPlotSettings={distributionPlotSettings}
chartSettings={chartSettings}
environment={p.getEnvironment()}
enableLocalSettings={enableLocalSettings}
/>
); );
} }
); );

View File

@ -1,14 +1,14 @@
import React from "react"; import React from "react";
import { CodeEditor } from "./CodeEditor"; import { CodeEditor } from "./CodeEditor";
import { SquiggleContainer } from "./SquiggleContainer"; import { SquiggleContainer } from "./SquiggleContainer";
import { useMaybeControlledValue } from "../lib/hooks";
import { useSquiggle, SquiggleArgs } from "../lib/hooks/useSquiggle";
import { SqLocation } from "@quri/squiggle-lang";
import { import {
splitSquiggleChartSettings, SquiggleViewer,
SquiggleChartProps, createViewSettings,
} from "./SquiggleChart"; FlattenedViewSettings,
import { useMaybeControlledValue, useSquiggle } from "../lib/hooks"; } from "./SquiggleViewer";
import { JsImports } from "../lib/jsImports";
import { defaultEnvironment, SqLocation, SqProject } from "@quri/squiggle-lang";
import { SquiggleViewer } from "./SquiggleViewer";
import { getErrorLocations, getValueToRender } from "../lib/utility"; import { getErrorLocations, getValueToRender } from "../lib/utility";
const WrappedCodeEditor: React.FC<{ const WrappedCodeEditor: React.FC<{
@ -28,13 +28,11 @@ const WrappedCodeEditor: React.FC<{
</div> </div>
); );
export type SquiggleEditorProps = SquiggleChartProps & { export type SquiggleEditorProps = SquiggleArgs &
FlattenedViewSettings & {
defaultCode?: string; defaultCode?: string;
onCodeChange?: (code: string) => void; onCodeChange?: (code: string) => void;
}; };
const defaultOnChange = () => {};
const defaultImports: JsImports = {};
export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => { export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
const [code, setCode] = useMaybeControlledValue({ const [code, setCode] = useMaybeControlledValue({
@ -43,34 +41,7 @@ export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
onChange: props.onCodeChange, onChange: props.onCodeChange,
}); });
const { distributionPlotSettings, chartSettings } = const resultAndBindings = useSquiggle({ ...props, code });
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 valueToRender = getValueToRender(resultAndBindings); const valueToRender = getValueToRender(resultAndBindings);
const errorLocations = getErrorLocations(resultAndBindings.result); const errorLocations = getErrorLocations(resultAndBindings.result);
@ -82,15 +53,7 @@ export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
setCode={setCode} setCode={setCode}
errorLocations={errorLocations} errorLocations={errorLocations}
/> />
<SquiggleViewer <SquiggleViewer result={valueToRender} {...createViewSettings(props)} />
result={valueToRender}
width={width}
height={height}
distributionPlotSettings={distributionPlotSettings}
chartSettings={chartSettings}
environment={environment ?? defaultEnvironment}
enableLocalSettings={enableLocalSettings}
/>
</SquiggleContainer> </SquiggleContainer>
); );
}; };

View File

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

View File

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

View File

@ -3,9 +3,13 @@ import React, { useContext, useRef, useState, useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup"; import { yupResolver } from "@hookform/resolvers/yup";
import { Modal } from "../ui/Modal"; import { Modal } from "../ui/Modal";
import { ViewSettings, viewSettingsSchema } from "../ViewSettings"; import {
ViewSettings,
viewSettingsSchema,
mergedToViewSettings,
viewSettingsToLocal,
} from "../ViewSettings";
import { ViewerContext } from "./ViewerContext"; import { ViewerContext } from "./ViewerContext";
import { defaultTickFormat } from "../../lib/distributionSpecBuilder";
import { PlaygroundContext } from "../SquigglePlayground"; import { PlaygroundContext } from "../SquigglePlayground";
import { SqValue } from "@quri/squiggle-lang"; import { SqValue } from "@quri/squiggle-lang";
import { locationAsString } from "./utils"; import { locationAsString } from "./utils";
@ -34,44 +38,14 @@ const ItemSettingsModal: React.FC<
const { register, watch } = useForm({ const { register, watch } = useForm({
resolver: yupResolver(viewSettingsSchema), resolver: yupResolver(viewSettingsSchema),
defaultValues: { defaultValues: mergedToViewSettings(mergedSettings),
// 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,
},
}); });
useEffect(() => { useEffect(() => {
const subscription = watch((vars) => { const subscription = watch((vars) => {
const settings = getSettings(value.location); // get the latest version const settings = getSettings(value.location); // get the latest version
setSettings(value.location, { setSettings(value.location, {
...settings, ...settings,
distributionPlotSettings: { ...viewSettingsToLocal(vars),
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,
},
}); });
onChange(); onChange();
}); });
@ -102,7 +76,6 @@ const ItemSettingsModal: React.FC<
<Modal.Body> <Modal.Body>
<ViewSettings <ViewSettings
register={register} register={register}
withShowEditorSetting={false}
withFunctionSettings={withFunctionSettings} withFunctionSettings={withFunctionSettings}
disableLogXSetting={disableLogX} disableLogXSetting={disableLogX}
/> />

View File

@ -1,6 +1,7 @@
import { defaultEnvironment, SqValueLocation } from "@quri/squiggle-lang"; import { defaultEnvironment, SqValueLocation } from "@quri/squiggle-lang";
import React from "react"; import React from "react";
import { LocalItemSettings, MergedItemSettings } from "./utils"; import { LocalItemSettings, MergedItemSettings } from "./utils";
import { viewSettingsSchema, viewSettingsToMerged } from "../ViewSettings";
type ViewerContextShape = { type ViewerContextShape = {
// Note that we don't store settings themselves in the context (that would cause rerenders of the entire tree on each settings update). // Note that we don't store settings themselves in the context (that would cause rerenders of the entire tree on each settings update).
@ -16,19 +17,8 @@ export const ViewerContext = React.createContext<ViewerContextShape>({
getSettings: () => ({ collapsed: false }), getSettings: () => ({ collapsed: false }),
getMergedSettings: () => ({ getMergedSettings: () => ({
collapsed: false, collapsed: false,
// copy-pasted from SquiggleChart
chartSettings: {
start: 0,
stop: 10,
count: 100,
},
distributionPlotSettings: {
showSummary: false,
logX: false,
expY: false,
},
environment: defaultEnvironment, environment: defaultEnvironment,
height: 150, ...viewSettingsToMerged(viewSettingsSchema.getDefault()),
}), }),
setSettings() {}, setSettings() {},
enableLocalSettings: false, enableLocalSettings: false,

View File

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

View File

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

View File

@ -5,49 +5,137 @@ import { InputItem } from "./ui/InputItem";
import { Checkbox } from "./ui/Checkbox"; import { Checkbox } from "./ui/Checkbox";
import { HeadedSection } from "./ui/HeadedSection"; import { HeadedSection } from "./ui/HeadedSection";
import { Text } from "./ui/Text"; import { Text } from "./ui/Text";
import { MergedItemSettings, LocalItemSettings } from "./SquiggleViewer/utils";
import { defaultTickFormat } from "../lib/distributionSpecBuilder"; import { defaultTickFormat } from "../lib/distributionSpecBuilder";
export const viewSettingsSchema = yup.object({}).shape({ export const viewSettingsSchema = yup.object({}).shape({
chartHeight: yup.number().required().positive().integer().default(350), chartHeight: yup.number().required().positive().integer().default(350),
showSummary: yup.boolean().required(), showSummary: yup.boolean().required().default(false),
showEditor: yup.boolean().required(), logX: yup.boolean().required().default(false),
logX: yup.boolean().required(), expY: yup.boolean().required().default(false),
expY: yup.boolean().required(), tickFormat: yup.string().required().default(defaultTickFormat),
tickFormat: yup.string().default(defaultTickFormat),
title: yup.string(), title: yup.string(),
minX: yup.number(), minX: yup.number(),
maxX: yup.number(), maxX: yup.number(),
xAxisType: yup
.mixed<"number" | "dateTime">()
.oneOf(["number", "dateTime"])
.default("number"),
distributionChartActions: yup.boolean(), distributionChartActions: yup.boolean(),
diagramStart: yup.number().required().positive().integer().default(0).min(0), diagramStart: yup.number().required().positive().integer().default(0).min(0),
diagramStop: yup.number().required().positive().integer().default(10).min(0), diagramStop: yup.number().required().positive().integer().default(10).min(0),
diagramCount: yup.number().required().positive().integer().default(20).min(2), 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<{ export const ViewSettings: React.FC<{
withShowEditorSetting?: boolean;
withFunctionSettings?: boolean; withFunctionSettings?: boolean;
disableLogXSetting?: boolean; disableLogXSetting?: boolean;
register: UseFormRegister<FormFields>; register: UseFormRegister<EditableViewSettings>;
}> = ({ }> = ({ withFunctionSettings = true, disableLogXSetting, register }) => {
withShowEditorSetting = true,
withFunctionSettings = true,
disableLogXSetting,
register,
}) => {
return ( return (
<div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl"> <div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl">
<HeadedSection title="General Display Settings"> <HeadedSection title="General Display Settings">
<div className="space-y-4"> <div className="space-y-4">
{withShowEditorSetting ? (
<Checkbox
name="showEditor"
register={register}
label="Show code editor on left"
/>
) : null}
<InputItem <InputItem
name="chartHeight" name="chartHeight"
type="number" type="number"
@ -57,6 +145,23 @@ export const ViewSettings: React.FC<{
</div> </div>
</HeadedSection> </HeadedSection>
<DistributionViewSettings
disableLogXSetting={disableLogXSetting}
register={register}
/>
{withFunctionSettings ? (
<FunctionViewSettings register={register} />
) : null}
</div>
);
};
export const DistributionViewSettings: React.FC<{
disableLogXSetting?: boolean;
register: UseFormRegister<EditableViewSettings>;
}> = ({ disableLogXSetting, register }) => {
return (
<div className="pt-8"> <div className="pt-8">
<HeadedSection title="Distribution Display Settings"> <HeadedSection title="Distribution Display Settings">
<div className="space-y-2"> <div className="space-y-2">
@ -113,16 +218,20 @@ export const ViewSettings: React.FC<{
</div> </div>
</HeadedSection> </HeadedSection>
</div> </div>
);
};
{withFunctionSettings ? ( export const FunctionViewSettings: React.FC<{
register: UseFormRegister<EditableViewSettings>;
}> = ({ register }) => (
<div className="pt-8"> <div className="pt-8">
<HeadedSection title="Function Display Settings"> <HeadedSection title="Function Display Settings">
<div className="space-y-6"> <div className="space-y-6">
<Text> <Text>
When displaying functions of single variables that return When displaying functions of single variables that return numbers or
numbers or distributions, we need to use defaults for the distributions, we need to use defaults for the x-axis. We need to
x-axis. We need to select a minimum and maximum value of x to select a minimum and maximum value of x to sample, and a number n of
sample, and a number n of the number of points to sample. the number of points to sample.
</Text> </Text>
<div className="space-y-4"> <div className="space-y-4">
<InputItem <InputItem
@ -147,7 +256,4 @@ export const ViewSettings: React.FC<{
</div> </div>
</HeadedSection> </HeadedSection>
</div> </div>
) : null} );
</div>
);
};

View File

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

View File

@ -1,6 +1,7 @@
import { import {
result, result,
SqError, SqError,
environment,
SqProject, SqProject,
SqRecord, SqRecord,
SqValue, SqValue,
@ -9,13 +10,31 @@ import { useEffect, useMemo } from "react";
import { JsImports, jsImportsToSquiggleCode } from "../jsImports"; import { JsImports, jsImportsToSquiggleCode } from "../jsImports";
import * as uuid from "uuid"; import * as uuid from "uuid";
type SquiggleArgs = { export type SquiggleArgs = {
code: string; code: string;
executionId?: number; executionId?: number;
jsImports?: JsImports; 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; project: SqProject;
/** What other squiggle sources from the project to continue. Default [] */
continues?: string[]; continues?: string[];
onChange?: (expr: SqValue | undefined, sourceName: string) => void;
}; };
export type ResultAndBindings = { export type ResultAndBindings = {
@ -29,12 +48,24 @@ const defaultContinues = [];
export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => { export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => {
const sourceName = useMemo(() => uuid.v4(), []); 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 continues = args.continues || defaultContinues;
const result = useMemo( const result = useMemo(
() => { () => {
const project = args.project; const project = p;
project.setSource(sourceName, args.code); project.setSource(sourceName, args.code);
let fullContinues = continues; let fullContinues = continues;
@ -61,25 +92,23 @@ export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => {
continues, continues,
args.project, args.project,
env, env,
p,
] ]
); );
const { onChange } = args; const { onChange } = args;
useEffect(() => { useEffect(() => {
onChange?.( onChange?.(result.result, sourceName);
result.result.tag === "Ok" ? result.result.value : undefined,
sourceName
);
}, [result, onChange, sourceName]); }, [result, onChange, sourceName]);
useEffect(() => { useEffect(() => {
return () => { return () => {
args.project.removeSource(sourceName); p.removeSource(sourceName);
if (args.project.getSource(importSourceName(sourceName))) if (p.getSource(importSourceName(sourceName)))
args.project.removeSource(importSourceName(sourceName)); p.removeSource(importSourceName(sourceName));
}; };
}, [args.project, sourceName]); }, [p, sourceName]);
return result; return result;
}; };

View File

@ -250,5 +250,3 @@ to allow large and small numbers being printed cleanly.
{Template.bind({})} {Template.bind({})}
</Story> </Story>
</Canvas> </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");
});