Compare commits

..

4 Commits

Author SHA1 Message Date
Vyacheslav Matyukhin
9e2eace05e
Merge pull request #1231 from quantified-uncertainty/project-in-editors
Project in editors and remove warnings
2022-10-14 18:06:49 +03:00
Vyacheslav Matyukhin
a0000cd179
Merge branch 'develop' into project-in-editors 2022-10-13 03:36:08 +04:00
Sam Nolan
98454a87b5 Get projects working in Playgrounds 2022-10-10 14:23:04 +11:00
Sam Nolan
0f8e7ce6b6 Project in editors and remove warnings 2022-10-08 16:23:58 +11:00
18 changed files with 538 additions and 404 deletions

View File

@ -24,7 +24,7 @@ export const Alert: React.FC<{
children, children,
}) => { }) => {
return ( return (
<div className={clsx("rounded-md p-4", backgroundColor)}> <div className={clsx("rounded-md p-4", backgroundColor)} role="status">
<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,10 +55,7 @@ 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,12 +89,8 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
const spec = buildVegaSpec({ const spec = buildVegaSpec({
...props, ...props,
minX: Number.isFinite(props.minX) minX: props.minX ?? Math.min(...domain.map((x) => x.x)),
? props.minX maxX: props.minX ?? Math.max(...domain.map((x) => x.x)),
: 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,22 +1,158 @@
import * as React from "react"; import * as React from "react";
import { useSquiggle, SquiggleArgs } from "../lib/hooks/useSquiggle";
import { import {
SquiggleViewer, SqValue,
FlattenedViewSettings, environment,
createViewSettings, SqProject,
} from "./SquiggleViewer"; defaultEnvironment,
} 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 = SquiggleArgs & FlattenedViewSettings; 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 = {};
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 resultAndBindings = useSquiggle(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,
project,
environment,
} = props;
const resultAndBindings = useSquiggle({
environment,
continues,
project,
code,
jsImports,
onChange,
executionId,
});
const valueToRender = getValueToRender(resultAndBindings); const valueToRender = getValueToRender(resultAndBindings);
return ( return (
<SquiggleViewer {...createViewSettings(props)} result={valueToRender} /> <SquiggleViewer
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 {
SquiggleViewer, splitSquiggleChartSettings,
createViewSettings, SquiggleChartProps,
FlattenedViewSettings, } from "./SquiggleChart";
} from "./SquiggleViewer"; import { useMaybeControlledValue, useSquiggle } from "../lib/hooks";
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,11 +28,13 @@ const WrappedCodeEditor: React.FC<{
</div> </div>
); );
export type SquiggleEditorProps = SquiggleArgs & export type SquiggleEditorProps = SquiggleChartProps & {
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({
@ -41,7 +43,30 @@ export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
onChange: props.onCodeChange, onChange: props.onCodeChange,
}); });
const resultAndBindings = useSquiggle({ ...props, code }); 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,
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);
@ -53,7 +78,15 @@ export const SquiggleEditor: React.FC<SquiggleEditorProps> = (props) => {
setCode={setCode} setCode={setCode}
errorLocations={errorLocations} errorLocations={errorLocations}
/> />
<SquiggleViewer result={valueToRender} {...createViewSettings(props)} /> <SquiggleViewer
result={valueToRender}
width={width}
height={height}
distributionPlotSettings={distributionPlotSettings}
chartSettings={chartSettings}
environment={environment ?? defaultEnvironment}
enableLocalSettings={enableLocalSettings}
/>
</SquiggleContainer> </SquiggleContainer>
); );
}; };

View File

@ -13,7 +13,6 @@ 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,
@ -29,8 +28,9 @@ import {
} from "@heroicons/react/solid"; } from "@heroicons/react/solid";
import clsx from "clsx"; import clsx from "clsx";
import { environment } from "@quri/squiggle-lang"; import { environment, SqProject } 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,27 +41,23 @@ 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 { import { SquiggleViewer } from "./SquiggleViewer";
SquiggleViewer,
FlattenedViewSettings,
createViewSettings,
} from "./SquiggleViewer";
type PlaygroundProps = SquiggleArgs & type PlaygroundProps = SquiggleChartProps & {
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; /* When settings change */
/* When settings change */ onSettingsChange?(settings: any): void;
onSettingsChange?(settings: any): void; /** Should we show the editor? */
/** Should we show the editor? */ 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({})
@ -82,7 +78,6 @@ 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);
@ -240,14 +235,25 @@ export const PlaygroundContext = React.createContext<PlaygroundContextShape>({
getLeftPanelElement: () => undefined, getLeftPanelElement: () => undefined,
}); });
export const SquigglePlayground: FC<PlaygroundProps> = (props) => { export const SquigglePlayground: FC<PlaygroundProps> = ({
const { defaultCode = "",
defaultCode = "", height = 500,
code: controlledCode, showSummary = true,
onCodeChange, logX = false,
onSettingsChange, expY = false,
showShareButton = false, title,
} = props; minX,
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,
@ -256,19 +262,29 @@ export const SquigglePlayground: FC<PlaygroundProps> = (props) => {
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 rawVars = useWatch({ const vars = useWatch({
control, control,
}); });
let vars = useMemo(() => ({ ...schema.getDefault(), ...rawVars }), [rawVars]);
useEffect(() => { useEffect(() => {
onSettingsChange?.(vars); onSettingsChange?.(vars);
@ -291,12 +307,14 @@ export const SquigglePlayground: FC<PlaygroundProps> = (props) => {
executionId, executionId,
} = useRunnerState(code); } = useRunnerState(code);
let args: SquiggleArgs = props; const resultAndBindings = useSquiggle({
args = { ...args, code, jsImports: imports, executionId }; environment,
if (!args.project) { continues,
args = { ...args, environment }; code: renderedCode,
} project,
const resultAndBindings = useSquiggle(args); jsImports: imports,
executionId,
});
const valueToRender = getValueToRender(resultAndBindings); const valueToRender = getValueToRender(resultAndBindings);
@ -306,7 +324,27 @@ export const SquigglePlayground: FC<PlaygroundProps> = (props) => {
{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 {...createViewSettings(vars)} result={valueToRender} /> <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}
/>
</div> </div>
); );
@ -321,7 +359,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = (props) => {
onSubmit={run} onSubmit={run}
oneLine={false} oneLine={false}
showGutter={true} showGutter={true}
height={(props.chartHeight ?? 200) - 1} height={height - 1}
/> />
</div> </div>
) : ( ) : (
@ -360,7 +398,7 @@ export const SquigglePlayground: FC<PlaygroundProps> = (props) => {
<div className="flex mt-2"> <div className="flex mt-2">
<div <div
className="w-1/2 relative" className="w-1/2 relative"
style={{ minHeight: props.chartHeight }} style={{ minHeight: height }}
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)}
{...settings.distributionPlotSettings}
height={settings.chartHeight}
environment={settings.environment} environment={settings.environment}
{...settings.distributionPlotSettings}
height={settings.height}
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.chartHeight} height={settings.height}
environment={{ environment={{
sampleCount: settings.environment.sampleCount / 10, sampleCount: settings.environment.sampleCount / 10,
xyPointLength: settings.environment.xyPointLength / 10, xyPointLength: settings.environment.xyPointLength / 10,
@ -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.chartHeight} height={settings.height}
width={width} width={width}
/> />
); );

View File

@ -3,13 +3,9 @@ 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 { import { ViewSettings, viewSettingsSchema } from "../ViewSettings";
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";
@ -38,14 +34,44 @@ const ItemSettingsModal: React.FC<
const { register, watch } = useForm({ const { register, watch } = useForm({
resolver: yupResolver(viewSettingsSchema), resolver: yupResolver(viewSettingsSchema),
defaultValues: mergedToViewSettings(mergedSettings), 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,
},
}); });
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,
...viewSettingsToLocal(vars), 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,
},
}); });
onChange(); onChange();
}); });
@ -76,6 +102,7 @@ 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> <div role={isTopLevel ? "status" : undefined}>
<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,7 +1,6 @@
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).
@ -17,8 +16,19 @@ 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,
...viewSettingsToMerged(viewSettingsSchema.getDefault()), height: 150,
}), }),
setSettings() {}, setSettings() {},
enableLocalSettings: false, enableLocalSettings: false,

