From 624e788094633d8b3fd517e4d367f2c0062c8553 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Thu, 7 Apr 2022 07:56:17 -0400 Subject: [PATCH 1/5] Cleaning axis of vega graphs --- .../components/src/spec-distributions.json | 9 ++-- packages/components/src/spec-percentiles.json | 47 +++++-------------- 2 files changed, 18 insertions(+), 38 deletions(-) diff --git a/packages/components/src/spec-distributions.json b/packages/components/src/spec-distributions.json index 40aebfe4..afd62dcb 100644 --- a/packages/components/src/spec-distributions.json +++ b/packages/components/src/spec-distributions.json @@ -82,10 +82,13 @@ { "orient": "bottom", "scale": "xscale", - "labelColor": "#666", - "tickColor": "#ddd", + "labelColor": "#727d93", + "tickColor": "#fff", + "tickOpacity": 0.0, + "domainColor": "#fff", + "domainOpacity": 0.0, "format": "~s", - "tickCount": 20 + "tickCount": 10 } ], "marks": [ diff --git a/packages/components/src/spec-percentiles.json b/packages/components/src/spec-percentiles.json index 64b9035d..5751f924 100644 --- a/packages/components/src/spec-percentiles.json +++ b/packages/components/src/spec-percentiles.json @@ -98,46 +98,23 @@ "orient": "bottom", "scale": "xscale", "grid": false, - "tickSize": 2, - "encode": { - "grid": { - "enter": { - "stroke": { - "value": "#ccc" - } - } - }, - "ticks": { - "enter": { - "stroke": { - "value": "#ccc" - } - } - } - } + "labelColor": "#727d93", + "tickColor": "#fff", + "tickOpacity": 0.0, + "domainColor": "#727d93", + "domainOpacity": 0.1, + "tickCount": 5 }, { "orient": "left", "scale": "yscale", "grid": false, - "domain": false, - "tickSize": 2, - "encode": { - "grid": { - "enter": { - "stroke": { - "value": "#ccc" - } - } - }, - "ticks": { - "enter": { - "stroke": { - "value": "#ccc" - } - } - } - } + "labelColor": "#727d93", + "tickColor": "#fff", + "tickOpacity": 0.0, + "domainColor": "#727d93", + "domainOpacity": 0.1, + "tickCount": 5 } ], "marks": [ From f63c775cb6060c97721a26074ffaa50434a34716 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Thu, 7 Apr 2022 09:53:45 -0400 Subject: [PATCH 2/5] Simple implementation of function hover working --- packages/components/src/SquiggleChart.tsx | 356 ++++++++++-------- packages/components/src/spec-percentiles.json | 20 +- .../src/stories/SquiggleChart.stories.mdx | 2 +- 3 files changed, 224 insertions(+), 154 deletions(-) diff --git a/packages/components/src/SquiggleChart.tsx b/packages/components/src/SquiggleChart.tsx index e381127c..ce978706 100644 --- a/packages/components/src/SquiggleChart.tsx +++ b/packages/components/src/SquiggleChart.tsx @@ -13,6 +13,7 @@ import * as chartSpecification from "./spec-distributions.json"; import * as percentilesSpec from "./spec-percentiles.json"; import { NumberShower } from "./NumberShower"; import styled from "styled-components"; +import { CONTINUOUS_TO_DISCRETE_SCALES } from "vega-lite/build/src/scale"; let SquiggleVegaChart = createClassFromSpec({ spec: chartSpecification as Spec, @@ -65,6 +66,195 @@ const ShowError: React.FC<{ heading: string; children: React.ReactNode }> = ({ ); }; +export const DistPlusChart: React.FC<{ + distPlus: DistPlus; + width: number; + height: number; +}> = ({ distPlus, width, height }) => { + let shape = distPlus.pointSetDist; + if (shape.tag === "Continuous") { + let xyShape = shape.value.xyShape; + let totalY = xyShape.ys.reduce((a, b) => a + b); + let total = 0; + let cdf = xyShape.ys.map((y) => { + total += y; + return total / totalY; + }); + let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x, y]) => ({ + cdf: (c * 100).toFixed(2) + "%", + x: x, + y: y, + })); + + return ( + + ); + } else if (shape.tag === "Discrete") { + let xyShape = shape.value.xyShape; + let totalY = xyShape.ys.reduce((a, b) => a + b); + let total = 0; + let cdf = xyShape.ys.map((y) => { + total += y; + return total / totalY; + }); + let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x, y]) => ({ + cdf: (c * 100).toFixed(2) + "%", + x: x, + y: y, + })); + + return ; + } else if (shape.tag === "Mixed") { + let discreteShape = shape.value.discrete.xyShape; + let totalDiscrete = discreteShape.ys.reduce((a, b) => a + b); + + let discretePoints = _.zip(discreteShape.xs, discreteShape.ys); + let continuousShape = shape.value.continuous.xyShape; + let continuousPoints = _.zip(continuousShape.xs, continuousShape.ys); + + interface labeledPoint { + x: number; + y: number; + type: "discrete" | "continuous"; + } + + let markedDisPoints: labeledPoint[] = discretePoints.map(([x, y]) => ({ + x: x, + y: y, + type: "discrete", + })); + let markedConPoints: labeledPoint[] = continuousPoints.map(([x, y]) => ({ + x: x, + y: y, + type: "continuous", + })); + + let sortedPoints = _.sortBy(markedDisPoints.concat(markedConPoints), "x"); + + let totalContinuous = 1 - totalDiscrete; + let totalY = continuousShape.ys.reduce((a: number, b: number) => a + b); + + let total = 0; + let cdf = sortedPoints.map((point: labeledPoint) => { + if (point.type === "discrete") { + total += point.y; + return total; + } else if (point.type === "continuous") { + total += (point.y / totalY) * totalContinuous; + return total; + } + }); + + interface cdfLabeledPoint { + cdf: string; + x: number; + y: number; + type: "discrete" | "continuous"; + } + let cdfLabeledPoint: cdfLabeledPoint[] = _.zipWith( + cdf, + sortedPoints, + (c: number, point: labeledPoint) => ({ + ...point, + cdf: (c * 100).toFixed(2) + "%", + }) + ); + let continuousValues = cdfLabeledPoint.filter( + (x) => x.type === "continuous" + ); + let discreteValues = cdfLabeledPoint.filter((x) => x.type === "discrete"); + + return ( + + ); + } +}; + +const _rangeByCount = (start, stop, count) => { + const step = (stop - start) / (count - 1); + const items = _.range(start, stop, step); + const result = items.concat([stop]); + return result; +}; + +type distPlusFn = ( + a: number +) => { tag: "Ok"; value: DistPlus } | { tag: "Error"; value: string }; + +// This could really use a line in the location of the signal. I couldn't get it to work. +// https://vega.github.io/vega/docs/signals/#handlers + +export const FunctionChart: React.FC<{ + distPlusFn: distPlusFn; + diagramStart: number; + diagramStop: number; + diagramCount: number; +}> = ({ distPlusFn, diagramStart, diagramStop, diagramCount }) => { + let [mouseOverlay, setMouseOverlay] = React.useState(NaN); + function handleHover(...args) { + setMouseOverlay(args[1]); + } + function handleOut(...args) { + setMouseOverlay(NaN); + } + const signalListeners = { mousemove: handleHover, mouseout: handleOut }; + let percentileArray = [ + 0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, + ]; + let mouseItem = distPlusFn(mouseOverlay); + let showChart = + mouseItem.tag === "Ok" ? ( + + ) : ( + <> + ); + let data = _rangeByCount(diagramStart, diagramStop, diagramCount) + .map((x) => { + let result = distPlusFn(x); + if (result.tag === "Ok") { + let percentiles = getPercentiles(percentileArray, result.value); + return { + x: x, + p1: percentiles[0], + p5: percentiles[1], + p10: percentiles[2], + p20: percentiles[3], + p30: percentiles[4], + p40: percentiles[5], + p50: percentiles[6], + p60: percentiles[7], + p70: percentiles[8], + p80: percentiles[9], + p90: percentiles[10], + p95: percentiles[11], + p99: percentiles[12], + }; + } else { + console.log("Error", x, result); + return null; + } + }) + .filter((x) => x !== null); + return ( + <> + + {showChart} + + ); +}; + export const SquiggleChart: React.FC = ({ squiggleString = "", sampleCount = 1000, @@ -95,154 +285,20 @@ export const SquiggleChart: React.FC = ({ if (chartResult["NAME"] === "Float") { return ; } else if (chartResult["NAME"] === "DistPlus") { - let shape = chartResult.VAL.pointSetDist; - if (shape.tag === "Continuous") { - let xyShape = shape.value.xyShape; - let totalY = xyShape.ys.reduce((a, b) => a + b); - let total = 0; - let cdf = xyShape.ys.map((y) => { - total += y; - return total / totalY; - }); - let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x, y]) => ({ - cdf: (c * 100).toFixed(2) + "%", - x: x, - y: y, - })); - - return ( - - ); - } else if (shape.tag === "Discrete") { - let xyShape = shape.value.xyShape; - let totalY = xyShape.ys.reduce((a, b) => a + b); - let total = 0; - let cdf = xyShape.ys.map((y) => { - total += y; - return total / totalY; - }); - let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x, y]) => ({ - cdf: (c * 100).toFixed(2) + "%", - x: x, - y: y, - })); - - return ; - } else if (shape.tag === "Mixed") { - let discreteShape = shape.value.discrete.xyShape; - let totalDiscrete = discreteShape.ys.reduce((a, b) => a + b); - - let discretePoints = _.zip(discreteShape.xs, discreteShape.ys); - let continuousShape = shape.value.continuous.xyShape; - let continuousPoints = _.zip(continuousShape.xs, continuousShape.ys); - - interface labeledPoint { - x: number; - y: number; - type: "discrete" | "continuous"; - } - - let markedDisPoints: labeledPoint[] = discretePoints.map( - ([x, y]) => ({ x: x, y: y, type: "discrete" }) - ); - let markedConPoints: labeledPoint[] = continuousPoints.map( - ([x, y]) => ({ x: x, y: y, type: "continuous" }) - ); - - let sortedPoints = _.sortBy( - markedDisPoints.concat(markedConPoints), - "x" - ); - - let totalContinuous = 1 - totalDiscrete; - let totalY = continuousShape.ys.reduce( - (a: number, b: number) => a + b - ); - - let total = 0; - let cdf = sortedPoints.map((point: labeledPoint) => { - if (point.type === "discrete") { - total += point.y; - return total; - } else if (point.type === "continuous") { - total += (point.y / totalY) * totalContinuous; - return total; - } - }); - - interface cdfLabeledPoint { - cdf: string; - x: number; - y: number; - type: "discrete" | "continuous"; - } - let cdfLabeledPoint: cdfLabeledPoint[] = _.zipWith( - cdf, - sortedPoints, - (c: number, point: labeledPoint) => ({ - ...point, - cdf: (c * 100).toFixed(2) + "%", - }) - ); - let continuousValues = cdfLabeledPoint.filter( - (x) => x.type === "continuous" - ); - let discreteValues = cdfLabeledPoint.filter( - (x) => x.type === "discrete" - ); - - return ( - - ); - } - } else if (chartResult.NAME === "Function") { - // We are looking at a function. In this case, we draw a Percentiles chart - let start = diagramStart; - let stop = diagramStop; - let count = diagramCount; - let step = (stop - start) / count; - let data = _.range(start, stop, step).map((x) => { - if (chartResult.NAME === "Function") { - let result = chartResult.VAL(x); - if (result.tag === "Ok") { - let percentileArray = [ - 0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, - 0.99, - ]; - - let percentiles = getPercentiles(percentileArray, result.value); - return { - x: x, - p1: percentiles[0], - p5: percentiles[1], - p10: percentiles[2], - p20: percentiles[3], - p30: percentiles[4], - p40: percentiles[5], - p50: percentiles[6], - p60: percentiles[7], - p70: percentiles[8], - p80: percentiles[9], - p90: percentiles[10], - p95: percentiles[11], - p99: percentiles[12], - }; - } - return null; - } - }); return ( - x !== null) }} - actions={false} + + ); + } else if (chartResult.NAME === "Function") { + return ( + ); } @@ -250,11 +306,7 @@ export const SquiggleChart: React.FC = ({ return <>{chartResults}; } else if (result.tag === "Error") { // At this point, we came across an error. What was our error? - return ( - - {result.value} - - ); + return {result.value}; } return

