From f393cfda9f0c807d885dab6ef943c9ee0e3e1413 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Thu, 26 May 2022 14:41:58 -0400 Subject: [PATCH] Simple line chart for Functions --- .../src/components/FunctionChart.tsx | 219 +++--------------- .../src/components/FunctionChart1Dist.tsx | 214 +++++++++++++++++ .../src/components/FunctionChart1Number.tsx | 145 ++++++++++++ .../src/components/SquigglePlayground.tsx | 28 ++- .../src/vega-specs/spec-line-chart.json | 88 +++++++ packages/components/tsconfig.json | 3 +- 6 files changed, 504 insertions(+), 193 deletions(-) create mode 100644 packages/components/src/components/FunctionChart1Dist.tsx create mode 100644 packages/components/src/components/FunctionChart1Number.tsx create mode 100644 packages/components/src/vega-specs/spec-line-chart.json diff --git a/packages/components/src/components/FunctionChart.tsx b/packages/components/src/components/FunctionChart.tsx index d774f3db..5a37f0ca 100644 --- a/packages/components/src/components/FunctionChart.tsx +++ b/packages/components/src/components/FunctionChart.tsx @@ -7,34 +7,11 @@ import { 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 { 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; @@ -48,167 +25,47 @@ interface FunctionChartProps { 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 + 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 result = runForeign(fn, [chartSettings.start], environment); + let resultType = result.tag === "Ok" ? result.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 comp = () => { + 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 comp(); }; diff --git a/packages/components/src/components/FunctionChart1Dist.tsx b/packages/components/src/components/FunctionChart1Dist.tsx new file mode 100644 index 00000000..e1d8333c --- /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 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 FunctionChart1Dist: 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 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..fa72bf71 --- /dev/null +++ b/packages/components/src/components/FunctionChart1Number.tsx @@ -0,0 +1,145 @@ +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 lineChartSpec from "../vega-specs/spec-line-chart.json"; +import { DistributionChart } from "./DistributionChart"; +import { NumberShower } from "./NumberShower"; +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; +}; + +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 FunctionChartProps { + fn: lambdaValue; + chartSettings: FunctionChartSettings; + environment: environment; + height: number; +} + +type point = { x: number; value: result }; + +let getFunctionImage = ({ 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 == "number") { + 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: 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, +}: 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 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/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 1bbd84a2..c28bf11a 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -72,9 +72,13 @@ 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``; @@ -111,6 +115,7 @@ const schema = yup .min(10) .max(10000), chartHeight: yup.number().required().positive().integer().default(350), + leftSize: yup.number().required().positive().integer().min(10).max(100).default(50), showTypes: yup.boolean(), showControls: yup.boolean(), showSummary: yup.boolean(), @@ -152,14 +157,15 @@ let SquigglePlayground: FC = ({ showTypes: showTypes, showControls: showControls, showSummary: showSummary, + leftSize: 50, }, }); - const foo = useWatch({ + const vars = useWatch({ control, }); let env: environment = { - sampleCount: Number(foo.sampleCount), - xyPointLength: Number(foo.xyPointLength), + sampleCount: Number(vars.sampleCount), + xyPointLength: Number(vars.xyPointLength), }; let getChangeJson = (r: string) => { setImportString(r); @@ -169,17 +175,17 @@ let SquigglePlayground: FC = ({ } catch (e) { setImportsAreValid(false); } - (""); }; return ( + - + = ({ squiggleString={squiggleString} environment={env} chartSettings={chartSettings} - height={foo.chartHeight} - showTypes={foo.showTypes} - showControls={foo.showControls} + height={vars.chartHeight} + showTypes={vars.showTypes} + showControls={vars.showControls} bindings={defaultBindings} jsImports={imports} - showSummary={foo.showSummary} + showSummary={vars.showSummary} /> diff --git a/packages/components/src/vega-specs/spec-line-chart.json b/packages/components/src/vega-specs/spec-line-chart.json new file mode 100644 index 00000000..117d9543 --- /dev/null +++ b/packages/components/src/vega-specs/spec-line-chart.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "width": 500, + "height": 200, + "padding": 5, + "data": [ + { + "name": "facet", + "values": [], + "format": { + "type": "json", + "parse": { + "timestamp": "date" + } + } + } + ], + "scales": [ + { + "name": "x", + "type": "linear", + "nice": true, + "domain": { + "data": "facet", + "field": "x" + }, + "range": "width" + }, + { + "name": "y", + "type": "linear", + "range": "height", + "nice": true, + "zero": true, + "domain": { + "data": "facet", + "field": "y" + } + } + ], + "signals": [ + { + "name": "mousemove", + "on": [{ "events": "mousemove", "update": "invert('x', x())" }] + }, + { + "name": "mouseout", + "on": [{ "events": "mouseout", "update": "invert('x', x())" }] + } + ], + "axes": [ + { + "orient": "bottom", + "scale": "x", + "grid": false, + "labelColor": "#727d93", + "tickColor": "#fff", + "tickOpacity": 0.0, + "domainColor": "#727d93", + "domainOpacity": 0.1, + "tickCount": 5 + }, + { + "orient": "left", + "scale": "y", + "grid": false, + "labelColor": "#727d93", + "tickColor": "#fff", + "tickOpacity": 0.0, + "domainColor": "#727d93", + "domainOpacity": 0.1, + "tickCount": 5 + } + ], + "marks": [ + { + "type": "line", + "from": { "data": "facet" }, + "encode": { + "enter": { + "x": { "scale": "x", "field": "x" }, + "y": { "scale": "y", "field": "y" }, + "strokeWidth": { "value": 2 } + } + } + } + ] +} diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index c8d799d5..38d28704 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -20,7 +20,8 @@ }, "files": [ "src/vega-specs/spec-distributions.json", - "src/vega-specs/spec-percentiles.json" + "src/vega-specs/spec-percentiles.json", + "src/vega-specs/spec-line-chart.json" ], "target": "ES6", "include": ["src/**/*", "src/*"],