View File

@ -1,5 +1,7 @@
import React, { useCallback, useRef } from "react"; import React, { useCallback, useRef } from "react";
import { SqValueLocation } from "@quri/squiggle-lang"; import { environment, 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 {
@ -8,40 +10,20 @@ 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"];
} & ViewSettings; width?: number;
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;
@ -52,9 +34,10 @@ const defaultSettings: LocalItemSettings = { collapsed: false };
export const SquiggleViewer: React.FC<Props> = ({ export const SquiggleViewer: React.FC<Props> = ({
result, result,
width, width,
chartHeight, height,
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
@ -76,7 +59,6 @@ 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: {
@ -88,14 +70,14 @@ export const SquiggleViewer: React.FC<Props> = ({
...(localSettings.chartSettings || {}), ...(localSettings.chartSettings || {}),
}, },
environment: { environment: {
...env, ...environment,
...localSettings.environment, ...(localSettings.environment || {}),
}, },
chartHeight: localSettings.chartHeight || chartHeight, height: localSettings.height || height,
}; };
return result; return result;
}, },
[distributionPlotSettings, chartSettings, chartHeight, getSettings] [distributionPlotSettings, chartSettings, environment, height, 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 { SqValueLocation, environment } from "@quri/squiggle-lang"; import { environment, SqValueLocation } 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>;
chartHeight?: number; height?: number;
environment?: environment; environment?: Partial<environment>;
}; };
export type MergedItemSettings = { export type MergedItemSettings = {
distributionPlotSettings: DistributionPlottingSettings; distributionPlotSettings: DistributionPlottingSettings;
chartSettings: FunctionChartSettings; chartSettings: FunctionChartSettings;
chartHeight: number; height: number;
environment: environment; environment: environment;
}; };

View File

@ -5,137 +5,49 @@ 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().default(false), showSummary: yup.boolean().required(),
logX: yup.boolean().required().default(false), showEditor: yup.boolean().required(),
expY: yup.boolean().required().default(false), logX: yup.boolean().required(),
tickFormat: yup.string().required().default(defaultTickFormat), expY: yup.boolean().required(),
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),
}); });
export type EditableViewSettings = yup.InferType<typeof viewSettingsSchema>; type FormFields = 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<EditableViewSettings>; register: UseFormRegister<FormFields>;
}> = ({ 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"
@ -145,115 +57,97 @@ export const ViewSettings: React.FC<{
</div> </div>
</HeadedSection> </HeadedSection>
<DistributionViewSettings <div className="pt-8">
disableLogXSetting={disableLogXSetting} <HeadedSection title="Distribution Display Settings">
register={register} <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>
{withFunctionSettings ? ( {withFunctionSettings ? (
<FunctionViewSettings register={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>
) : 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,7 +70,15 @@ const width = 500;
export function buildVegaSpec( export function buildVegaSpec(
specOptions: DistributionChartSpecOptions & { maxY: number } specOptions: DistributionChartSpecOptions & { maxY: number }
): VisualizationSpec { ): VisualizationSpec {
const { title, minX, maxX, logX, expY, xAxisType, maxY } = specOptions; const {
title,
minX,
maxX,
logX,
expY,
xAxisType = "number",
maxY,
} = specOptions;
const dateTime = xAxisType === "dateTime"; const dateTime = xAxisType === "dateTime";

View File

@ -1,40 +1,23 @@
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";
export type SquiggleArgs = { type SquiggleArgs = {
environment?: environment;
code: string; code: string;
executionId?: number; executionId?: number;
jsImports?: JsImports; jsImports?: JsImports;
onChange?: ( project?: SqProject;
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 = {
@ -46,9 +29,7 @@ const importSourceName = (sourceName: string) => "imports-" + sourceName;
const defaultContinues = []; const defaultContinues = [];
export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => { export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => {
const sourceName = useMemo(() => uuid.v4(), []); const project = useMemo(() => {
const p = useMemo(() => {
if (args.project) { if (args.project) {
return args.project; return args.project;
} else { } else {
@ -60,13 +41,13 @@ export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => {
} }
}, [args.project, args.environment]); }, [args.project, args.environment]);
const env = p.getEnvironment(); const sourceName = useMemo(() => uuid.v4(), []);
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) {
@ -90,25 +71,27 @@ export const useSquiggle = (args: SquiggleArgs): ResultAndBindings => {
args.executionId, args.executionId,
sourceName, sourceName,
continues, continues,
args.project, project,
env, env,
p,
] ]
); );
const { onChange } = args; const { onChange } = args;
useEffect(() => { useEffect(() => {
onChange?.(result.result, sourceName); onChange?.(
result.result.tag === "Ok" ? result.result.value : undefined,
sourceName
);
}, [result, onChange, sourceName]); }, [result, onChange, sourceName]);
useEffect(() => { useEffect(() => {
return () => { return () => {
p.removeSource(sourceName); project.removeSource(sourceName);
if (p.getSource(importSourceName(sourceName))) if (project.getSource(importSourceName(sourceName)))
p.removeSource(importSourceName(sourceName)); project.removeSource(importSourceName(sourceName));
}; };
}, [p, sourceName]); }, [project, sourceName]);
return result; return result;
}; };

View File

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

View File

@ -1,9 +1,14 @@
import { render } from "@testing-library/react"; import { render, screen } from "@testing-library/react";
import React from "react"; import React from "react";
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
import { SquiggleChart } from "../src/index"; import {
SquiggleChart,
SquiggleEditor,
SquigglePlayground,
} from "../src/index";
import { SqProject } from "@quri/squiggle-lang";
test("Logs nothing on render", async () => { test("Chart logs nothing on render", async () => {
const { unmount } = render(<SquiggleChart code={"normal(0, 1)"} />); const { unmount } = render(<SquiggleChart code={"normal(0, 1)"} />);
unmount(); unmount();
@ -11,3 +16,38 @@ test("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

@ -1,12 +0,0 @@
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");
});