{"Invalid Response"}

; }; diff --git a/packages/components/src/spec-percentiles.json b/packages/components/src/spec-percentiles.json index 5751f924..a9fc08d4 100644 --- a/packages/components/src/spec-percentiles.json +++ b/packages/components/src/spec-percentiles.json @@ -1,7 +1,7 @@ { "$schema": "https://vega.github.io/schema/vega/v5.json", "width": 500, - "height": 400, + "height": 200, "padding": 5, "data": [ { @@ -93,6 +93,16 @@ } } ], + "signals": [ + { + "name": "mousemove", + "on": [{"events": "mousemove", "update": "invert('xscale', x())"}] + }, + { + "name": "mouseout", + "on": [{"events": "mouseout", "update": "invert('xscale', x())"}] + } + ], "axes": [ { "orient": "bottom", @@ -118,6 +128,14 @@ } ], "marks": [ + { + "type": "rule", + "encode": { + "update": { + "xscale": {"scale": "xscale", "signal": "mousemove"} + } + } + }, { "type": "area", "from": { diff --git a/packages/components/src/stories/SquiggleChart.stories.mdx b/packages/components/src/stories/SquiggleChart.stories.mdx index 76c40228..94273232 100644 --- a/packages/components/src/stories/SquiggleChart.stories.mdx +++ b/packages/components/src/stories/SquiggleChart.stories.mdx @@ -83,7 +83,7 @@ The default is show 10 points between 0 and 10. {Template.bind({})} From 382733e6f309df8a546de5a9b991aa4bf720bdd2 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Thu, 7 Apr 2022 11:26:18 -0400 Subject: [PATCH 3/5] Show errors in FunctionChart --- packages/components/src/SquiggleChart.tsx | 63 ++++++++++++++--------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/packages/components/src/SquiggleChart.tsx b/packages/components/src/SquiggleChart.tsx index ce978706..7b0b3e43 100644 --- a/packages/components/src/SquiggleChart.tsx +++ b/packages/components/src/SquiggleChart.tsx @@ -216,41 +216,57 @@ export const FunctionChart: React.FC<{ ) : ( <> ); - let data = _rangeByCount(diagramStart, diagramStop, diagramCount) + let data1 = _rangeByCount(diagramStart, diagramStop, diagramCount); + let valueData = data1 .map((x) => { let result = distPlusFn(x); if (result.tag === "Ok") { - let percentiles = getPercentiles(percentileArray, result.value); - return { - x: x, - p1: percentiles[0], - p5: percentiles[1], - p10: percentiles[2], - p20: percentiles[3], - p30: percentiles[4], - p40: percentiles[5], - p50: percentiles[6], - p60: percentiles[7], - p70: percentiles[8], - p80: percentiles[9], - p90: percentiles[10], - p95: percentiles[11], - p99: percentiles[12], - }; - } else { - console.log("Error", x, result); - return null; - } + return { x: x, value: result.value }; + } else return null; + }) + .filter((x) => x !== null) + .map(({ x, value }) => { + let percentiles = getPercentiles(percentileArray, value); + return { + x: x, + p1: percentiles[0], + p5: percentiles[1], + p10: percentiles[2], + p20: percentiles[3], + p30: percentiles[4], + p40: percentiles[5], + p50: percentiles[6], + p60: percentiles[7], + p70: percentiles[8], + p80: percentiles[9], + p90: percentiles[10], + p95: percentiles[11], + p99: percentiles[12], + }; + }); + + let errorData = data1 + .map((x) => { + let result = distPlusFn(x); + if (result.tag === "Error") { + return { x: x, error: result.value }; + } else return null; }) .filter((x) => x !== null); + let error2 = _.groupBy(errorData, (x) => x.error); return ( <> {showChart} + {_.keysIn(error2).map((k) => ( + + {`Values: [${error2[k].map((r) => r.x.toFixed(2)).join(",")}]`} + + ))} ); }; @@ -308,7 +324,6 @@ export const SquiggleChart: React.FC = ({ // At this point, we came across an error. What was our error? return {result.value}; } - return

{"Invalid Response"}

; }; function getPercentiles(percentiles: number[], t: DistPlus) { From b85c4f9a6b42d273ef81597edb1e5156adffe5b3 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Thu, 7 Apr 2022 13:26:47 -0400 Subject: [PATCH 4/5] Separated out parts of SquiggleChart, and renamed files around --- packages/components/src/SquiggleChart.tsx | 413 ------------------ .../src/{ => components}/CodeEditor.tsx | 0 .../src/components/DistPlusChart.tsx | 124 ++++++ packages/components/src/components/Error.tsx | 20 + .../src/components/FunctionChart.tsx | 188 ++++++++ .../src/{ => components}/NumberShower.tsx | 0 .../src/components/SquiggleChart.tsx | 91 ++++ .../src/{ => components}/SquiggleEditor.tsx | 0 .../{ => components}/SquigglePlayground.tsx | 0 packages/components/src/index.ts | 6 +- .../src/stories/NumberShower.stories.mdx | 2 +- .../src/stories/SquiggleChart.stories.mdx | 2 +- .../src/stories/SquiggleEditor.stories.mdx | 2 +- .../stories/SquigglePlayground.stories.mdx | 2 +- .../{ => vega-specs}/spec-distributions.json | 0 .../{ => vega-specs}/spec-percentiles.json | 0 packages/components/tsconfig.json | 2 +- 17 files changed, 431 insertions(+), 421 deletions(-) delete mode 100644 packages/components/src/SquiggleChart.tsx rename packages/components/src/{ => components}/CodeEditor.tsx (100%) create mode 100644 packages/components/src/components/DistPlusChart.tsx create mode 100644 packages/components/src/components/Error.tsx create mode 100644 packages/components/src/components/FunctionChart.tsx rename packages/components/src/{ => components}/NumberShower.tsx (100%) create mode 100644 packages/components/src/components/SquiggleChart.tsx rename packages/components/src/{ => components}/SquiggleEditor.tsx (100%) rename packages/components/src/{ => components}/SquigglePlayground.tsx (100%) rename packages/components/src/{ => vega-specs}/spec-distributions.json (100%) rename packages/components/src/{ => vega-specs}/spec-percentiles.json (100%) diff --git a/packages/components/src/SquiggleChart.tsx b/packages/components/src/SquiggleChart.tsx deleted file mode 100644 index 7b0b3e43..00000000 --- a/packages/components/src/SquiggleChart.tsx +++ /dev/null @@ -1,413 +0,0 @@ -import * as React from "react"; -import _ from "lodash"; -import type { Spec } from "vega"; -import { run } from "@quri/squiggle-lang"; -import type { - DistPlus, - SamplingInputs, - exportEnv, - exportDistribution, -} from "@quri/squiggle-lang"; -import { createClassFromSpec } from "react-vega"; -import * as chartSpecification from "./spec-distributions.json"; -import * as percentilesSpec from "./spec-percentiles.json"; -import { NumberShower } from "./NumberShower"; -import styled from "styled-components"; -import { CONTINUOUS_TO_DISCRETE_SCALES } from "vega-lite/build/src/scale"; - -let SquiggleVegaChart = createClassFromSpec({ - spec: chartSpecification as Spec, -}); - -let SquigglePercentilesChart = createClassFromSpec({ - spec: percentilesSpec as Spec, -}); - -export interface SquiggleChartProps { - /** The input string for squiggle */ - squiggleString?: string; - - /** 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; - /** variables declared before this expression */ - environment?: exportEnv; - /** When the environment changes */ - onEnvChange?(env: exportEnv): void; - /** CSS width of the element */ - width?: number; - height?: number; -} - -const Error = styled.div` - border: 1px solid #792e2e; - background: #eee2e2; - padding: 0.4em 0.8em; -`; - -const ShowError: React.FC<{ heading: string; children: React.ReactNode }> = ({ - heading = "Error", - children, -}) => { - return ( - -

{heading}

- {children} -
- ); -}; - -export const DistPlusChart: React.FC<{ - distPlus: DistPlus; - width: number; - height: number; -}> = ({ distPlus, width, height }) => { - let shape = distPlus.pointSetDist; - if (shape.tag === "Continuous") { - let xyShape = shape.value.xyShape; - let totalY = xyShape.ys.reduce((a, b) => a + b); - let total = 0; - let cdf = xyShape.ys.map((y) => { - total += y; - return total / totalY; - }); - let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x, y]) => ({ - cdf: (c * 100).toFixed(2) + "%", - x: x, - y: y, - })); - - return ( - - ); - } else if (shape.tag === "Discrete") { - let xyShape = shape.value.xyShape; - let totalY = xyShape.ys.reduce((a, b) => a + b); - let total = 0; - let cdf = xyShape.ys.map((y) => { - total += y; - return total / totalY; - }); - let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x, y]) => ({ - cdf: (c * 100).toFixed(2) + "%", - x: x, - y: y, - })); - - return ; - } else if (shape.tag === "Mixed") { - let discreteShape = shape.value.discrete.xyShape; - let totalDiscrete = discreteShape.ys.reduce((a, b) => a + b); - - let discretePoints = _.zip(discreteShape.xs, discreteShape.ys); - let continuousShape = shape.value.continuous.xyShape; - let continuousPoints = _.zip(continuousShape.xs, continuousShape.ys); - - interface labeledPoint { - x: number; - y: number; - type: "discrete" | "continuous"; - } - - let markedDisPoints: labeledPoint[] = discretePoints.map(([x, y]) => ({ - x: x, - y: y, - type: "discrete", - })); - let markedConPoints: labeledPoint[] = continuousPoints.map(([x, y]) => ({ - x: x, - y: y, - type: "continuous", - })); - - let sortedPoints = _.sortBy(markedDisPoints.concat(markedConPoints), "x"); - - let totalContinuous = 1 - totalDiscrete; - let totalY = continuousShape.ys.reduce((a: number, b: number) => a + b); - - let total = 0; - let cdf = sortedPoints.map((point: labeledPoint) => { - if (point.type === "discrete") { - total += point.y; - return total; - } else if (point.type === "continuous") { - total += (point.y / totalY) * totalContinuous; - return total; - } - }); - - interface cdfLabeledPoint { - cdf: string; - x: number; - y: number; - type: "discrete" | "continuous"; - } - let cdfLabeledPoint: cdfLabeledPoint[] = _.zipWith( - cdf, - sortedPoints, - (c: number, point: labeledPoint) => ({ - ...point, - cdf: (c * 100).toFixed(2) + "%", - }) - ); - let continuousValues = cdfLabeledPoint.filter( - (x) => x.type === "continuous" - ); - let discreteValues = cdfLabeledPoint.filter((x) => x.type === "discrete"); - - return ( - - ); - } -}; - -const _rangeByCount = (start, stop, count) => { - const step = (stop - start) / (count - 1); - const items = _.range(start, stop, step); - const result = items.concat([stop]); - return result; -}; - -type distPlusFn = ( - a: number -) => { tag: "Ok"; value: DistPlus } | { tag: "Error"; value: string }; - -// This could really use a line in the location of the signal. I couldn't get it to work. -// https://vega.github.io/vega/docs/signals/#handlers - -export const FunctionChart: React.FC<{ - distPlusFn: distPlusFn; - diagramStart: number; - diagramStop: number; - diagramCount: number; -}> = ({ distPlusFn, diagramStart, diagramStop, diagramCount }) => { - let [mouseOverlay, setMouseOverlay] = React.useState(NaN); - function handleHover(...args) { - setMouseOverlay(args[1]); - } - function handleOut(...args) { - setMouseOverlay(NaN); - } - const signalListeners = { mousemove: handleHover, mouseout: handleOut }; - let percentileArray = [ - 0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, - ]; - let mouseItem = distPlusFn(mouseOverlay); - let showChart = - mouseItem.tag === "Ok" ? ( - - ) : ( - <> - ); - let data1 = _rangeByCount(diagramStart, diagramStop, diagramCount); - let valueData = data1 - .map((x) => { - let result = distPlusFn(x); - if (result.tag === "Ok") { - return { x: x, value: result.value }; - } else return null; - }) - .filter((x) => x !== null) - .map(({ x, value }) => { - let percentiles = getPercentiles(percentileArray, value); - return { - x: x, - p1: percentiles[0], - p5: percentiles[1], - p10: percentiles[2], - p20: percentiles[3], - p30: percentiles[4], - p40: percentiles[5], - p50: percentiles[6], - p60: percentiles[7], - p70: percentiles[8], - p80: percentiles[9], - p90: percentiles[10], - p95: percentiles[11], - p99: percentiles[12], - }; - }); - - let errorData = data1 - .map((x) => { - let result = distPlusFn(x); - if (result.tag === "Error") { - return { x: x, error: result.value }; - } else return null; - }) - .filter((x) => x !== null); - let error2 = _.groupBy(errorData, (x) => x.error); - return ( - <> - - {showChart} - {_.keysIn(error2).map((k) => ( - - {`Values: [${error2[k].map((r) => r.x.toFixed(2)).join(",")}]`} - - ))} - - ); -}; - -export const SquiggleChart: React.FC = ({ - squiggleString = "", - sampleCount = 1000, - outputXYPoints = 1000, - kernelWidth, - pointDistLength = 1000, - diagramStart = 0, - diagramStop = 10, - diagramCount = 20, - environment = [], - onEnvChange = () => {}, - width = 500, - height = 60, -}: SquiggleChartProps) => { - let samplingInputs: SamplingInputs = { - sampleCount: sampleCount, - outputXYPoints: outputXYPoints, - kernelWidth: kernelWidth, - pointDistLength: pointDistLength, - }; - - let result = run(squiggleString, samplingInputs, environment); - if (result.tag === "Ok") { - let environment = result.value.environment; - let exports = result.value.exports; - onEnvChange(environment); - let chartResults = exports.map((chartResult: exportDistribution) => { - if (chartResult["NAME"] === "Float") { - return ; - } else if (chartResult["NAME"] === "DistPlus") { - return ( - - ); - } else if (chartResult.NAME === "Function") { - return ( - - ); - } - }); - return <>{chartResults}; - } else if (result.tag === "Error") { - // At this point, we came across an error. What was our error? - return {result.value}; - } -}; - -function getPercentiles(percentiles: number[], t: DistPlus) { - if (t.pointSetDist.tag === "Discrete") { - let total = 0; - let maxX = _.max(t.pointSetDist.value.xyShape.xs); - let bounds = percentiles.map((_) => maxX); - _.zipWith( - t.pointSetDist.value.xyShape.xs, - t.pointSetDist.value.xyShape.ys, - (x, y) => { - total += y; - percentiles.forEach((v, i) => { - if (total > v && bounds[i] === maxX) { - bounds[i] = x; - } - }); - } - ); - return bounds; - } else if (t.pointSetDist.tag === "Continuous") { - let total = 0; - let maxX = _.max(t.pointSetDist.value.xyShape.xs); - let totalY = _.sum(t.pointSetDist.value.xyShape.ys); - let bounds = percentiles.map((_) => maxX); - _.zipWith( - t.pointSetDist.value.xyShape.xs, - t.pointSetDist.value.xyShape.ys, - (x, y) => { - total += y / totalY; - percentiles.forEach((v, i) => { - if (total > v && bounds[i] === maxX) { - bounds[i] = x; - } - }); - } - ); - return bounds; - } else if (t.pointSetDist.tag === "Mixed") { - let discreteShape = t.pointSetDist.value.discrete.xyShape; - let totalDiscrete = discreteShape.ys.reduce((a, b) => a + b); - - let discretePoints = _.zip(discreteShape.xs, discreteShape.ys); - let continuousShape = t.pointSetDist.value.continuous.xyShape; - let continuousPoints = _.zip(continuousShape.xs, continuousShape.ys); - - interface labeledPoint { - x: number; - y: number; - type: "discrete" | "continuous"; - } - - let markedDisPoints: labeledPoint[] = discretePoints.map(([x, y]) => ({ - x: x, - y: y, - type: "discrete", - })); - let markedConPoints: labeledPoint[] = continuousPoints.map(([x, y]) => ({ - x: x, - y: y, - type: "continuous", - })); - - let sortedPoints = _.sortBy(markedDisPoints.concat(markedConPoints), "x"); - - let totalContinuous = 1 - totalDiscrete; - let totalY = continuousShape.ys.reduce((a: number, b: number) => a + b); - - let total = 0; - let maxX = _.max(sortedPoints.map((x) => x.x)); - let bounds = percentiles.map((_) => maxX); - sortedPoints.map((point: labeledPoint) => { - if (point.type === "discrete") { - total += point.y; - } else if (point.type === "continuous") { - total += (point.y / totalY) * totalContinuous; - } - percentiles.forEach((v, i) => { - if (total > v && bounds[i] === maxX) { - bounds[i] = total; - } - }); - return total; - }); - return bounds; - } -} diff --git a/packages/components/src/CodeEditor.tsx b/packages/components/src/components/CodeEditor.tsx similarity index 100% rename from packages/components/src/CodeEditor.tsx rename to packages/components/src/components/CodeEditor.tsx diff --git a/packages/components/src/components/DistPlusChart.tsx b/packages/components/src/components/DistPlusChart.tsx new file mode 100644 index 00000000..02f2fabd --- /dev/null +++ b/packages/components/src/components/DistPlusChart.tsx @@ -0,0 +1,124 @@ +import * as React from "react"; +import _ from "lodash"; +import type { Spec } from "vega"; +import type { + DistPlus, +} from "@quri/squiggle-lang"; +import { createClassFromSpec } from "react-vega"; +import * as chartSpecification from "../vega-specs/spec-distributions.json"; + +let SquiggleVegaChart = createClassFromSpec({ + spec: chartSpecification as Spec, +}); + +export const DistPlusChart: React.FC<{ + distPlus: DistPlus; + width: number; + height: number; +}> = ({ distPlus, width, height }) => { + let shape = distPlus.pointSetDist; + if (shape.tag === "Continuous") { + let xyShape = shape.value.xyShape; + let totalY = xyShape.ys.reduce((a, b) => a + b); + let total = 0; + let cdf = xyShape.ys.map((y) => { + total += y; + return total / totalY; + }); + let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x, y]) => ({ + cdf: (c * 100).toFixed(2) + "%", + x: x, + y: y, + })); + + return ( + + ); + } else if (shape.tag === "Discrete") { + let xyShape = shape.value.xyShape; + let totalY = xyShape.ys.reduce((a, b) => a + b); + let total = 0; + let cdf = xyShape.ys.map((y) => { + total += y; + return total / totalY; + }); + let values = _.zip(cdf, xyShape.xs, xyShape.ys).map(([c, x, y]) => ({ + cdf: (c * 100).toFixed(2) + "%", + x: x, + y: y, + })); + + return ; + } else if (shape.tag === "Mixed") { + let discreteShape = shape.value.discrete.xyShape; + let totalDiscrete = discreteShape.ys.reduce((a, b) => a + b); + + let discretePoints = _.zip(discreteShape.xs, discreteShape.ys); + let continuousShape = shape.value.continuous.xyShape; + let continuousPoints = _.zip(continuousShape.xs, continuousShape.ys); + + interface labeledPoint { + x: number; + y: number; + type: "discrete" | "continuous"; + } + + let markedDisPoints: labeledPoint[] = discretePoints.map(([x, y]) => ({ + x: x, + y: y, + type: "discrete", + })); + let markedConPoints: labeledPoint[] = continuousPoints.map(([x, y]) => ({ + x: x, + y: y, + type: "continuous", + })); + + let sortedPoints = _.sortBy(markedDisPoints.concat(markedConPoints), "x"); + + let totalContinuous = 1 - totalDiscrete; + let totalY = continuousShape.ys.reduce((a: number, b: number) => a + b); + + let total = 0; + let cdf = sortedPoints.map((point: labeledPoint) => { + if (point.type === "discrete") { + total += point.y; + return total; + } else if (point.type === "continuous") { + total += (point.y / totalY) * totalContinuous; + return total; + } + }); + + interface cdfLabeledPoint { + cdf: string; + x: number; + y: number; + type: "discrete" | "continuous"; + } + let cdfLabeledPoint: cdfLabeledPoint[] = _.zipWith( + cdf, + sortedPoints, + (c: number, point: labeledPoint) => ({ + ...point, + cdf: (c * 100).toFixed(2) + "%", + }) + ); + let continuousValues = cdfLabeledPoint.filter( + (x) => x.type === "continuous" + ); + let discreteValues = cdfLabeledPoint.filter((x) => x.type === "discrete"); + + return ( + + ); + } +}; \ No newline at end of file diff --git a/packages/components/src/components/Error.tsx b/packages/components/src/components/Error.tsx new file mode 100644 index 00000000..4f5236ae --- /dev/null +++ b/packages/components/src/components/Error.tsx @@ -0,0 +1,20 @@ +import * as React from "react"; +import styled from "styled-components"; + +const ShowError = styled.div` + border: 1px solid #792e2e; + background: #eee2e2; + padding: 0.4em 0.8em; +`; + +export const Error: React.FC<{ heading: string; children: React.ReactNode }> = ({ + heading = "Error", + children, +}) => { + return ( + +

