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,
}) => {
return (
<div className={clsx("rounded-md p-4", backgroundColor)}>
<div className={clsx("rounded-md p-4", backgroundColor)} role="status">
<div className="flex">
<Icon
className={clsx("h-5 w-5 flex-shrink-0", iconColor)}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,137 +5,49 @@ import { InputItem } from "./ui/InputItem";
import { Checkbox } from "./ui/Checkbox";
import { HeadedSection } from "./ui/HeadedSection";
import { Text } from "./ui/Text";
import { MergedItemSettings, LocalItemSettings } from "./SquiggleViewer/utils";
import { defaultTickFormat } from "../lib/distributionSpecBuilder";
export const viewSettingsSchema = yup.object({}).shape({
chartHeight: yup.number().required().positive().integer().default(350),
showSummary: yup.boolean().required().default(false),
logX: yup.boolean().required().default(false),
expY: yup.boolean().required().default(false),
tickFormat: yup.string().required().default(defaultTickFormat),
showSummary: yup.boolean().required(),
showEditor: yup.boolean().required(),
logX: yup.boolean().required(),
expY: yup.boolean().required(),
tickFormat: yup.string().default(defaultTickFormat),
title: yup.string(),
minX: yup.number(),
maxX: yup.number(),
xAxisType: yup
.mixed<"number" | "dateTime">()
.oneOf(["number", "dateTime"])
.default("number"),
distributionChartActions: yup.boolean(),
diagramStart: yup.number().required().positive().integer().default(0).min(0),
diagramStop: yup.number().required().positive().integer().default(10).min(0),
diagramCount: yup.number().required().positive().integer().default(20).min(2),
});
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,
});
type FormFields = yup.InferType<typeof viewSettingsSchema>;
// This component is used in two places: for global settings in SquigglePlayground, and for item-specific settings in modal dialogs.
export const ViewSettings: React.FC<{
withShowEditorSetting?: boolean;
withFunctionSettings?: boolean;
disableLogXSetting?: boolean;
register: UseFormRegister<EditableViewSettings>;
}> = ({ withFunctionSettings = true, disableLogXSetting, register }) => {
register: UseFormRegister<FormFields>;
}> = ({
withShowEditorSetting = true,
withFunctionSettings = true,
disableLogXSetting,
register,
}) => {
return (
<div className="space-y-6 p-3 divide-y divide-gray-200 max-w-xl">
<HeadedSection title="General Display Settings">
<div className="space-y-4">
{withShowEditorSetting ? (
<Checkbox
name="showEditor"
register={register}
label="Show code editor on left"
/>
) : null}
<InputItem
name="chartHeight"
type="number"
@ -145,115 +57,97 @@ export const ViewSettings: React.FC<{
</div>
</HeadedSection>
<DistributionViewSettings
disableLogXSetting={disableLogXSetting}
register={register}
/>
<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>
{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}
</div>
);
};
export const DistributionViewSettings: React.FC<{
disableLogXSetting?: boolean;
register: UseFormRegister<EditableViewSettings>;
}> = ({ disableLogXSetting, register }) => {
return (
<div className="pt-8">
<HeadedSection title="Distribution Display Settings">
<div className="space-y-2">
<Checkbox
register={register}
name="logX"
label="Show x scale logarithmically"
disabled={disableLogXSetting}
tooltip={
disableLogXSetting
? "Your distribution has mass lower than or equal to 0. Log only works on strictly positive values."
: undefined
}
/>
<Checkbox
register={register}
name="expY"
label="Show y scale exponentially"
/>
<Checkbox
register={register}
name="distributionChartActions"
label="Show vega chart controls"
/>
<Checkbox
register={register}
name="showSummary"
label="Show summary statistics"
/>
<InputItem
name="minX"
type="number"
register={register}
label="Min X Value"
/>
<InputItem
name="maxX"
type="number"
register={register}
label="Max X Value"
/>
<InputItem
name="title"
type="text"
register={register}
label="Title"
/>
<InputItem
name="tickFormat"
type="text"
register={register}
label="Tick Format"
/>
</div>
</HeadedSection>
</div>
);
};
export const FunctionViewSettings: React.FC<{
register: UseFormRegister<EditableViewSettings>;
}> = ({ register }) => (
<div className="pt-8">
<HeadedSection title="Function Display Settings">
<div className="space-y-6">
<Text>
When displaying functions of single variables that return numbers or
distributions, we need to use defaults for the x-axis. We need to
select a minimum and maximum value of x to sample, and a number n of
the number of points to sample.
</Text>
<div className="space-y-4">
<InputItem
type="number"
name="diagramStart"
register={register}
label="Min X Value"
/>
<InputItem
type="number"
name="diagramStop"
register={register}
label="Max X Value"
/>
<InputItem
type="number"
name="diagramCount"
register={register}
label="Points between X min and X max to sample"
/>
</div>
</div>
</HeadedSection>
</div>
);

View File

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

View File

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

View File

@ -250,3 +250,5 @@ to allow large and small numbers being printed cleanly.
{Template.bind({})}
</Story>
</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 "@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)"} />);
unmount();
@ -11,3 +16,38 @@ test("Logs nothing on render", async () => {
expect(console.warn).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");
});