From b015c20fa4b36aa557d97ffa184762d45fd4cb14 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Sun, 15 May 2022 14:39:50 -0400 Subject: [PATCH 1/5] Cached FunctionChart percentiles calculation --- .../src/components/FunctionChart.tsx | 142 ++++++++++-------- .../src/components/SquiggleChart.tsx | 6 +- .../src/components/SquiggleEditor.tsx | 2 +- .../src/stories/SquiggleChart.stories.mdx | 13 ++ 4 files changed, 93 insertions(+), 70 deletions(-) diff --git a/packages/components/src/components/FunctionChart.tsx b/packages/components/src/components/FunctionChart.tsx index 2fd4587d..26a13c78 100644 --- a/packages/components/src/components/FunctionChart.tsx +++ b/packages/components/src/components/FunctionChart.tsx @@ -7,6 +7,8 @@ import { lambdaValue, environment, runForeign, + squiggleExpression, + errorValue, errorValueToString, } from "@quri/squiggle-lang"; import { createClassFromSpec } from "react-vega"; @@ -45,6 +47,23 @@ interface FunctionChartProps { environment: environment; } +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; +}[]; + export const FunctionChart: React.FC = ({ fn, chartSettings, @@ -58,7 +77,9 @@ export const FunctionChart: React.FC = ({ setMouseOverlay(NaN); } const signalListeners = { mousemove: handleHover, mouseout: handleOut }; - let mouseItem = runForeign(fn, [mouseOverlay], environment); + let mouseItem: result = !!mouseOverlay + ? runForeign(fn, [mouseOverlay], { sampleCount: 10000, xyPointLength: 1000 }) + : { tag: "Error", value: { tag: "REExpectedType", value: "Expected float, got NaN" } }; let showChart = mouseItem.tag === "Ok" && mouseItem.value.tag == "distribution" ? ( = ({ chartSettings.count ); type point = { x: number; value: result }; - let valueData: point[] = React.useMemo( - () => - data1.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", - }, - }; - } + + let getPercentiles: () => percentiles = () => { + let valueData:any = data1.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: errorValueToString(result.value) }, + value: { + tag: "Error", + value: + "Cannot currently render functions that don't return distributions", + }, }; } - }), - [environment, fn] - ); + } else { + return { + x, + value: { tag: "Error", value: errorValueToString(result.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 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:percentiles = functionImage.map(({ x, value }) => { + 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.12)), + 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; + }; - let percentiles = functionImage.map(({ x, value }) => { - return { - x: x, - p1: unwrap(value.inv(0.01)), - p5: unwrap(value.inv(0.05)), - p10: unwrap(value.inv(0.12)), - p20: unwrap(value.inv(0.2)), - p30: unwrap(value.inv(0.3)), - p40: unwrap(value.inv(0.4)), - p50: unwrap(value.inv(0.5)), - p60: unwrap(value.inv(0.6)), - p70: unwrap(value.inv(0.7)), - p80: unwrap(value.inv(0.8)), - p90: unwrap(value.inv(0.9)), - p95: unwrap(value.inv(0.95)), - p99: unwrap(value.inv(0.99)), - }; - }); + let _getPercentiles = React.useMemo(getPercentiles, [environment, fn]) - let groupedErrors = _.groupBy(errors, (x) => x.value); return ( <> {showChart} - {_.entries(groupedErrors).map(([errorName, errorPoints]) => ( - - Values:{" "} - {errorPoints - .map((r, i) => ) - .reduce((a, b) => ( - <> - {a}, {b} - - ))} - - ))} ); }; diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index a54ac64d..b3b71e68 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -194,7 +194,7 @@ const SquiggleItem: React.FC = ({ ); } @@ -232,7 +232,7 @@ const ChartWrapper = styled.div` "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; `; -let defaultChartSettings = { start: 0, stop: 10, count: 100 }; +let defaultChartSettings = { start: 0, stop: 10, count: 20 }; export const SquiggleChart: React.FC = ({ squiggleString = "", environment, @@ -247,7 +247,7 @@ export const SquiggleChart: React.FC = ({ chartSettings = defaultChartSettings, }: SquiggleChartProps) => { let expressionResult = run(squiggleString, bindings, environment, jsImports); - let e = environment ? environment : defaultEnvironment; + let e = environment ? environment : { sampleCount: 100000, xyPointLength: 1000 }; let internal: JSX.Element; if (expressionResult.tag === "Ok") { let expression = expressionResult.value; diff --git a/packages/components/src/components/SquiggleEditor.tsx b/packages/components/src/components/SquiggleEditor.tsx index c4ac1876..a69f6ce2 100644 --- a/packages/components/src/components/SquiggleEditor.tsx +++ b/packages/components/src/components/SquiggleEditor.tsx @@ -56,7 +56,7 @@ export let SquiggleEditor: React.FC = ({ environment, diagramStart = 0, diagramStop = 10, - diagramCount = 100, + diagramCount = 20, onChange, bindings = defaultBindings, jsImports = defaultImports, diff --git a/packages/components/src/stories/SquiggleChart.stories.mdx b/packages/components/src/stories/SquiggleChart.stories.mdx index 54ed634d..9b4d3c83 100644 --- a/packages/components/src/stories/SquiggleChart.stories.mdx +++ b/packages/components/src/stories/SquiggleChart.stories.mdx @@ -153,6 +153,19 @@ to allow large and small numbers being printed cleanly. +## Functions + + + {Template.bind({})} + + + ## Records From 91ccd333e0fe19ef7ae933a5754bc5ccd9f0f469 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Sun, 15 May 2022 15:21:00 -0400 Subject: [PATCH 2/5] Minor refactors --- .../src/components/FunctionChart.tsx | 128 ++++++++++++++++-- .../src/components/SquiggleChart.tsx | 7 +- 2 files changed, 117 insertions(+), 18 deletions(-) diff --git a/packages/components/src/components/FunctionChart.tsx b/packages/components/src/components/FunctionChart.tsx index 26a13c78..32919d67 100644 --- a/packages/components/src/components/FunctionChart.tsx +++ b/packages/components/src/components/FunctionChart.tsx @@ -64,6 +64,81 @@ type percentiles = { p99: number; }[]; +type errors = _.Dictionary< + { + x: number; + value: string; + }[] +>; + +type point = { x: number; value: result }; + +let getPercentiles = ({ chartSettings, fn, environment }) => { + let data1 = _rangeByCount( + chartSettings.start, + chartSettings.stop, + chartSettings.count + ); + let valueData: point[] = data1.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] = 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 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.12)), + 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, @@ -78,8 +153,14 @@ export const FunctionChart: React.FC = ({ } const signalListeners = { mousemove: handleHover, mouseout: handleOut }; let mouseItem: result = !!mouseOverlay - ? runForeign(fn, [mouseOverlay], { sampleCount: 10000, xyPointLength: 1000 }) - : { tag: "Error", value: { tag: "REExpectedType", value: "Expected float, got NaN" } }; + ? runForeign(fn, [mouseOverlay], { + sampleCount: 10000, + xyPointLength: 1000, + }) + : { + tag: "Error", + value: { tag: "REExpectedType", value: "Expected float, got NaN" }, + }; let showChart = mouseItem.tag === "Ok" && mouseItem.value.tag == "distribution" ? ( = ({ ) : ( <> ); - let data1 = _rangeByCount( - chartSettings.start, - chartSettings.stop, - chartSettings.count - ); - type point = { x: number; value: result }; - let getPercentiles: () => percentiles = () => { - let valueData:any = data1.map((x) => { + let getPercentiles2: () => { + percentiles: percentiles; + errors: errors; + } = () => { + let data1 = _rangeByCount( + chartSettings.start, + chartSettings.stop, + chartSettings.count + ); + let valueData: point[] = data1.map((x) => { let result = runForeign(fn, [x], environment); if (result.tag === "Ok") { if (result.value.tag == "distribution") { @@ -133,8 +216,11 @@ export const FunctionChart: React.FC = ({ } return acc; }, initialPartition); - - let percentiles:percentiles = functionImage.map(({ x, value }) => { + 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, @@ -153,19 +239,31 @@ export const FunctionChart: React.FC = ({ p99: unwrap(toPointSet.inv(0.99)), }; }); - return percentiles; + return { percentiles, errors: groupedErrors }; }; - let _getPercentiles = React.useMemo(getPercentiles, [environment, fn]) + let _getPercentiles = React.useMemo(getPercentiles2, [environment, fn]); return ( <> {showChart} + {_.entries(_getPercentiles.errors).map(([errorName, errorPoints]) => ( + + Values:{" "} + {errorPoints + .map((r, i) => ) + .reduce((a, b) => ( + <> + {a}, {b} + + ))} + + ))} ); }; diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index b3b71e68..f3f1d9c6 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -10,7 +10,6 @@ import { jsImports, defaultImports, defaultBindings, - defaultEnvironment, } from "@quri/squiggle-lang"; import { NumberShower } from "./NumberShower"; import { DistributionChart } from "./DistributionChart"; @@ -194,7 +193,7 @@ const SquiggleItem: React.FC = ({ ); } @@ -233,6 +232,8 @@ const ChartWrapper = styled.div` `; let defaultChartSettings = { start: 0, stop: 10, count: 20 }; +let defaultEnvironment = { sampleCount: 10000, xyPointLength: 1000 }; + export const SquiggleChart: React.FC = ({ squiggleString = "", environment, @@ -247,7 +248,7 @@ export const SquiggleChart: React.FC = ({ chartSettings = defaultChartSettings, }: SquiggleChartProps) => { let expressionResult = run(squiggleString, bindings, environment, jsImports); - let e = environment ? environment : { sampleCount: 100000, xyPointLength: 1000 }; + let e = environment ? environment : defaultEnvironment; let internal: JSX.Element; if (expressionResult.tag === "Ok") { let expression = expressionResult.value; From 56728c7ea5bc8aed9fc25482b19764676cdffade Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Sun, 15 May 2022 15:23:58 -0400 Subject: [PATCH 3/5] Removed unneeded code --- .../src/components/FunctionChart.tsx | 74 +------------------ 1 file changed, 4 insertions(+), 70 deletions(-) diff --git a/packages/components/src/components/FunctionChart.tsx b/packages/components/src/components/FunctionChart.tsx index 32919d67..10f97b7d 100644 --- a/packages/components/src/components/FunctionChart.tsx +++ b/packages/components/src/components/FunctionChart.tsx @@ -173,76 +173,10 @@ export const FunctionChart: React.FC = ({ <> ); - let getPercentiles2: () => { - percentiles: percentiles; - errors: errors; - } = () => { - let data1 = _rangeByCount( - chartSettings.start, - chartSettings.stop, - chartSettings.count - ); - let valueData: point[] = data1.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] = 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 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.12)), - 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 }; - }; - - let _getPercentiles = React.useMemo(getPercentiles2, [environment, fn]); + let _getPercentiles = React.useMemo( + () => getPercentiles({ chartSettings, fn, environment }), + [environment, fn] + ); return ( <> From b63136080be2ebf0e8097b8bad19eb28e7d15153 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Sun, 15 May 2022 15:29:54 -0400 Subject: [PATCH 4/5] Formatting --- packages/components/src/components/SquiggleChart.tsx | 5 ++++- packages/components/src/stories/SquiggleChart.stories.mdx | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index f3f1d9c6..eb6214e2 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -193,7 +193,10 @@ const SquiggleItem: React.FC = ({ ); } diff --git a/packages/components/src/stories/SquiggleChart.stories.mdx b/packages/components/src/stories/SquiggleChart.stories.mdx index 9b4d3c83..9ad98ef0 100644 --- a/packages/components/src/stories/SquiggleChart.stories.mdx +++ b/packages/components/src/stories/SquiggleChart.stories.mdx @@ -154,6 +154,7 @@ to allow large and small numbers being printed cleanly. ## Functions + Date: Sun, 15 May 2022 15:58:37 -0400 Subject: [PATCH 5/5] Responded to CR --- .../src/components/FunctionChart.tsx | 54 +++++++++++-------- .../src/components/SquiggleChart.tsx | 2 +- 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/packages/components/src/components/FunctionChart.tsx b/packages/components/src/components/FunctionChart.tsx index 10f97b7d..242bf719 100644 --- a/packages/components/src/components/FunctionChart.tsx +++ b/packages/components/src/components/FunctionChart.tsx @@ -74,12 +74,13 @@ type errors = _.Dictionary< type point = { x: number; value: result }; let getPercentiles = ({ chartSettings, fn, environment }) => { - let data1 = _rangeByCount( + let chartPointsToRender = _rangeByCount( chartSettings.start, chartSettings.stop, chartSettings.count ); - let valueData: point[] = data1.map((x) => { + + let chartPointsData: point[] = chartPointsToRender.map((x) => { let result = runForeign(fn, [x], environment); if (result.tag === "Ok") { if (result.value.tag == "distribution") { @@ -101,11 +102,13 @@ let getPercentiles = ({ chartSettings, fn, environment }) => { }; } }); + let initialPartition: [ { x: number; value: Distribution }[], { x: number; value: string }[] ] = [[], []]; - let [functionImage, errors] = valueData.reduce((acc, current) => { + + let [functionImage, errors] = chartPointsData.reduce((acc, current) => { if (current.value.tag === "Ok") { acc[0].push({ x: current.x, value: current.value.value }); } else { @@ -113,7 +116,9 @@ let getPercentiles = ({ chartSettings, fn, environment }) => { } 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 @@ -123,7 +128,7 @@ let getPercentiles = ({ chartSettings, fn, environment }) => { x: x, p1: unwrap(toPointSet.inv(0.01)), p5: unwrap(toPointSet.inv(0.05)), - p10: unwrap(toPointSet.inv(0.12)), + p10: unwrap(toPointSet.inv(0.1)), p20: unwrap(toPointSet.inv(0.2)), p30: unwrap(toPointSet.inv(0.3)), p40: unwrap(toPointSet.inv(0.4)), @@ -136,6 +141,7 @@ let getPercentiles = ({ chartSettings, fn, environment }) => { p99: unwrap(toPointSet.inv(0.99)), }; }); + return { percentiles, errors: groupedErrors }; }; @@ -153,13 +159,13 @@ export const FunctionChart: React.FC = ({ } const signalListeners = { mousemove: handleHover, mouseout: handleOut }; let mouseItem: result = !!mouseOverlay - ? runForeign(fn, [mouseOverlay], { - sampleCount: 10000, - xyPointLength: 1000, - }) + ? runForeign(fn, [mouseOverlay], environment) : { tag: "Error", - value: { tag: "REExpectedType", value: "Expected float, got NaN" }, + value: { + tag: "REExpectedType", + value: "Hover x-coordinate returned NaN. Expected a number.", + }, }; let showChart = mouseItem.tag === "Ok" && mouseItem.value.tag == "distribution" ? ( @@ -173,7 +179,7 @@ export const FunctionChart: React.FC = ({ <> ); - let _getPercentiles = React.useMemo( + let getPercentilesMemoized = React.useMemo( () => getPercentiles({ chartSettings, fn, environment }), [environment, fn] ); @@ -181,23 +187,25 @@ export const FunctionChart: React.FC = ({ return ( <> {showChart} - {_.entries(_getPercentiles.errors).map(([errorName, errorPoints]) => ( - - Values:{" "} - {errorPoints - .map((r, i) => ) - .reduce((a, b) => ( - <> - {a}, {b} - - ))} - - ))} + {_.entries(getPercentilesMemoized.errors).map( + ([errorName, errorPoints]) => ( + + Values:{" "} + {errorPoints + .map((r, i) => ) + .reduce((a, b) => ( + <> + {a}, {b} + + ))} + + ) + )} ); }; diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index eb6214e2..ce562368 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -10,6 +10,7 @@ import { jsImports, defaultImports, defaultBindings, + defaultEnvironment, } from "@quri/squiggle-lang"; import { NumberShower } from "./NumberShower"; import { DistributionChart } from "./DistributionChart"; @@ -235,7 +236,6 @@ const ChartWrapper = styled.div` `; let defaultChartSettings = { start: 0, stop: 10, count: 20 }; -let defaultEnvironment = { sampleCount: 10000, xyPointLength: 1000 }; export const SquiggleChart: React.FC = ({ squiggleString = "",