{heading}

+ {children} +
+ ); +}; diff --git a/packages/components/src/components/FunctionChart.tsx b/packages/components/src/components/FunctionChart.tsx new file mode 100644 index 00000000..7625b2da --- /dev/null +++ b/packages/components/src/components/FunctionChart.tsx @@ -0,0 +1,188 @@ +import * as React from "react"; +import _ from "lodash"; +import type { Spec } from "vega"; +import type { DistPlus } from "@quri/squiggle-lang"; +import { createClassFromSpec } from "react-vega"; +import * as percentilesSpec from "../vega-specs/spec-percentiles.json"; +import { DistPlusChart } from "./DistPlusChart"; +import { Error } from "./Error"; + +let SquigglePercentilesChart = createClassFromSpec({ + spec: percentilesSpec as Spec, +}); + +type distPlusFn = ( + a: number +) => { tag: "Ok"; value: DistPlus } | { tag: "Error"; value: string }; + +const _rangeByCount = (start, stop, count) => { + const step = (stop - start) / (count - 1); + const items = _.range(start, stop, step); + const result = items.concat([stop]); + return result; +}; + +export const FunctionChart: React.FC<{ + distPlusFn: distPlusFn; + diagramStart: number; + diagramStop: number; + diagramCount: number; +}> = ({ distPlusFn, diagramStart, diagramStop, diagramCount }) => { + let [mouseOverlay, setMouseOverlay] = React.useState(NaN); + function handleHover(...args) { + setMouseOverlay(args[1]); + } + function handleOut(...args) { + setMouseOverlay(NaN); + } + const signalListeners = { mousemove: handleHover, mouseout: handleOut }; + let percentileArray = [ + 0.01, 0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95, 0.99, + ]; + let mouseItem = distPlusFn(mouseOverlay); + let showChart = + mouseItem.tag === "Ok" ? ( + + ) : ( + <> + ); + let data1 = _rangeByCount(diagramStart, diagramStop, diagramCount); + let valueData = data1 + .map((x) => { + let result = distPlusFn(x); + if (result.tag === "Ok") { + return { x: x, value: result.value }; + } else return null; + }) + .filter((x) => x !== null) + .map(({ x, value }) => { + let percentiles = getPercentiles(percentileArray, value); + return { + x: x, + p1: percentiles[0], + p5: percentiles[1], + p10: percentiles[2], + p20: percentiles[3], + p30: percentiles[4], + p40: percentiles[5], + p50: percentiles[6], + p60: percentiles[7], + p70: percentiles[8], + p80: percentiles[9], + p90: percentiles[10], + p95: percentiles[11], + p99: percentiles[12], + }; + }); + + let errorData = data1 + .map((x) => { + let result = distPlusFn(x); + if (result.tag === "Error") { + return { x: x, error: result.value }; + } else return null; + }) + .filter((x) => x !== null); + let error2 = _.groupBy(errorData, (x) => x.error); + return ( + <> + + {showChart} + {_.keysIn(error2).map((k) => ( + + {`Values: [${error2[k].map((r) => r.x.toFixed(2)).join(",")}]`} + + ))} + + ); +}; + +function getPercentiles(percentiles: number[], t: DistPlus) { + if (t.pointSetDist.tag === "Discrete") { + let total = 0; + let maxX = _.max(t.pointSetDist.value.xyShape.xs); + let bounds = percentiles.map((_) => maxX); + _.zipWith( + t.pointSetDist.value.xyShape.xs, + t.pointSetDist.value.xyShape.ys, + (x, y) => { + total += y; + percentiles.forEach((v, i) => { + if (total > v && bounds[i] === maxX) { + bounds[i] = x; + } + }); + } + ); + return bounds; + } else if (t.pointSetDist.tag === "Continuous") { + let total = 0; + let maxX = _.max(t.pointSetDist.value.xyShape.xs); + let totalY = _.sum(t.pointSetDist.value.xyShape.ys); + let bounds = percentiles.map((_) => maxX); + _.zipWith( + t.pointSetDist.value.xyShape.xs, + t.pointSetDist.value.xyShape.ys, + (x, y) => { + total += y / totalY; + percentiles.forEach((v, i) => { + if (total > v && bounds[i] === maxX) { + bounds[i] = x; + } + }); + } + ); + return bounds; + } else if (t.pointSetDist.tag === "Mixed") { + let discreteShape = t.pointSetDist.value.discrete.xyShape; + let totalDiscrete = discreteShape.ys.reduce((a, b) => a + b); + + let discretePoints = _.zip(discreteShape.xs, discreteShape.ys); + let continuousShape = t.pointSetDist.value.continuous.xyShape; + let continuousPoints = _.zip(continuousShape.xs, continuousShape.ys); + + interface labeledPoint { + x: number; + y: number; + type: "discrete" | "continuous"; + } + + let markedDisPoints: labeledPoint[] = discretePoints.map(([x, y]) => ({ + x: x, + y: y, + type: "discrete", + })); + let markedConPoints: labeledPoint[] = continuousPoints.map(([x, y]) => ({ + x: x, + y: y, + type: "continuous", + })); + + let sortedPoints = _.sortBy(markedDisPoints.concat(markedConPoints), "x"); + + let totalContinuous = 1 - totalDiscrete; + let totalY = continuousShape.ys.reduce((a: number, b: number) => a + b); + + let total = 0; + let maxX = _.max(sortedPoints.map((x) => x.x)); + let bounds = percentiles.map((_) => maxX); + sortedPoints.map((point: labeledPoint) => { + if (point.type === "discrete") { + total += point.y; + } else if (point.type === "continuous") { + total += (point.y / totalY) * totalContinuous; + } + percentiles.forEach((v, i) => { + if (total > v && bounds[i] === maxX) { + bounds[i] = total; + } + }); + return total; + }); + return bounds; + } +} diff --git a/packages/components/src/NumberShower.tsx b/packages/components/src/components/NumberShower.tsx similarity index 100% rename from packages/components/src/NumberShower.tsx rename to packages/components/src/components/NumberShower.tsx diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx new file mode 100644 index 00000000..f5dc5667 --- /dev/null +++ b/packages/components/src/components/SquiggleChart.tsx @@ -0,0 +1,91 @@ +import * as React from "react"; +import _ from "lodash"; +import { run } from "@quri/squiggle-lang"; +import type { + SamplingInputs, + exportEnv, + exportDistribution, +} from "@quri/squiggle-lang"; +import { NumberShower } from "./NumberShower"; +import { DistPlusChart } from "./DistPlusChart"; +import { FunctionChart } from "./FunctionChart"; +import { Error } from "./Error"; + +export interface SquiggleChartProps { + /** The input string for squiggle */ + squiggleString?: string; + /** 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; + /** variables declared before this expression */ + environment?: exportEnv; + /** When the environment changes */ + onEnvChange?(env: exportEnv): void; + /** CSS width of the element */ + width?: number; + height?: number; +} + +export const SquiggleChart: React.FC = ({ + squiggleString = "", + sampleCount = 1000, + outputXYPoints = 1000, + kernelWidth, + pointDistLength = 1000, + diagramStart = 0, + diagramStop = 10, + diagramCount = 20, + environment = [], + onEnvChange = () => {}, + width = 500, + height = 60, +}: SquiggleChartProps) => { + let samplingInputs: SamplingInputs = { + sampleCount: sampleCount, + outputXYPoints: outputXYPoints, + kernelWidth: kernelWidth, + pointDistLength: pointDistLength, + }; + + let result = run(squiggleString, samplingInputs, environment); + if (result.tag === "Ok") { + let environment = result.value.environment; + let exports = result.value.exports; + onEnvChange(environment); + let chartResults = exports.map((chartResult: exportDistribution) => { + if (chartResult["NAME"] === "Float") { + return ; + } else if (chartResult["NAME"] === "DistPlus") { + return ( + + ); + } else if (chartResult.NAME === "Function") { + return ( + + ); + } + }); + return <>{chartResults}; + } else if (result.tag === "Error") { + // At this point, we came across an error. What was our error? + return {result.value}; + } +}; diff --git a/packages/components/src/SquiggleEditor.tsx b/packages/components/src/components/SquiggleEditor.tsx similarity index 100% rename from packages/components/src/SquiggleEditor.tsx rename to packages/components/src/components/SquiggleEditor.tsx diff --git a/packages/components/src/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx similarity index 100% rename from packages/components/src/SquigglePlayground.tsx rename to packages/components/src/components/SquigglePlayground.tsx diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index 48aa2b16..c16e5ac7 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -1,6 +1,6 @@ -export { SquiggleChart } from "./SquiggleChart"; -export { SquiggleEditor, renderSquiggleEditorToDom } from "./SquiggleEditor"; +export { SquiggleChart } from "./components/SquiggleChart"; +export { SquiggleEditor, renderSquiggleEditorToDom } from "./components/SquiggleEditor"; import SquigglePlayground, { renderSquigglePlaygroundToDom, -} from "./SquigglePlayground"; +} from "./components/SquigglePlayground"; export { SquigglePlayground, renderSquigglePlaygroundToDom }; diff --git a/packages/components/src/stories/NumberShower.stories.mdx b/packages/components/src/stories/NumberShower.stories.mdx index 5f040be5..fd9f39d1 100644 --- a/packages/components/src/stories/NumberShower.stories.mdx +++ b/packages/components/src/stories/NumberShower.stories.mdx @@ -1,4 +1,4 @@ -import { NumberShower } from "../NumberShower"; +import { NumberShower } from "../components/NumberShower"; import { Canvas, Meta, Story, Props } from "@storybook/addon-docs"; diff --git a/packages/components/src/stories/SquiggleChart.stories.mdx b/packages/components/src/stories/SquiggleChart.stories.mdx index 94273232..9c4799e1 100644 --- a/packages/components/src/stories/SquiggleChart.stories.mdx +++ b/packages/components/src/stories/SquiggleChart.stories.mdx @@ -1,4 +1,4 @@ -import { SquiggleChart } from "../SquiggleChart"; +import { SquiggleChart } from "../components/SquiggleChart"; import { Canvas, Meta, Story, Props } from "@storybook/addon-docs"; diff --git a/packages/components/src/stories/SquiggleEditor.stories.mdx b/packages/components/src/stories/SquiggleEditor.stories.mdx index 9c86d4b6..3ae37bb3 100644 --- a/packages/components/src/stories/SquiggleEditor.stories.mdx +++ b/packages/components/src/stories/SquiggleEditor.stories.mdx @@ -1,4 +1,4 @@ -import { SquiggleEditor } from "../SquiggleEditor"; +import { SquiggleEditor } from "../components/SquiggleEditor"; import { Canvas, Meta, Story, Props } from "@storybook/addon-docs"; diff --git a/packages/components/src/stories/SquigglePlayground.stories.mdx b/packages/components/src/stories/SquigglePlayground.stories.mdx index 2964dadc..5aa20ba8 100644 --- a/packages/components/src/stories/SquigglePlayground.stories.mdx +++ b/packages/components/src/stories/SquigglePlayground.stories.mdx @@ -1,4 +1,4 @@ -import SquigglePlayground from "../SquigglePlayground"; +import SquigglePlayground from "../components/SquigglePlayground"; import { Canvas, Meta, Story, Props } from "@storybook/addon-docs"; diff --git a/packages/components/src/spec-distributions.json b/packages/components/src/vega-specs/spec-distributions.json similarity index 100% rename from packages/components/src/spec-distributions.json rename to packages/components/src/vega-specs/spec-distributions.json diff --git a/packages/components/src/spec-percentiles.json b/packages/components/src/vega-specs/spec-percentiles.json similarity index 100% rename from packages/components/src/spec-percentiles.json rename to packages/components/src/vega-specs/spec-percentiles.json diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index 571a0de7..5152fc9b 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -16,7 +16,7 @@ "declaration": true, "sourceMap": true }, - "files": ["src/spec-distributions.json", "src/spec-percentiles.json"], + "files": ["src/vega-specs/spec-distributions.json", "src/vega-specs/spec-percentiles.json"], "target": "ES6", "include": ["src/**/*", "src/*"], "exclude": ["node_modules", "**/*.spec.ts", "webpack.config.js"], From 005d617dcf5dab504141c222a0a335215248fb04 Mon Sep 17 00:00:00 2001 From: Ozzie Gooen Date: Fri, 8 Apr 2022 07:47:56 -0400 Subject: [PATCH 5/5] Changed size of discrete component points --- packages/components/src/vega-specs/spec-distributions.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/components/src/vega-specs/spec-distributions.json b/packages/components/src/vega-specs/spec-distributions.json index afd62dcb..c8f340c3 100644 --- a/packages/components/src/vega-specs/spec-distributions.json +++ b/packages/components/src/vega-specs/spec-distributions.json @@ -160,9 +160,7 @@ "shape": { "value": "circle" }, - "width": { - "value": 5 - }, + "size": [{"value": 30}], "tooltip": { "signal": "datum.y" }