diff --git a/packages/components/src/stories/SquiggleChart.tsx b/packages/components/src/stories/SquiggleChart.tsx index 471cc2d4..6f21f80d 100644 --- a/packages/components/src/stories/SquiggleChart.tsx +++ b/packages/components/src/stories/SquiggleChart.tsx @@ -2,138 +2,16 @@ import * as React from 'react'; import * as PropTypes from 'prop-types'; import * as _ from 'lodash'; import './button.css'; -import type { PowScale, Spec } from 'vega'; +import type { Spec } from 'vega'; import { run } from '@squiggle/squiggle-lang'; +import type { DistPlus } from '@squiggle/squiggle-lang'; import { createClassFromSpec } from 'react-vega'; +import * as chartSpecification from './spec-distributions.json' +import * as percentilesSpec from './spec-pertentiles.json' -let scales : PowScale[] = [{ - "name": "xscale", - "type": "pow", - "exponent": {"signal": "xscale"}, - "range": "width", - "zero": false, - "nice": false, - "domain": { - "fields": [ - { "data": "con", "field": "x"}, - { "data": "dis", "field": "x"} - ] - } - }, { - "name": "yscale", - "type": "pow", - "exponent": {"signal": "yscale"}, - "range": "height", - "nice": true, - "zero": true, - "domain": { - "fields": [ - { "data": "con", "field": "y"}, - { "data": "dis", "field": "y"} - ] - } - } -] +let SquiggleVegaChart = createClassFromSpec({'spec': chartSpecification as Spec}); - -let specification : Spec = { - "$schema": "https://vega.github.io/schema/vega/v5.json", - "description": "A basic area chart example.", - "width": 500, - "height": 200, - "padding": 5, - "data": [{"name": "con"}, {"name": "dis"}], - - "signals": [ - { - "name": "mousex", - "description": "x position of mouse", - "update": "0", - "on": [{"events": "mousemove", "update": "1-x()/width"}] - }, - { - "name": "xscale", - "description": "The transform of the x scale", - "value": 1.0, - "bind": { - "input": "range", - "min": 0.1, - "max": 1 - } - }, - { - "name": "yscale", - "description": "The transform of the y scale", - "value": 1.0, - "bind": { - "input": "range", - "min": 0.1, - "max": 1 - } - } - ], - - "scales": scales, - - "axes": [ - {"orient": "bottom", "scale": "xscale", "tickCount": 20}, - {"orient": "left", "scale": "yscale"} - ], - - "marks": [ - { - "type": "area", - "from": {"data": "con"}, - "encode": { - "enter": { - "tooltip": {"signal": "datum.cdf"} - }, - "update": { - "x": {"scale": "xscale", "field": "x"}, - "y": {"scale": "yscale", "field": "y"}, - "y2": {"scale": "yscale", "value": 0}, - "fill": { - "signal": "{gradient: 'linear', x1: 1, y1: 1, x2: 0, y2: 1, stops: [ {offset: 0.0, color: 'steelblue'}, {offset: clamp(mousex, 0, 1), color: 'steelblue'}, {offset: clamp(mousex, 0, 1), color: 'blue'}, {offset: 1.0, color: 'blue'} ] }" - }, - "interpolate": {"value": "monotone"}, - "fillOpacity": {"value": 1} - } - } - }, - { - "type": "rect", - "from": {"data": "dis"}, - "encode": { - "enter": { - "y2": {"scale": "yscale", "value": 0}, - "width": {"value": 1} - }, - "update": { - "x": {"scale": "xscale", "field": "x"}, - "y": {"scale": "yscale", "field": "y"}, - - } - } - }, - { - "type": "symbol", - "from": {"data": "dis"}, - "encode": { - "enter": { - "shape": {"value": "circle"}, - "width": {"value": 5}, - "tooltip": {"signal": "datum.y"}, - }, - "update": { - "x": {"scale": "xscale", "field": "x"}, - "y": {"scale": "yscale", "field": "y"}, - } - } - } - ] -}; - -let SquiggleVegaChart = createClassFromSpec({'spec': specification}); +let SquigglePercentilesChart = createClassFromSpec({'spec': percentilesSpec as Spec}); /** * Primary UI component for user interaction @@ -235,8 +113,58 @@ export const SquiggleChart = ({ squiggleString }: { squiggleString: string}) => } } else if(chartResult.NAME === "Function"){ + console.log("Function time") + // We are looking at a function. In this case, we draw a Percentiles chart + let data = _.range(0,10,0.1).map((_,i) => { + let x = i /10; + console.log(chartResult); + console.log("Run thing") + if(chartResult.NAME=="Function"){ + let result = chartResult.VAL(x); + console.log(result); + 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 0; + }) + console.log(data); + return + } } else if(result.tag == "Error") { // At this point, we came across an error. What was our error? @@ -246,6 +174,79 @@ export const SquiggleChart = ({ squiggleString }: { squiggleString: string}) => return (

{"Invalid Response"}

) }; +function getPercentiles(percentiles:number[], t : DistPlus) { + if(t.shape.tag == "Discrete") { + let total = 0; + let maxX = _.max(t.shape.value.xyShape.xs) + let bounds = percentiles.map(_ => maxX); + _.zipWith(t.shape.value.xyShape.xs,t.shape.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.shape.tag == "Continuous"){ + let total = 0; + let maxX = _.max(t.shape.value.xyShape.xs) + let totalY = _.sum(t.shape.value.xyShape.ys) + let bounds = percentiles.map(_ => maxX); + _.zipWith(t.shape.value.xyShape.xs,t.shape.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.shape.tag == "Mixed"){ + let discreteShape = t.shape.value.discrete.xyShape; + let totalDiscrete = discreteShape.ys.reduce((a, b) => a + b); + + let discretePoints = _.zip(discreteShape.xs, discreteShape.ys); + let continuousShape = t.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 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; + } +} + SquiggleChart.propTypes = { /** * Squiggle String diff --git a/packages/components/src/stories/spec-distributions.json b/packages/components/src/stories/spec-distributions.json new file mode 100644 index 00000000..361b0377 --- /dev/null +++ b/packages/components/src/stories/spec-distributions.json @@ -0,0 +1,122 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "description": "A basic area chart example.", + "width": 500, + "height": 200, + "padding": 5, + "data": [{"name": "con"}, {"name": "dis"}], + + "signals": [ + { + "name": "mousex", + "description": "x position of mouse", + "update": "0", + "on": [{"events": "mousemove", "update": "1-x()/width"}] + }, + { + "name": "xscale", + "description": "The transform of the x scale", + "value": 1.0, + "bind": { + "input": "range", + "min": 0.1, + "max": 1 + } + }, + { + "name": "yscale", + "description": "The transform of the y scale", + "value": 1.0, + "bind": { + "input": "range", + "min": 0.1, + "max": 1 + } + } + ], + + "scales": [{ + "name": "xscale", + "type": "pow", + "exponent": {"signal": "xscale"}, + "range": "width", + "zero": false, + "nice": false, + "domain": { + "fields": [ + { "data": "con", "field": "x"}, + { "data": "dis", "field": "x"} + ] + } + }, { + "name": "yscale", + "type": "pow", + "exponent": {"signal": "yscale"}, + "range": "height", + "nice": true, + "zero": true, + "domain": { + "fields": [ + { "data": "con", "field": "y"}, + { "data": "dis", "field": "y"} + ] + } + } + ], + + "axes": [ + {"orient": "bottom", "scale": "xscale", "tickCount": 20}, + {"orient": "left", "scale": "yscale"} + ], + + "marks": [ + { + "type": "area", + "from": {"data": "con"}, + "encode": { + "enter": { + "tooltip": {"signal": "datum.cdf"} + }, + "update": { + "x": {"scale": "xscale", "field": "x"}, + "y": {"scale": "yscale", "field": "y"}, + "y2": {"scale": "yscale", "value": 0}, + "fill": { + "signal": "{gradient: 'linear', x1: 1, y1: 1, x2: 0, y2: 1, stops: [ {offset: 0.0, color: 'steelblue'}, {offset: clamp(mousex, 0, 1), color: 'steelblue'}, {offset: clamp(mousex, 0, 1), color: 'blue'}, {offset: 1.0, color: 'blue'} ] }" + }, + "interpolate": {"value": "monotone"}, + "fillOpacity": {"value": 1} + } + } + }, + { + "type": "rect", + "from": {"data": "dis"}, + "encode": { + "enter": { + "y2": {"scale": "yscale", "value": 0}, + "width": {"value": 1} + }, + "update": { + "x": {"scale": "xscale", "field": "x"}, + "y": {"scale": "yscale", "field": "y"} + } + } + }, + { + "type": "symbol", + "from": {"data": "dis"}, + "encode": { + "enter": { + "shape": {"value": "circle"}, + "width": {"value": 5}, + "tooltip": {"signal": "datum.y"} + }, + "update": { + "x": {"scale": "xscale", "field": "x"}, + "y": {"scale": "yscale", "field": "y"} + } + } + } + ] +} diff --git a/packages/components/src/stories/spec-pertentiles.json b/packages/components/src/stories/spec-pertentiles.json new file mode 100644 index 00000000..c3b0a21f --- /dev/null +++ b/packages/components/src/stories/spec-pertentiles.json @@ -0,0 +1,208 @@ +{ + "$schema": "https://vega.github.io/schema/vega/v5.json", + "width": 500, + "height": 400, + "padding": 5, + "data": [ + { + "name": "facet", + "values": [], + "format": { "type": "json", "parse": { "timestamp": "date" } } + }, + { + "name": "table", + "source": "facet", + "transform": [ + { + "type": "aggregate", + "groupby": ["x"], + "ops": [ + "mean", + "mean", + "mean", + "mean", + "mean", + "mean", + "mean", + "mean", + "mean", + "mean", + "mean", + "mean", + "mean" + ], + "fields": [ + "p1", + "p5", + "p10", + "p20", + "p30", + "p40", + "p50", + "p60", + "p70", + "p80", + "p90", + "p95", + "p99" + ], + "as": [ + "p1", + "p5", + "p10", + "p20", + "p30", + "p40", + "p50", + "p60", + "p70", + "p80", + "p90", + "p95", + "p99" + ] + } + ] + } + ], + "scales": [ + { + "name": "xscale", + "type": "linear", + "nice": true, + "domain": { "data": "facet", "field": "x" }, + "range": "width" + }, + { + "name": "yscale", + "type": "linear", + "range": "height", + "nice": true, + "zero": true, + "domain": { "data": "facet", "field": "p99" } + } + ], + "axes": [ + { + "orient": "bottom", + "scale": "xscale", + "grid": false, + "tickSize": 2, + "encode": { + "grid": { "enter": { "stroke": { "value": "#ccc" } } }, + "ticks": { "enter": { "stroke": { "value": "#ccc" } } } + } + }, + { + "orient": "left", + "scale": "yscale", + "grid": false, + "domain": false, + "tickSize": 2, + "encode": { + "grid": { "enter": { "stroke": { "value": "#ccc" } } }, + "ticks": { "enter": { "stroke": { "value": "#ccc" } } } + } + } + ], + "marks": [ + { + "type": "area", + "from": { "data": "table" }, + "encode": { + "enter": { "fill": { "value": "#4C78A8" } }, + "update": { + "interpolate": { "value": "monotone" }, + "x": { "scale": "xscale", "field": "x" }, + "y": { "scale": "yscale", "field": "p1" }, + "y2": { "scale": "yscale", "field": "p99" }, + "opacity": { "value": 0.05 } + } + } + }, + { + "type": "area", + "from": { "data": "table" }, + "encode": { + "enter": { "fill": { "value": "#4C78A8" } }, + "update": { + "interpolate": { "value": "monotone" }, + "x": { "scale": "xscale", "field": "x" }, + "y": { "scale": "yscale", "field": "p5" }, + "y2": { "scale": "yscale", "field": "p95" }, + "opacity": { "value": 0.1 } + } + } + }, + { + "type": "area", + "from": { "data": "table" }, + "encode": { + "enter": { "fill": { "value": "#4C78A8" } }, + "update": { + "interpolate": { "value": "monotone" }, + "x": { "scale": "xscale", "field": "x" }, + "y": { "scale": "yscale", "field": "p10" }, + "y2": { "scale": "yscale", "field": "p90" }, + "opacity": { "value": 0.15 } + } + } + }, + { + "type": "area", + "from": { "data": "table" }, + "encode": { + "enter": { "fill": { "value": "#4C78A8" } }, + "update": { + "interpolate": { "value": "monotone" }, + "x": { "scale": "xscale", "field": "x" }, + "y": { "scale": "yscale", "field": "p20" }, + "y2": { "scale": "yscale", "field": "p80" }, + "opacity": { "value": 0.2 } + } + } + }, + { + "type": "area", + "from": { "data": "table" }, + "encode": { + "enter": { "fill": { "value": "#4C78A8" } }, + "update": { + "interpolate": { "value": "monotone" }, + "x": { "scale": "xscale", "field": "x" }, + "y": { "scale": "yscale", "field": "p30" }, + "y2": { "scale": "yscale", "field": "p70" }, + "opacity": { "value": 0.2 } + } + } + }, + { + "type": "area", + "from": { "data": "table" }, + "encode": { + "enter": { "fill": { "value": "#4C78A8" } }, + "update": { + "interpolate": { "value": "monotone" }, + "x": { "scale": "xscale", "field": "x" }, + "y": { "scale": "yscale", "field": "p40" }, + "y2": { "scale": "yscale", "field": "p60" }, + "opacity": { "value": 0.2 } + } + } + }, + { + "type": "line", + "from": { "data": "table" }, + "encode": { + "update": { + "interpolate": { "value": "monotone" }, + "stroke": { "value": "#4C78A8" }, + "strokeWidth": { "value": 2 }, + "opacity": { "value": 0.8 }, + "x": { "scale": "xscale", "field": "x" }, + "y": { "scale": "yscale", "field": "p50" } + } + } + } + ] +} diff --git a/packages/components/tsconfig.json b/packages/components/tsconfig.json index 5beed619..d4c8ee3d 100644 --- a/packages/components/tsconfig.json +++ b/packages/components/tsconfig.json @@ -5,6 +5,7 @@ "noImplicitAny": false, "removeComments": true, "preserveConstEnums": true, + "resolveJsonModule": true, "sourceMap": true }, "include": ["src/**/*"],