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({})}