From e058e315ad1af9139f351d1e0d0f74fdbeb52e28 Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Tue, 10 May 2022 15:52:13 +0000 Subject: [PATCH] Function charting --- .../src/components/FunctionChart.tsx | 121 ++++++++++++------ .../src/components/SquiggleChart.tsx | 65 +++++----- .../src/components/SquiggleEditor.tsx | 35 ++--- .../src/components/SquigglePlayground.tsx | 24 +++- packages/squiggle-lang/src/js/index.ts | 37 ++++-- .../squiggle-lang/src/js/rescript_interop.ts | 21 +++ .../src/rescript/TypescriptInterface.res | 3 + 7 files changed, 202 insertions(+), 104 deletions(-) diff --git a/packages/components/src/components/FunctionChart.tsx b/packages/components/src/components/FunctionChart.tsx index ea00aa9c..8e5abf32 100644 --- a/packages/components/src/components/FunctionChart.tsx +++ b/packages/components/src/components/FunctionChart.tsx @@ -1,18 +1,24 @@ import * as React from "react"; import _ from "lodash"; import type { Spec } from "vega"; -import type { Distribution, errorValue, result } from "@quri/squiggle-lang"; +import { + Distribution, + result, + lambdaValue, + environment, + runForeign, + 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, }); -type distPlusFn = (a: number) => result; - const _rangeByCount = (start: number, stop: number, count: number) => { const step = (stop - start) / (count - 1); const items = _.range(start, stop, step); @@ -27,51 +33,85 @@ function unwrap(x: result): a { throw Error("FAILURE TO UNWRAP"); } } +export type FunctionChartSettings = { + start: number; + stop: number; + count: number; +}; -function mapFilter(xs: a[], f: (x: a) => b | undefined): b[] { - let initial: b[] = []; - return xs.reduce((previous, current) => { - let value: b | undefined = f(current); - if (value !== undefined) { - return previous.concat([value]); - } else { - return previous; - } - }, initial); +interface FunctionChartProps { + fn: lambdaValue; + chartSettings: FunctionChartSettings; + environment: environment; } -export const FunctionChart: React.FC<{ - distPlusFn: distPlusFn; - diagramStart: number; - diagramStop: number; - diagramCount: number; -}> = ({ distPlusFn, diagramStart, diagramStop, diagramCount }) => { +export const FunctionChart: React.FC = ({ + fn, + chartSettings, + environment, +}: FunctionChartProps) => { let [mouseOverlay, setMouseOverlay] = React.useState(0); - function handleHover(...args) { - setMouseOverlay(args[1]); + function handleHover(_name: string, value: unknown) { + setMouseOverlay(value as number); } function handleOut() { setMouseOverlay(NaN); } const signalListeners = { mousemove: handleHover, mouseout: handleOut }; - let mouseItem = distPlusFn(mouseOverlay); + let mouseItem = runForeign(fn, [mouseOverlay], environment); let showChart = - mouseItem.tag === "Ok" ? ( + mouseItem.tag === "Ok" && mouseItem.value.tag == "distribution" ? ( ) : ( <> ); - let data1 = _rangeByCount(diagramStart, diagramStop, diagramCount); - let valueData = mapFilter(data1, (x) => { - let result = distPlusFn(x); + let data1 = _rangeByCount( + chartSettings.start, + chartSettings.stop, + chartSettings.count + ); + type point = { x: number; value: result }; + let valueData: point[] = data1.map((x) => { + let result = runForeign(fn, [x], environment); if (result.tag === "Ok") { - return { x: x, value: result.value }; + 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) }, + }; } - }).map(({ x, value }) => { + }); + + let initialPartition: [ + { x: number; value: Distribution }[], + { x: number; value: string }[] + ] = [[], []]; + let [functionImage, errors] = valueData.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 percentiles = functionImage.map(({ x, value }) => { return { x: x, p1: unwrap(value.inv(0.01)), @@ -90,24 +130,25 @@ export const FunctionChart: React.FC<{ }; }); - let errorData = mapFilter(data1, (x) => { - let result = distPlusFn(x); - if (result.tag === "Error") { - return { x: x, error: result.value }; - } - }); - let error2 = _.groupBy(errorData, (x) => x.error); + let groupedErrors = _.groupBy(errors, (x) => x.value); return ( <> {showChart} - {_.keysIn(error2).map((k) => ( - - {`Values: [${error2[k].map((r) => r.x.toFixed(2)).join(",")}]`} + {_.entries(groupedErrors).map(([errorName, errorPoints]) => ( + + Values:{" "} + {errorPoints + .map((r) => ) + .reduce((a, b) => ( + <> + {a}, {b} + + ))} ))} diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index 638dcb34..c833f9c3 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -6,7 +6,7 @@ import { errorValueToString, squiggleExpression, bindings, - samplingParams, + environment, jsImports, defaultImports, defaultBindings, @@ -14,6 +14,7 @@ import { import { NumberShower } from "./NumberShower"; import { DistributionChart } from "./DistributionChart"; import { ErrorBox } from "./ErrorBox"; +import { FunctionChart, FunctionChartSettings } from "./FunctionChart"; const variableBox = { Component: styled.div` @@ -36,7 +37,7 @@ const variableBox = { interface VariableBoxProps { heading: string; children: React.ReactNode; - showTypes?: boolean; + showTypes: boolean; } export const VariableBox: React.FC = ({ @@ -66,9 +67,13 @@ export interface SquiggleItemProps { width?: number; height: number; /** Whether to show type information */ - showTypes?: boolean; + showTypes: boolean; /** Whether to show users graph controls (scale etc) */ - showControls?: boolean; + showControls: boolean; + /** Settings for displaying functions */ + chartSettings: FunctionChartSettings; + /** Environment for further function executions */ + environment: environment; } const SquiggleItem: React.FC = ({ @@ -77,6 +82,8 @@ const SquiggleItem: React.FC = ({ height, showTypes = false, showControls = false, + chartSettings, + environment, }: SquiggleItemProps) => { switch (expression.tag) { case "number": @@ -143,6 +150,8 @@ const SquiggleItem: React.FC = ({ height={50} showTypes={showTypes} showControls={showControls} + chartSettings={chartSettings} + environment={environment} /> ))} @@ -159,6 +168,8 @@ const SquiggleItem: React.FC = ({ height={50} showTypes={showTypes} showControls={showControls} + chartSettings={chartSettings} + environment={environment} /> ))} @@ -172,9 +183,11 @@ const SquiggleItem: React.FC = ({ ); case "lambda": return ( - - There is no viewer currently available for function types. - + ); } }; @@ -185,28 +198,22 @@ export interface SquiggleChartProps { /** If the output requires monte carlo sampling, the amount of samples */ sampleCount?: number; /** The amount of points returned to draw the distribution */ - outputXYPoints?: number; - kernelWidth?: number; - pointDistLength?: number; - /** If the result is a function, where the function starts */ - diagramStart?: number; - /** If the result is a function, where the function ends */ - diagramStop?: number; - /** If the result is a function, how many points along the function it samples */ - diagramCount?: number; + environment: environment; + /** If the result is a function, where the function starts, ends and the amount of stops */ + chartSettings?: FunctionChartSettings; /** When the environment changes */ onChange?(expr: squiggleExpression): void; /** CSS width of the element */ width?: number; height?: number; /** Bindings of previous variables declared */ - bindings?: bindings; + bindings: bindings; /** JS imported parameters */ - jsImports?: jsImports; + jsImports: jsImports; /** Whether to show type information about returns, default false */ - showTypes?: boolean; + showTypes: boolean; /** Whether to show graph controls (scale etc)*/ - showControls?: boolean; + showControls: boolean; } const ChartWrapper = styled.div` @@ -215,10 +222,10 @@ const ChartWrapper = styled.div` "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; `; +let defaultChartSettings = { start: 0, stop: 10, count: 100 }; export const SquiggleChart: React.FC = ({ squiggleString = "", - sampleCount = 1000, - outputXYPoints = 1000, + environment, onChange = () => {}, height = 60, bindings = defaultBindings, @@ -226,17 +233,9 @@ export const SquiggleChart: React.FC = ({ width, showTypes = false, showControls = false, + chartSettings = defaultChartSettings, }: SquiggleChartProps) => { - let samplingInputs: samplingParams = { - sampleCount: sampleCount, - xyPointLength: outputXYPoints, - }; - let expressionResult = run( - squiggleString, - bindings, - samplingInputs, - jsImports - ); + let expressionResult = run(squiggleString, bindings, environment, jsImports); let internal: JSX.Element; if (expressionResult.tag === "Ok") { let expression = expressionResult.value; @@ -248,6 +247,8 @@ export const SquiggleChart: React.FC = ({ height={height} showTypes={showTypes} showControls={showControls} + chartSettings={chartSettings} + environment={environment} /> ); } else { diff --git a/packages/components/src/components/SquiggleEditor.tsx b/packages/components/src/components/SquiggleEditor.tsx index 572dbd76..8e99182d 100644 --- a/packages/components/src/components/SquiggleEditor.tsx +++ b/packages/components/src/components/SquiggleEditor.tsx @@ -5,7 +5,7 @@ import { CodeEditor } from "./CodeEditor"; import styled from "styled-components"; import type { squiggleExpression, - samplingParams, + environment, bindings, jsImports, } from "@quri/squiggle-lang"; @@ -24,8 +24,6 @@ export interface SquiggleEditorProps { sampleCount?: number; /** The amount of points returned to draw the distribution */ outputXYPoints?: number; - kernelWidth?: number; - pointDistLength?: number; /** If the result is a function, where the function starts */ diagramStart?: number; /** If the result is a function, where the function ends */ @@ -55,13 +53,11 @@ const Input = styled.div` export let SquiggleEditor: React.FC = ({ initialSquiggleString = "", width, - sampleCount, - outputXYPoints, - kernelWidth, - pointDistLength, - diagramStart, - diagramStop, - diagramCount, + sampleCount = 1000, + outputXYPoints = 1000, + diagramStart = 0, + diagramStop = 10, + diagramCount = 100, onChange, bindings = defaultBindings, jsImports = defaultImports, @@ -69,6 +65,15 @@ export let SquiggleEditor: React.FC = ({ showControls = false, }: SquiggleEditorProps) => { let [expression, setExpression] = React.useState(initialSquiggleString); + let chartSettings = { + start: diagramStart, + stop: diagramStop, + count: diagramCount, + }; + let env: environment = { + sampleCount: sampleCount, + xyPointLength: outputXYPoints, + }; return (
@@ -82,14 +87,10 @@ export let SquiggleEditor: React.FC = ({ = ({ outputXYPoints = 1000, jsImports = defaultImports, }: SquigglePartialProps) => { - let samplingInputs: samplingParams = { + let samplingInputs: environment = { sampleCount: sampleCount, xyPointLength: outputXYPoints, }; diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 424cff8d..44fc4803 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -4,6 +4,11 @@ import ReactDOM from "react-dom"; import { SquiggleChart } from "./SquiggleChart"; import CodeEditor from "./CodeEditor"; import styled from "styled-components"; +import { + defaultBindings, + environment, + defaultImports, +} from "@quri/squiggle-lang"; interface FieldFloatProps { label: string; @@ -89,6 +94,15 @@ let SquigglePlayground: FC = ({ let [diagramStart, setDiagramStart] = useState(0); let [diagramStop, setDiagramStop] = useState(10); let [diagramCount, setDiagramCount] = useState(20); + let chartSettings = { + start: diagramStart, + stop: diagramStop, + count: diagramCount, + }; + let env: environment = { + sampleCount: sampleCount, + xyPointLength: outputXYPoints, + }; return ( @@ -105,15 +119,13 @@ let SquigglePlayground: FC = ({ diff --git a/packages/squiggle-lang/src/js/index.ts b/packages/squiggle-lang/src/js/index.ts index 961d4935..306e5a07 100644 --- a/packages/squiggle-lang/src/js/index.ts +++ b/packages/squiggle-lang/src/js/index.ts @@ -1,6 +1,5 @@ import * as _ from "lodash"; import { - samplingParams, environment, defaultEnvironment, evaluatePartialUsingExternalBindings, @@ -8,6 +7,7 @@ import { externalBindings, expressionValue, errorValue, + foreignFunctionInterface, } from "../rescript/TypescriptInterface.gen"; export { makeSampleSetDist, @@ -15,25 +15,30 @@ export { distributionErrorToString, distributionError, } from "../rescript/TypescriptInterface.gen"; -export type { - samplingParams, - errorValue, - externalBindings as bindings, - jsImports, -}; +export type { errorValue, externalBindings as bindings, jsImports }; import { jsValueToBinding, + jsValueToExpressionValue, jsValue, rescriptExport, squiggleExpression, convertRawToTypescript, + lambdaValue, } from "./rescript_interop"; import { result, resultMap, tag, tagged } from "./types"; import { Distribution, shape } from "./distribution"; -export { Distribution, squiggleExpression, result, resultMap, shape }; +export { + Distribution, + squiggleExpression, + result, + resultMap, + shape, + lambdaValue, + environment, +}; -export let defaultSamplingInputs: samplingParams = { +export let defaultSamplingInputs: environment = { sampleCount: 10000, xyPointLength: 10000, }; @@ -72,6 +77,20 @@ export function runPartial( ); } +export function runForeign( + fn: lambdaValue, + args: jsValue[], + environment?: environment +): result { + let e = environment ? environment : defaultEnvironment; + let res: result = foreignFunctionInterface( + fn, + args.map(jsValueToExpressionValue), + e + ); + return resultMap(res, (x) => createTsExport(x, e)); +} + function mergeImportsWithBindings( bindings: externalBindings, imports: jsImports diff --git a/packages/squiggle-lang/src/js/rescript_interop.ts b/packages/squiggle-lang/src/js/rescript_interop.ts index 45f4124b..0781f081 100644 --- a/packages/squiggle-lang/src/js/rescript_interop.ts +++ b/packages/squiggle-lang/src/js/rescript_interop.ts @@ -1,5 +1,6 @@ import * as _ from "lodash"; import { + expressionValue, mixedShape, sampleSetDist, genericDist, @@ -87,6 +88,8 @@ export type squiggleExpression = | tagged<"number", number> | tagged<"record", { [key: string]: squiggleExpression }>; +export { lambdaValue }; + export function convertRawToTypescript( result: rescriptExport, environment: environment @@ -168,3 +171,21 @@ export function jsValueToBinding(value: jsValue): rescriptExport { return { TAG: 7, _0: _.mapValues(value, jsValueToBinding) }; } } + +export function jsValueToExpressionValue(value: jsValue): expressionValue { + if (typeof value === "boolean") { + return { tag: "EvBool", value: value as boolean }; + } else if (typeof value === "string") { + return { tag: "EvString", value: value as string }; + } else if (typeof value === "number") { + return { tag: "EvNumber", value: value as number }; + } else if (Array.isArray(value)) { + return { tag: "EvArray", value: value.map(jsValueToExpressionValue) }; + } else { + // Record + return { + tag: "EvRecord", + value: _.mapValues(value, jsValueToExpressionValue), + }; + } +} diff --git a/packages/squiggle-lang/src/rescript/TypescriptInterface.res b/packages/squiggle-lang/src/rescript/TypescriptInterface.res index 6ebb8377..13763e72 100644 --- a/packages/squiggle-lang/src/rescript/TypescriptInterface.res +++ b/packages/squiggle-lang/src/rescript/TypescriptInterface.res @@ -84,3 +84,6 @@ type environment = ReducerInterface_ExpressionValue.environment @genType let defaultEnvironment = ReducerInterface_ExpressionValue.defaultEnvironment + +@genType +let foreignFunctionInterface = Reducer.foreignFunctionInterface