diff --git a/packages/components/package.json b/packages/components/package.json
index 67b2eeb5..3b594bc5 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -3,18 +3,21 @@
"version": "0.2.20",
"license": "MIT",
"dependencies": {
+ "@hookform/resolvers": "^2.8.10",
"@quri/squiggle-lang": "^0.2.8",
"@react-hook/size": "^2.1.2",
"lodash": "^4.17.21",
"react": "^18.1.0",
"react-ace": "^10.1.0",
"react-dom": "^18.1.0",
+ "react-hook-form": "^7.31.2",
"react-use": "^17.4.0",
"react-vega": "^7.5.1",
"styled-components": "^5.3.5",
"vega": "^5.22.1",
"vega-embed": "^6.20.6",
- "vega-lite": "^5.2.0"
+ "vega-lite": "^5.2.0",
+ "yup": "^0.32.11"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.17.12",
diff --git a/packages/components/src/components/FunctionChart.tsx b/packages/components/src/components/FunctionChart.tsx
index 242bf719..bb5a9e24 100644
--- a/packages/components/src/components/FunctionChart.tsx
+++ b/packages/components/src/components/FunctionChart.tsx
@@ -1,40 +1,9 @@
import * as React from "react";
-import _ from "lodash";
-import type { Spec } from "vega";
-import {
- Distribution,
- result,
- lambdaValue,
- environment,
- runForeign,
- squiggleExpression,
- errorValue,
- errorValueToString,
-} from "@quri/squiggle-lang";
-import { createClassFromSpec } from "react-vega";
-import * as percentilesSpec from "../vega-specs/spec-percentiles.json";
-import { DistributionChart } from "./DistributionChart";
-import { NumberShower } from "./NumberShower";
+import { lambdaValue, environment, runForeign } from "@quri/squiggle-lang";
+import { FunctionChart1Dist } from "./FunctionChart1Dist";
+import { FunctionChart1Number } from "./FunctionChart1Number";
import { ErrorBox } from "./ErrorBox";
-let SquigglePercentilesChart = createClassFromSpec({
- spec: percentilesSpec as Spec,
-});
-
-const _rangeByCount = (start: number, stop: number, count: number) => {
- const step = (stop - start) / (count - 1);
- const items = _.range(start, stop, step);
- const result = items.concat([stop]);
- return result;
-};
-
-function unwrap(x: result): a {
- if (x.tag === "Ok") {
- return x.value;
- } else {
- throw Error("FAILURE TO UNWRAP");
- }
-}
export type FunctionChartSettings = {
start: number;
stop: number;
@@ -45,167 +14,61 @@ interface FunctionChartProps {
fn: lambdaValue;
chartSettings: FunctionChartSettings;
environment: environment;
+ height: number;
}
-type percentiles = {
- x: number;
- p1: number;
- p5: number;
- p10: number;
- p20: number;
- p30: number;
- p40: number;
- p50: number;
- p60: number;
- p70: number;
- p80: number;
- p90: number;
- p95: number;
- p99: number;
-}[];
-
-type errors = _.Dictionary<
- {
- x: number;
- value: string;
- }[]
->;
-
-type point = { x: number; value: result };
-
-let getPercentiles = ({ chartSettings, fn, environment }) => {
- let chartPointsToRender = _rangeByCount(
- chartSettings.start,
- chartSettings.stop,
- chartSettings.count
- );
-
- let chartPointsData: point[] = chartPointsToRender.map((x) => {
- let result = runForeign(fn, [x], environment);
- if (result.tag === "Ok") {
- if (result.value.tag == "distribution") {
- return { x, value: { tag: "Ok", value: result.value.value } };
- } else {
- return {
- x,
- value: {
- tag: "Error",
- value:
- "Cannot currently render functions that don't return distributions",
- },
- };
- }
- } else {
- return {
- x,
- value: { tag: "Error", value: errorValueToString(result.value) },
- };
- }
- });
-
- let initialPartition: [
- { x: number; value: Distribution }[],
- { x: number; value: string }[]
- ] = [[], []];
-
- let [functionImage, errors] = chartPointsData.reduce((acc, current) => {
- if (current.value.tag === "Ok") {
- acc[0].push({ x: current.x, value: current.value.value });
- } else {
- acc[1].push({ x: current.x, value: current.value.value });
- }
- return acc;
- }, initialPartition);
-
- let groupedErrors: errors = _.groupBy(errors, (x) => x.value);
-
- let percentiles: percentiles = functionImage.map(({ x, value }) => {
- // We convert it to to a pointSet distribution first, so that in case its a sample set
- // distribution, it doesn't internally convert it to a pointSet distribution for every
- // single inv() call.
- let toPointSet: Distribution = unwrap(value.toPointSet());
- return {
- x: x,
- p1: unwrap(toPointSet.inv(0.01)),
- p5: unwrap(toPointSet.inv(0.05)),
- p10: unwrap(toPointSet.inv(0.1)),
- p20: unwrap(toPointSet.inv(0.2)),
- p30: unwrap(toPointSet.inv(0.3)),
- p40: unwrap(toPointSet.inv(0.4)),
- p50: unwrap(toPointSet.inv(0.5)),
- p60: unwrap(toPointSet.inv(0.6)),
- p70: unwrap(toPointSet.inv(0.7)),
- p80: unwrap(toPointSet.inv(0.8)),
- p90: unwrap(toPointSet.inv(0.9)),
- p95: unwrap(toPointSet.inv(0.95)),
- p99: unwrap(toPointSet.inv(0.99)),
- };
- });
-
- return { percentiles, errors: groupedErrors };
-};
-
export const FunctionChart: React.FC = ({
fn,
chartSettings,
environment,
+ height,
}: FunctionChartProps) => {
- let [mouseOverlay, setMouseOverlay] = React.useState(0);
- function handleHover(_name: string, value: unknown) {
- setMouseOverlay(value as number);
- }
- function handleOut() {
- setMouseOverlay(NaN);
- }
- const signalListeners = { mousemove: handleHover, mouseout: handleOut };
- let mouseItem: result = !!mouseOverlay
- ? runForeign(fn, [mouseOverlay], environment)
- : {
- tag: "Error",
- value: {
- tag: "REExpectedType",
- value: "Hover x-coordinate returned NaN. Expected a number.",
- },
- };
- let showChart =
- mouseItem.tag === "Ok" && mouseItem.value.tag == "distribution" ? (
-
- ) : (
- <>>
- );
+ let result1 = runForeign(fn, [chartSettings.start], environment);
+ let result2 = runForeign(fn, [chartSettings.stop], environment);
+ let getValidResult = () => {
+ if (result1.tag === "Ok") {
+ return result1;
+ } else if (result2.tag === "Ok") {
+ return result2;
+ } else {
+ return result1;
+ }
+ };
+ let validResult = getValidResult();
+ let resultType = validResult.tag === "Ok" ? validResult.value.tag : "Error";
- let getPercentilesMemoized = React.useMemo(
- () => getPercentiles({ chartSettings, fn, environment }),
- [environment, fn]
- );
-
- return (
- <>
-
- {showChart}
- {_.entries(getPercentilesMemoized.errors).map(
- ([errorName, errorPoints]) => (
-
- Values:{" "}
- {errorPoints
- .map((r, i) => )
- .reduce((a, b) => (
- <>
- {a}, {b}
- >
- ))}
+ let component = () => {
+ switch (resultType) {
+ case "distribution":
+ return (
+
+ );
+ case "number":
+ return (
+
+ );
+ case "Error":
+ return (
+ The function failed to be run
+ );
+ default:
+ return (
+
+ There is no function visualization for this type of function
- )
- )}
- >
- );
+ );
+ }
+ };
+
+ return component();
};
diff --git a/packages/components/src/components/FunctionChart1Dist.tsx b/packages/components/src/components/FunctionChart1Dist.tsx
new file mode 100644
index 00000000..836bcdd5
--- /dev/null
+++ b/packages/components/src/components/FunctionChart1Dist.tsx
@@ -0,0 +1,214 @@
+import * as React from "react";
+import _ from "lodash";
+import type { Spec } from "vega";
+import {
+ Distribution,
+ result,
+ lambdaValue,
+ environment,
+ runForeign,
+ squiggleExpression,
+ errorValue,
+ errorValueToString,
+} from "@quri/squiggle-lang";
+import { createClassFromSpec } from "react-vega";
+import * as percentilesSpec from "../vega-specs/spec-percentiles.json";
+import { DistributionChart } from "./DistributionChart";
+import { NumberShower } from "./NumberShower";
+import { ErrorBox } from "./ErrorBox";
+
+let SquigglePercentilesChart = createClassFromSpec({
+ spec: percentilesSpec as Spec,
+});
+
+const _rangeByCount = (start: number, stop: number, count: number) => {
+ const step = (stop - start) / (count - 1);
+ const items = _.range(start, stop, step);
+ const result = items.concat([stop]);
+ return result;
+};
+
+function unwrap(x: result): a {
+ if (x.tag === "Ok") {
+ return x.value;
+ } else {
+ throw Error("FAILURE TO UNWRAP");
+ }
+}
+export type FunctionChartSettings = {
+ start: number;
+ stop: number;
+ count: number;
+};
+
+interface FunctionChart1DistProps {
+ fn: lambdaValue;
+ chartSettings: FunctionChartSettings;
+ environment: environment;
+ height: number;
+}
+
+type percentiles = {
+ x: number;
+ p1: number;
+ p5: number;
+ p10: number;
+ p20: number;
+ p30: number;
+ p40: number;
+ p50: number;
+ p60: number;
+ p70: number;
+ p80: number;
+ p90: number;
+ p95: number;
+ p99: number;
+}[];
+
+type errors = _.Dictionary<
+ {
+ x: number;
+ value: string;
+ }[]
+>;
+
+type point = { x: number; value: result };
+
+let getPercentiles = ({ chartSettings, fn, environment }) => {
+ let chartPointsToRender = _rangeByCount(
+ chartSettings.start,
+ chartSettings.stop,
+ chartSettings.count
+ );
+
+ let chartPointsData: point[] = chartPointsToRender.map((x) => {
+ let result = runForeign(fn, [x], environment);
+ if (result.tag === "Ok") {
+ if (result.value.tag == "distribution") {
+ return { x, value: { tag: "Ok", value: result.value.value } };
+ } else {
+ return {
+ x,
+ value: {
+ tag: "Error",
+ value:
+ "Cannot currently render functions that don't return distributions",
+ },
+ };
+ }
+ } else {
+ return {
+ x,
+ value: { tag: "Error", value: errorValueToString(result.value) },
+ };
+ }
+ });
+
+ let initialPartition: [
+ { x: number; value: Distribution }[],
+ { x: number; value: string }[]
+ ] = [[], []];
+
+ let [functionImage, errors] = chartPointsData.reduce((acc, current) => {
+ if (current.value.tag === "Ok") {
+ acc[0].push({ x: current.x, value: current.value.value });
+ } else {
+ acc[1].push({ x: current.x, value: current.value.value });
+ }
+ return acc;
+ }, initialPartition);
+
+ let groupedErrors: errors = _.groupBy(errors, (x) => x.value);
+
+ let percentiles: percentiles = functionImage.map(({ x, value }) => {
+ // We convert it to to a pointSet distribution first, so that in case its a sample set
+ // distribution, it doesn't internally convert it to a pointSet distribution for every
+ // single inv() call.
+ let toPointSet: Distribution = unwrap(value.toPointSet());
+ return {
+ x: x,
+ p1: unwrap(toPointSet.inv(0.01)),
+ p5: unwrap(toPointSet.inv(0.05)),
+ p10: unwrap(toPointSet.inv(0.1)),
+ p20: unwrap(toPointSet.inv(0.2)),
+ p30: unwrap(toPointSet.inv(0.3)),
+ p40: unwrap(toPointSet.inv(0.4)),
+ p50: unwrap(toPointSet.inv(0.5)),
+ p60: unwrap(toPointSet.inv(0.6)),
+ p70: unwrap(toPointSet.inv(0.7)),
+ p80: unwrap(toPointSet.inv(0.8)),
+ p90: unwrap(toPointSet.inv(0.9)),
+ p95: unwrap(toPointSet.inv(0.95)),
+ p99: unwrap(toPointSet.inv(0.99)),
+ };
+ });
+
+ return { percentiles, errors: groupedErrors };
+};
+
+export const FunctionChart1Dist: React.FC = ({
+ fn,
+ chartSettings,
+ environment,
+ height,
+}: FunctionChart1DistProps) => {
+ let [mouseOverlay, setMouseOverlay] = React.useState(0);
+ function handleHover(_name: string, value: unknown) {
+ setMouseOverlay(value as number);
+ }
+ function handleOut() {
+ setMouseOverlay(NaN);
+ }
+ const signalListeners = { mousemove: handleHover, mouseout: handleOut };
+ let mouseItem: result = !!mouseOverlay
+ ? runForeign(fn, [mouseOverlay], environment)
+ : {
+ tag: "Error",
+ value: {
+ tag: "REExpectedType",
+ value: "Hover x-coordinate returned NaN. Expected a number.",
+ },
+ };
+ let showChart =
+ mouseItem.tag === "Ok" && mouseItem.value.tag == "distribution" ? (
+
+ ) : (
+ <>>
+ );
+
+ let getPercentilesMemoized = React.useMemo(
+ () => getPercentiles({ chartSettings, fn, environment }),
+ [environment, fn]
+ );
+
+ return (
+ <>
+
+ {showChart}
+ {_.entries(getPercentilesMemoized.errors).map(
+ ([errorName, errorPoints]) => (
+
+ Values:{" "}
+ {errorPoints
+ .map((r, i) => )
+ .reduce((a, b) => (
+ <>
+ {a}, {b}
+ >
+ ))}
+
+ )
+ )}
+ >
+ );
+};
diff --git a/packages/components/src/components/FunctionChart1Number.tsx b/packages/components/src/components/FunctionChart1Number.tsx
new file mode 100644
index 00000000..9d2feca7
--- /dev/null
+++ b/packages/components/src/components/FunctionChart1Number.tsx
@@ -0,0 +1,119 @@
+import * as React from "react";
+import _ from "lodash";
+import type { Spec } from "vega";
+import {
+ result,
+ lambdaValue,
+ environment,
+ runForeign,
+ errorValueToString,
+} from "@quri/squiggle-lang";
+import { createClassFromSpec } from "react-vega";
+import * as lineChartSpec from "../vega-specs/spec-line-chart.json";
+import { ErrorBox } from "./ErrorBox";
+
+let SquiggleLineChart = createClassFromSpec({
+ spec: lineChartSpec as Spec,
+});
+
+const _rangeByCount = (start: number, stop: number, count: number) => {
+ const step = (stop - start) / (count - 1);
+ const items = _.range(start, stop, step);
+ const result = items.concat([stop]);
+ return result;
+};
+
+export type FunctionChartSettings = {
+ start: number;
+ stop: number;
+ count: number;
+};
+
+interface FunctionChart1NumberProps {
+ fn: lambdaValue;
+ chartSettings: FunctionChartSettings;
+ environment: environment;
+ height: number;
+}
+
+type point = { x: number; value: result };
+
+let getFunctionImage = ({ chartSettings, fn, environment }) => {
+ //We adjust the count, because the count is made for distributions, which are much more expensive to estimate
+ let adjustedCount = chartSettings.count * 20;
+
+ let chartPointsToRender = _rangeByCount(
+ chartSettings.start,
+ chartSettings.stop,
+ adjustedCount
+ );
+
+ let chartPointsData: point[] = chartPointsToRender.map((x) => {
+ let result = runForeign(fn, [x], environment);
+ if (result.tag === "Ok") {
+ if (result.value.tag == "number") {
+ return { x, value: { tag: "Ok", value: result.value.value } };
+ } else {
+ return {
+ x,
+ value: {
+ tag: "Error",
+ value: "This component expected number outputs",
+ },
+ };
+ }
+ } else {
+ return {
+ x,
+ value: { tag: "Error", value: errorValueToString(result.value) },
+ };
+ }
+ });
+
+ let initialPartition: [
+ { x: number; value: number }[],
+ { x: number; value: string }[]
+ ] = [[], []];
+
+ let [functionImage, errors] = chartPointsData.reduce((acc, current) => {
+ if (current.value.tag === "Ok") {
+ acc[0].push({ x: current.x, value: current.value.value });
+ } else {
+ acc[1].push({ x: current.x, value: current.value.value });
+ }
+ return acc;
+ }, initialPartition);
+
+ return { errors, functionImage };
+};
+
+export const FunctionChart1Number: React.FC = ({
+ fn,
+ chartSettings,
+ environment,
+ height,
+}: FunctionChart1NumberProps) => {
+ let getFunctionImageMemoized = React.useMemo(
+ () => getFunctionImage({ chartSettings, fn, environment }),
+ [environment, fn]
+ );
+
+ let data = getFunctionImageMemoized.functionImage.map(({ x, value }) => ({
+ x,
+ y: value,
+ }));
+ return (
+ <>
+
+ {getFunctionImageMemoized.errors.map(({ x, value }) => (
+
+ Error at point ${x}
+
+ ))}
+ >
+ );
+};
diff --git a/packages/components/src/components/JsonEditor.tsx b/packages/components/src/components/JsonEditor.tsx
new file mode 100644
index 00000000..c81debf4
--- /dev/null
+++ b/packages/components/src/components/JsonEditor.tsx
@@ -0,0 +1,51 @@
+import _ from "lodash";
+import React, { FC } from "react";
+import AceEditor from "react-ace";
+
+import "ace-builds/src-noconflict/mode-json";
+import "ace-builds/src-noconflict/theme-github";
+
+interface CodeEditorProps {
+ value: string;
+ onChange: (value: string) => void;
+ oneLine?: boolean;
+ width?: number;
+ height: number;
+ showGutter?: boolean;
+}
+
+export let JsonEditor: FC = ({
+ value,
+ onChange,
+ oneLine = false,
+ showGutter = false,
+ height,
+}: CodeEditorProps) => {
+ let lineCount = value.split("\n").length;
+ let id = _.uniqueId();
+ return (
+
+ );
+};
+
+export default JsonEditor;
diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx
index bb166682..f33ccb84 100644
--- a/packages/components/src/components/SquiggleChart.tsx
+++ b/packages/components/src/components/SquiggleChart.tsx
@@ -196,7 +196,7 @@ const SquiggleItem: React.FC = ({
= ({
= ({
= ({
squiggleString = "",
environment,
onChange = () => {},
- height = 60,
+ height = 200,
bindings = defaultBindings,
jsImports = defaultImports,
showSummary = false,
diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx
index f403a4ce..921a3282 100644
--- a/packages/components/src/components/SquigglePlayground.tsx
+++ b/packages/components/src/components/SquigglePlayground.tsx
@@ -3,12 +3,12 @@ import React, { FC, ReactElement, useState } from "react";
import ReactDOM from "react-dom";
import { SquiggleChart } from "./SquiggleChart";
import CodeEditor from "./CodeEditor";
+import JsonEditor from "./JsonEditor";
import styled from "styled-components";
-import {
- defaultBindings,
- environment,
- defaultImports,
-} from "@quri/squiggle-lang";
+import { useForm, useWatch } from "react-hook-form";
+import * as yup from "yup";
+import { yupResolver } from "@hookform/resolvers/yup";
+import { defaultBindings, environment } from "@quri/squiggle-lang";
interface FieldFloatProps {
label: string;
@@ -68,9 +68,14 @@ const Display = styled.div`
max-height: ${(props) => props.maxHeight}px;
`;
-const Row = styled.div`
+interface RowProps {
+ readonly leftPercentage: number;
+}
+
+const Row = styled.div`
display: grid;
- grid-template-columns: 50% 50%;
+ grid-template-columns: ${(p) => p.leftPercentage}% ${(p) =>
+ 100 - p.leftPercentage}%;
`;
const Col = styled.div``;
@@ -87,17 +92,64 @@ interface PlaygroundProps {
showSummary?: boolean;
}
+const schema = yup
+ .object()
+ .shape({
+ sampleCount: yup
+ .number()
+ .required()
+ .positive()
+ .integer()
+ .default(1000)
+ .min(10)
+ .max(1000000),
+ xyPointLength: yup
+ .number()
+ .required()
+ .positive()
+ .integer()
+ .default(1000)
+ .min(10)
+ .max(10000),
+ chartHeight: yup.number().required().positive().integer().default(350),
+ leftSizePercent: yup
+ .number()
+ .required()
+ .positive()
+ .integer()
+ .min(10)
+ .max(100)
+ .default(50),
+ showTypes: yup.boolean(),
+ showControls: yup.boolean(),
+ showSummary: yup.boolean(),
+ showSettingsPage: yup.boolean().default(false),
+ })
+ .required();
+
+type InputProps = {
+ label: string;
+ children: ReactElement;
+};
+
+const InputItem: React.FC = ({ label, children }) => (
+
+
+ {children}
+
+);
+
let SquigglePlayground: FC = ({
initialSquiggleString = "",
- height = 300,
+ height = 500,
showTypes = false,
showControls = false,
showSummary = false,
}: PlaygroundProps) => {
let [squiggleString, setSquiggleString] = useState(initialSquiggleString);
- let [sampleCount, setSampleCount] = useState(1000);
- let [outputXYPoints, setOutputXYPoints] = useState(1000);
- let [pointDistLength, setPointDistLength] = useState(1000);
+ let [importString, setImportString] = useState("{}");
+ let [imports, setImports] = useState({});
+ let [importsAreValid, setImportsAreValid] = useState(true);
let [diagramStart, setDiagramStart] = useState(0);
let [diagramStop, setDiagramStop] = useState(10);
let [diagramCount, setDiagramCount] = useState(20);
@@ -106,21 +158,95 @@ let SquigglePlayground: FC = ({
stop: diagramStop,
count: diagramCount,
};
+ const {
+ register,
+ formState: { errors },
+ control,
+ } = useForm({
+ resolver: yupResolver(schema),
+ defaultValues: {
+ sampleCount: 1000,
+ xyPointLength: 1000,
+ chartHeight: 150,
+ showTypes: showTypes,
+ showControls: showControls,
+ showSummary: showSummary,
+ leftSizePercent: 50,
+ showSettingsPage: false,
+ },
+ });
+ const vars = useWatch({
+ control,
+ });
let env: environment = {
- sampleCount: sampleCount,
- xyPointLength: outputXYPoints,
+ sampleCount: Number(vars.sampleCount),
+ xyPointLength: Number(vars.xyPointLength),
+ };
+ let getChangeJson = (r: string) => {
+ setImportString(r);
+ try {
+ setImports(JSON.parse(r));
+ setImportsAreValid(true);
+ } catch (e) {
+ setImportsAreValid(false);
+ }
};
return (
-
+
+
-
+ {vars.showSettingsPage ? (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <>
+
+ {importsAreValid ? "Valid" : "Invalid"}
+ >
+
+ >
+ ) : (
+
+ )}
@@ -128,12 +254,12 @@ let SquigglePlayground: FC = ({
squiggleString={squiggleString}
environment={env}
chartSettings={chartSettings}
- height={150}
- showTypes={showTypes}
- showControls={showControls}
+ height={vars.chartHeight}
+ showTypes={vars.showTypes}
+ showControls={vars.showControls}
bindings={defaultBindings}
- jsImports={defaultImports}
- showSummary={showSummary}
+ jsImports={imports}
+ showSummary={vars.showSummary}
/>
diff --git a/packages/components/src/stories/SquiggleChart.stories.mdx b/packages/components/src/stories/SquiggleChart.stories.mdx
index 9ad98ef0..bce22c1c 100644
--- a/packages/components/src/stories/SquiggleChart.stories.mdx
+++ b/packages/components/src/stories/SquiggleChart.stories.mdx
@@ -153,11 +153,11 @@ to allow large and small numbers being printed cleanly.
-## Functions
+## Functions (Distribution Output)
+## Functions (Number Output)
+
+
+
## Records