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
18 changed files with 404 additions and 538 deletions

View File

@ -24,7 +24,7 @@ export const Alert: React.FC<{
children, children,
}) => { }) => {
return ( return (
<div className={clsx("rounded-md p-4", backgroundColor)} role="status"> <div className={clsx("rounded-md p-4", backgroundColor)}>
<div className="flex"> <div className="flex">
<Icon <Icon
className={clsx("h-5 w-5 flex-shrink-0", iconColor)} className={clsx("h-5 w-5 flex-shrink-0", iconColor)}

View File

@ -55,7 +55,10 @@ export const CodeEditor: FC<CodeEditorProps> = ({
editorProps={{ editorProps={{
$blockScrolling: true, $blockScrolling: true,
}} }}
setOptions={{}} setOptions={{
enableBasicAutocompletion: false,
enableLiveAutocompletion: false,
}}
commands={[ commands={[
{ {
name: "submit", name: "submit",

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,158 +1,22 @@
import * as React from "react"; import * as React from "react";
import { useSquiggle, SquiggleArgs } from "../lib/hooks/useSquiggle";
import { import {
SqValue, SquiggleViewer,
environment, FlattenedViewSettings,
SqProject, createViewSettings,
defaultEnvironment, } from "./SquiggleViewer";
} from "@quri/squiggle-lang";
import { useSquiggle } from "../lib/hooks";
import { SquiggleViewer } from "./SquiggleViewer";
import { JsImports } from "../lib/jsImports";
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 = {};
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,
project,
environment,
} = props;
const resultAndBindings = useSquiggle({
environment,
continues,
project,
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={
project ? project.getEnvironment() : environment ?? defaultEnvironment
}
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 &
defaultCode?: string; FlattenedViewSettings & {
onCodeChange?: (code: string) => void; defaultCode?: string;
}; 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,30 +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,
continues,
project,
} = props;
const resultAndBindings = useSquiggle({
environment,
continues,
code,
project,
jsImports,
onChange,
executionId,
});
const valueToRender = getValueToRender(resultAndBindings); const valueToRender = getValueToRender(resultAndBindings);
const errorLocations = getErrorLocations(resultAndBindings.result); const errorLocations = getErrorLocations(resultAndBindings.result);
@ -78,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,23 +41,27 @@ 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 &
/** The initial squiggle string to put in the playground */ FlattenedViewSettings & {
defaultCode?: string; /** The initial squiggle string to put in the playground */
onCodeChange?(expr: string): void; defaultCode?: string;
/* When settings change */ onCodeChange?(expr: string): void;
onSettingsChange?(settings: any): void; /* When settings change */
/** Should we show the editor? */ onSettingsChange?(settings: any): void;
showEditor?: boolean; /** Should we show the editor? */
/** Useful for playground on squiggle website, where we update the anchor link based on current code and settings */ showEditor?: boolean;
showShareButton?: boolean; /** Useful for playground on squiggle website, where we update the anchor link based on current code and settings */
}; 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,25 +240,14 @@ export const PlaygroundContext = React.createContext<PlaygroundContextShape>({
getLeftPanelElement: () => undefined, getLeftPanelElement: () => undefined,
}); });
export const SquigglePlayground: FC<PlaygroundProps> = ({ export const SquigglePlayground: FC<PlaygroundProps> = (props) => {
defaultCode = "", const {
height = 500, defaultCode = "",
showSummary = true, code: controlledCode,
logX = false, onCodeChange,
expY = false, onSettingsChange,
title, showShareButton = false,
minX, } = props;
maxX,
tickFormat = defaultTickFormat,
distributionChartActions,
code: controlledCode,
onCodeChange,
onSettingsChange,
showEditor = true,
showShareButton = false,
continues,
project,
}) => {
const [code, setCode] = useMaybeControlledValue({ const [code, setCode] = useMaybeControlledValue({
value: controlledCode, value: controlledCode,
defaultValue: defaultCode, defaultValue: defaultCode,
@ -262,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);
@ -307,14 +291,12 @@ export const SquigglePlayground: FC<PlaygroundProps> = ({
executionId, executionId,
} = useRunnerState(code); } = useRunnerState(code);
const resultAndBindings = useSquiggle({ let args: SquiggleArgs = props;
environment, args = { ...args, code, jsImports: imports, executionId };
continues, if (!args.project) {
code: renderedCode, args = { ...args, environment };
project, }
jsImports: imports, const resultAndBindings = useSquiggle(args);
executionId,
});
const valueToRender = getValueToRender(resultAndBindings); const valueToRender = getValueToRender(resultAndBindings);
@ -324,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>
); );
@ -359,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>
) : ( ) : (
@ -398,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

@ -45,7 +45,7 @@ export const VariableBox: React.FC<VariableBoxProps> = ({
: location.path.items[location.path.items.length - 1]; : location.path.items[location.path.items.length - 1];
return ( return (
<div role={isTopLevel ? "status" : undefined}> <div>
<header className="inline-flex space-x-1"> <header className="inline-flex space-x-1">
<Tooltip text={heading}> <Tooltip text={heading}>
<span <span

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,97 +145,115 @@ export const ViewSettings: React.FC<{
</div> </div>
</HeadedSection> </HeadedSection>
<div className="pt-8"> <DistributionViewSettings
<HeadedSection title="Distribution Display Settings"> disableLogXSetting={disableLogXSetting}
<div className="space-y-2"> register={register}
<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>
{withFunctionSettings ? ( {withFunctionSettings ? (
<div className="pt-8"> <FunctionViewSettings register={register} />
<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} ) : null}
</div> </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 */ /** 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,23 +1,40 @@
import { import {
result, result,
SqError, SqError,
environment,
SqProject, SqProject,
SqRecord, SqRecord,
SqValue, SqValue,
environment,
} from "@quri/squiggle-lang"; } from "@quri/squiggle-lang";
import { useEffect, useMemo } from "react"; 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 = {
environment?: environment;
code: string; code: string;
executionId?: number; executionId?: number;
jsImports?: JsImports; jsImports?: JsImports;
project?: SqProject; 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[]; continues?: string[];
onChange?: (expr: SqValue | undefined, sourceName: string) => void;
}; };
export type ResultAndBindings = { export type ResultAndBindings = {
@ -29,7 +46,9 @@ const importSourceName = (sourceName: string) => "imports-" + sourceName;
const defaultContinues = []; const defaultContinues = [];
export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => { export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => {
const project = useMemo(() => { const sourceName = useMemo(() => uuid.v4(), []);
const p = useMemo(() => {
if (args.project) { if (args.project) {
return args.project; return args.project;
} else { } else {
@ -41,13 +60,13 @@ export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => {
} }
}, [args.project, args.environment]); }, [args.project, args.environment]);
const sourceName = useMemo(() => uuid.v4(), []); const env = p.getEnvironment();
const env = project.getEnvironment();
const continues = args.continues || defaultContinues; const continues = args.continues || defaultContinues;
const result = useMemo( const result = useMemo(
() => { () => {
const project = p;
project.setSource(sourceName, args.code); project.setSource(sourceName, args.code);
let fullContinues = continues; let fullContinues = continues;
if (args.jsImports && Object.keys(args.jsImports).length) { if (args.jsImports && Object.keys(args.jsImports).length) {
@ -71,27 +90,25 @@ export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => {
args.executionId, args.executionId,
sourceName, sourceName,
continues, continues,
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 () => {
project.removeSource(sourceName); p.removeSource(sourceName);
if (project.getSource(importSourceName(sourceName))) if (p.getSource(importSourceName(sourceName)))
project.removeSource(importSourceName(sourceName)); p.removeSource(importSourceName(sourceName));
}; };
}, [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

@ -1,14 +1,9 @@
import { render, screen } from "@testing-library/react"; import { render } from "@testing-library/react";
import React from "react"; import React from "react";
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { import { SquiggleChart } from "../src/index";
SquiggleChart,
SquiggleEditor,
SquigglePlayground,
} from "../src/index";
import { SqProject } from "@quri/squiggle-lang";
test("Chart logs nothing on render", async () => { test("Logs nothing on render", async () => {
const { unmount } = render(<SquiggleChart code={"normal(0, 1)"} />); const { unmount } = render(<SquiggleChart code={"normal(0, 1)"} />);
unmount(); unmount();
@ -16,38 +11,3 @@ test("Chart logs nothing on render", async () => {
expect(console.warn).not.toBeCalled(); expect(console.warn).not.toBeCalled();
expect(console.error).not.toBeCalled(); expect(console.error).not.toBeCalled();
}); });
test("Editor logs nothing on render", async () => {
const { unmount } = render(<SquiggleEditor code={"normal(0, 1)"} />);
unmount();
expect(console.log).not.toBeCalled();
expect(console.warn).not.toBeCalled();
expect(console.error).not.toBeCalled();
});
test("Project dependencies work in editors", async () => {
const project = SqProject.create();
render(<SquiggleEditor code={"x = 1"} project={project} />);
const source = project.getSourceIds()[0];
const { container } = render(
<SquiggleEditor code={"x + 1"} project={project} continues={[source]} />
);
expect(container).toHaveTextContent("2");
});
test("Project dependencies work in playgrounds", async () => {
const project = SqProject.create();
project.setSource("depend", "x = 1");
render(
<SquigglePlayground
code={"x + 1"}
project={project}
continues={["depend"]}
/>
);
// We must await here because SquigglePlayground loads results asynchronously
expect(await screen.findByRole("status")).toHaveTextContent("2");
});

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");
});