From 9cbeee04515c7f290a80ff39981c51d4ffbfef59 Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Tue, 12 Jul 2022 17:09:24 +1000 Subject: [PATCH 01/14] Add multiple plotting --- .../src/components/DistributionChart.tsx | 76 +++++++++++++++--- .../src/components/SquiggleItem.tsx | 17 +++- .../src/lib/distributionSpecBuilder.ts | 79 +++++++++++++------ 3 files changed, 135 insertions(+), 37 deletions(-) diff --git a/packages/components/src/components/DistributionChart.tsx b/packages/components/src/components/DistributionChart.tsx index af644d29..0b9535b2 100644 --- a/packages/components/src/components/DistributionChart.tsx +++ b/packages/components/src/components/DistributionChart.tsx @@ -4,6 +4,7 @@ import { result, distributionError, distributionErrorToString, + squiggleExpression, } from "@quri/squiggle-lang"; import { Vega } from "react-vega"; import { ErrorAlert } from "./Alert"; @@ -23,16 +24,58 @@ export type DistributionPlottingSettings = { showControls: boolean; } & DistributionChartSpecOptions; +export type Plot = { + distributions: Distribution[]; +}; + export type DistributionChartProps = { - distribution: Distribution; + plot: Plot; width?: number; height: number; actions?: boolean; } & DistributionPlottingSettings; +export function defaultPlot(distribution: Distribution): Plot { + return { distributions: [distribution] }; +} +export function makePlot(expression: { + [key: string]: squiggleExpression; +}): Plot | void { + if (expression["distributions"].tag === "array") { + let distributions: Distribution[] = expression["distributions"].value + .map((x) => { + if (x.tag === "distribution") { + return x.value; + } + }) + .filter((x): x is Distribution => x !== undefined); + return { distributions }; + } +} +function all(arr: boolean[]): boolean { + return arr.reduce((x, y) => x && y, true); +} + +function flattenResult(x: result[]): result { + if (x.length === 0) { + return { tag: "Ok", value: [] }; + } else { + if (x[0].tag === "Error") { + return x[0]; + } else { + let rest = flattenResult(x.splice(1)); + if (rest.tag === "Error") { + return rest; + } else { + return { tag: "Ok", value: [x[0].value].concat(rest.value) }; + } + } + } +} + export const DistributionChart: React.FC = (props) => { const { - distribution, + plot, height, showSummary, width, @@ -47,19 +90,23 @@ export const DistributionChart: React.FC = (props) => { React.useEffect(() => setLogX(logX), [logX]); React.useEffect(() => setExpY(expY), [expY]); - const shape = distribution.pointSet(); const [sized] = useSize((size) => { - if (shape.tag === "Error") { + let shapes = flattenResult(plot.distributions.map((x) => x.pointSet())); + if (shapes.tag === "Error") { return ( - {distributionErrorToString(shape.value)} + {distributionErrorToString(shapes.value)} ); } - const massBelow0 = - shape.value.continuous.some((x) => x.x <= 0) || - shape.value.discrete.some((x) => x.x <= 0); + const massBelow0 = all( + shapes.value.map( + (shape) => + shape.continuous.some((x) => x.x <= 0) || + shape.discrete.some((x) => x.x <= 0) + ) + ); const spec = buildVegaSpec(props); let widthProp = width ? width : size.width; @@ -69,13 +116,20 @@ export const DistributionChart: React.FC = (props) => { ); widthProp = 20; } + let continuousPoints = shapes.value.flatMap((shape, i) => + shape.continuous.map((point) => ({ ...point, name: i + 1 })) + ); + let discretePoints = shapes.value.flatMap((shape, i) => + shape.discrete.map((point) => ({ ...point, name: i + 1 })) + ); + console.log(continuousPoints); return (
{!(isLogX && massBelow0) ? ( = (props) => { )}
- {showSummary && } + {showSummary && plot.distributions.length == 1 && ( + + )}
{showControls && (
diff --git a/packages/components/src/components/SquiggleItem.tsx b/packages/components/src/components/SquiggleItem.tsx index 48a8a0fb..09b9fa27 100644 --- a/packages/components/src/components/SquiggleItem.tsx +++ b/packages/components/src/components/SquiggleItem.tsx @@ -8,6 +8,8 @@ import { NumberShower } from "./NumberShower"; import { DistributionChart, DistributionPlottingSettings, + makePlot, + defaultPlot, } from "./DistributionChart"; import { FunctionChart, FunctionChartSettings } from "./FunctionChart"; @@ -102,7 +104,7 @@ export const SquiggleItem: React.FC = ({
{expression.value.toString()}
) : null} = ({ ); case "record": + let plot = makePlot(expression.value); + if (plot) { + return ( + + ); + } return (
@@ -246,7 +259,7 @@ export const SquiggleItem: React.FC = ({
{Object.entries(expression.value) - .filter(([key, r]) => key !== "Math") + .filter(([key, _]) => key !== "Math") .map(([key, r]) => (
diff --git a/packages/components/src/lib/distributionSpecBuilder.ts b/packages/components/src/lib/distributionSpecBuilder.ts index 4286dbdb..515a1b9f 100644 --- a/packages/components/src/lib/distributionSpecBuilder.ts +++ b/packages/components/src/lib/distributionSpecBuilder.ts @@ -137,7 +137,21 @@ export function buildVegaSpec( }, ], signals: [], - scales: [xScale, expY ? expYScale : linearYScale], + scales: [ + xScale, + expY ? expYScale : linearYScale, + { + name: "color", + type: "ordinal", + domain: { + fields: [ + { data: "con", field: "name" }, + { data: "dis", field: "name" }, + ], + }, + range: { scheme: "category20b" }, + }, + ], axes: [ { orient: "bottom", @@ -153,33 +167,48 @@ export function buildVegaSpec( ], marks: [ { - type: "area", + name: "group", + type: "group", from: { - data: "con", - }, - encode: { - update: { - interpolate: { value: "linear" }, - x: { - scale: "xscale", - field: "x", - }, - y: { - scale: "yscale", - field: "y", - }, - y2: { - scale: "yscale", - value: 0, - }, - fill: { - value: color, - }, - fillOpacity: { - value: 1, - }, + facet: { + name: "faceted_path_main", + data: "con", + groupby: ["name"], }, }, + marks: [ + { + name: "distribution_charts", + type: "area", + from: { + data: "faceted_path_main", + }, + encode: { + update: { + interpolate: { value: "linear" }, + x: { + scale: "xscale", + field: "x", + }, + y: { + scale: "yscale", + field: "y", + }, + y2: { + scale: "yscale", + value: 0, + }, + fill: { + field: "name", + scale: "color", + }, + fillOpacity: { + value: 1, + }, + }, + }, + }, + ], }, { type: "rect", From 98ae0459c96765126e530918ab1e8494bc870fd1 Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Wed, 13 Jul 2022 14:15:07 +1000 Subject: [PATCH 02/14] Refactor specification to include discrete --- .../src/components/DistributionChart.tsx | 48 +++- .../src/components/FunctionChart1Dist.tsx | 3 +- .../src/lib/distributionSpecBuilder.ts | 267 ++++++++---------- 3 files changed, 157 insertions(+), 161 deletions(-) diff --git a/packages/components/src/components/DistributionChart.tsx b/packages/components/src/components/DistributionChart.tsx index 0b9535b2..f1253629 100644 --- a/packages/components/src/components/DistributionChart.tsx +++ b/packages/components/src/components/DistributionChart.tsx @@ -5,6 +5,7 @@ import { distributionError, distributionErrorToString, squiggleExpression, + resultMap, } from "@quri/squiggle-lang"; import { Vega } from "react-vega"; import { ErrorAlert } from "./Alert"; @@ -24,8 +25,10 @@ export type DistributionPlottingSettings = { showControls: boolean; } & DistributionChartSpecOptions; +export type LabeledDistribution = { name: string; distribution: Distribution }; + export type Plot = { - distributions: Distribution[]; + distributions: LabeledDistribution[]; }; export type DistributionChartProps = { @@ -36,19 +39,29 @@ export type DistributionChartProps = { } & DistributionPlottingSettings; export function defaultPlot(distribution: Distribution): Plot { - return { distributions: [distribution] }; + return { distributions: [{ name: "default", distribution }] }; } + export function makePlot(expression: { [key: string]: squiggleExpression; }): Plot | void { if (expression["distributions"].tag === "array") { - let distributions: Distribution[] = expression["distributions"].value + let distributions: LabeledDistribution[] = expression["distributions"].value .map((x) => { - if (x.tag === "distribution") { - return x.value; + if ( + x.tag === "record" && + x.value["name"] && + x.value["name"].tag === "string" && + x.value["distribution"] && + x.value["distribution"].tag === "distribution" + ) { + return { + name: x.value["name"].value, + distribution: x.value["distribution"].value, + }; } }) - .filter((x): x is Distribution => x !== undefined); + .filter((x): x is LabeledDistribution => x !== undefined); return { distributions }; } } @@ -91,7 +104,15 @@ export const DistributionChart: React.FC = (props) => { React.useEffect(() => setExpY(expY), [expY]); const [sized] = useSize((size) => { - let shapes = flattenResult(plot.distributions.map((x) => x.pointSet())); + let shapes = flattenResult( + plot.distributions.map((x) => + resultMap(x.distribution.pointSet(), (shape) => ({ + name: x.name, + continuous: shape.continuous, + discrete: shape.discrete, + })) + ) + ); if (shapes.tag === "Error") { return ( @@ -116,20 +137,17 @@ export const DistributionChart: React.FC = (props) => { ); widthProp = 20; } - let continuousPoints = shapes.value.flatMap((shape, i) => - shape.continuous.map((point) => ({ ...point, name: i + 1 })) - ); - let discretePoints = shapes.value.flatMap((shape, i) => - shape.discrete.map((point) => ({ ...point, name: i + 1 })) + const domain = shapes.value.flatMap((shape) => + shape.discrete.concat(shape.continuous) ); + console.log(shapes.value); - console.log(continuousPoints); return (
{!(isLogX && massBelow0) ? ( = (props) => { )}
{showSummary && plot.distributions.length == 1 && ( - + )}
{showControls && ( diff --git a/packages/components/src/components/FunctionChart1Dist.tsx b/packages/components/src/components/FunctionChart1Dist.tsx index 650d2753..3b203de5 100644 --- a/packages/components/src/components/FunctionChart1Dist.tsx +++ b/packages/components/src/components/FunctionChart1Dist.tsx @@ -16,6 +16,7 @@ import * as percentilesSpec from "../vega-specs/spec-percentiles.json"; import { DistributionChart, DistributionPlottingSettings, + defaultPlot, } from "./DistributionChart"; import { NumberShower } from "./NumberShower"; import { ErrorAlert } from "./Alert"; @@ -177,7 +178,7 @@ export const FunctionChart1Dist: React.FC = ({ let showChart = mouseItem.tag === "Ok" && mouseItem.value.tag === "distribution" ? ( Date: Wed, 13 Jul 2022 15:29:39 +1000 Subject: [PATCH 03/14] Make parser of distributions stricter --- .../src/components/DistributionChart.tsx | 116 +++++++++++++++--- 1 file changed, 97 insertions(+), 19 deletions(-) diff --git a/packages/components/src/components/DistributionChart.tsx b/packages/components/src/components/DistributionChart.tsx index f1253629..b8310609 100644 --- a/packages/components/src/components/DistributionChart.tsx +++ b/packages/components/src/components/DistributionChart.tsx @@ -42,27 +42,105 @@ export function defaultPlot(distribution: Distribution): Plot { return { distributions: [{ name: "default", distribution }] }; } -export function makePlot(expression: { +function error(err: b): result { + return { tag: "Error", value: err }; +} + +function ok(x: a): result { + return { tag: "Ok", value: x }; +} + +function parseString(expr: squiggleExpression): result { + if (expr.tag === "string") { + return ok(expr.value); + } else { + return error("Expression was not string"); + } +} + +function parseRecord( + expr: squiggleExpression +): result<{ [key: string]: squiggleExpression }, string> { + if (expr.tag === "record") { + return ok(expr.value); + } else { + return error("Expression was not a record"); + } +} + +function parseDistribution( + expr: squiggleExpression +): result { + if (expr.tag === "distribution") { + return ok(expr.value); + } else { + return error("Expression was not a distribution"); + } +} + +function parseArray( + expr: squiggleExpression +): result { + if (expr.tag === "array") { + return ok(expr.value); + } else { + return error("Expression was not a distribution"); + } +} + +function parseField( + record: { [key: string]: squiggleExpression }, + field: string, + parser: (expr: squiggleExpression) => result +): result { + if (record[field]) { + return parser(record[field]); + } else { + return error("record does not have field " + field); + } +} + +function resultBind( + x: result, + fn: (y: a) => result +): result { + if (x.tag === "Ok") { + return fn(x.value); + } else { + return x; + } +} + +function parseLabeledDistribution( + x: squiggleExpression +): result { + return resultBind(parseRecord(x), (record) => + resultBind(parseField(record, "name", parseString), (name) => + resultBind( + parseField(record, "distribution", parseDistribution), + (distribution) => ok({ name, distribution }) + ) + ) + ); +} + +function parsePlot(record: { + [key: string]: squiggleExpression; +}): result { + return resultBind(parseField(record, "distributions", parseArray), (array) => + resultBind( + flattenResult(array.map(parseLabeledDistribution)), + (distributions) => ok({ distributions }) + ) + ); +} + +export function makePlot(record: { [key: string]: squiggleExpression; }): Plot | void { - if (expression["distributions"].tag === "array") { - let distributions: LabeledDistribution[] = expression["distributions"].value - .map((x) => { - if ( - x.tag === "record" && - x.value["name"] && - x.value["name"].tag === "string" && - x.value["distribution"] && - x.value["distribution"].tag === "distribution" - ) { - return { - name: x.value["name"].value, - distribution: x.value["distribution"].value, - }; - } - }) - .filter((x): x is LabeledDistribution => x !== undefined); - return { distributions }; + const plotResult = parsePlot(record); + if (plotResult.tag == "Ok") { + return plotResult.value; } } function all(arr: boolean[]): boolean { From 0c7ac98aaf1e9055b434cc526ab87fd9a79441ce Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Wed, 13 Jul 2022 15:32:28 +1000 Subject: [PATCH 04/14] Color discrete components of distributions --- packages/components/src/lib/distributionSpecBuilder.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/components/src/lib/distributionSpecBuilder.ts b/packages/components/src/lib/distributionSpecBuilder.ts index 36f298d2..229e9ffe 100644 --- a/packages/components/src/lib/distributionSpecBuilder.ts +++ b/packages/components/src/lib/distributionSpecBuilder.ts @@ -213,6 +213,10 @@ export function buildVegaSpec( scale: "yscale", value: 0, }, + fill: { + scale: "color", + field: { parent: "name" }, + }, }, }, }, @@ -240,6 +244,10 @@ export function buildVegaSpec( scale: "yscale", field: "y", }, + fill: { + scale: "color", + field: { parent: "name" }, + }, }, }, }, From a5a131daf15766c50bde4df2802cdf05446a4893 Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Wed, 13 Jul 2022 15:33:38 +1000 Subject: [PATCH 05/14] Remove console.log --- packages/components/src/components/DistributionChart.tsx | 1 - packages/components/src/components/SquigglePlayground.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/components/src/components/DistributionChart.tsx b/packages/components/src/components/DistributionChart.tsx index b8310609..6d4e7c55 100644 --- a/packages/components/src/components/DistributionChart.tsx +++ b/packages/components/src/components/DistributionChart.tsx @@ -218,7 +218,6 @@ export const DistributionChart: React.FC = (props) => { const domain = shapes.value.flatMap((shape) => shape.discrete.concat(shape.continuous) ); - console.log(shapes.value); return (
diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 534be721..d24ba141 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -529,7 +529,6 @@ export const SquigglePlayground: FC = ({ const withoutEditor =
{tabs}
; - console.log(vars); return ( From 9cc000070b4544e97873e077f6bd5d2350c554eb Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Wed, 13 Jul 2022 15:54:45 +1000 Subject: [PATCH 06/14] Refactor parsing to lib files --- .../src/components/DistributionChart.tsx | 121 +----------------- packages/components/src/lib/plotting.ts | 90 +++++++++++++ packages/components/src/lib/utility.ts | 33 +++++ 3 files changed, 125 insertions(+), 119 deletions(-) create mode 100644 packages/components/src/lib/plotting.ts create mode 100644 packages/components/src/lib/utility.ts diff --git a/packages/components/src/components/DistributionChart.tsx b/packages/components/src/components/DistributionChart.tsx index 6d4e7c55..7556da26 100644 --- a/packages/components/src/components/DistributionChart.tsx +++ b/packages/components/src/components/DistributionChart.tsx @@ -17,6 +17,8 @@ import { DistributionChartSpecOptions, } from "../lib/distributionSpecBuilder"; import { NumberShower } from "./NumberShower"; +import { Plot, parsePlot } from "../lib/plotting"; +import { flattenResult, all } from "../lib/utility"; export type DistributionPlottingSettings = { /** Whether to show a summary of means, stdev, percentiles etc */ @@ -25,12 +27,6 @@ export type DistributionPlottingSettings = { showControls: boolean; } & DistributionChartSpecOptions; -export type LabeledDistribution = { name: string; distribution: Distribution }; - -export type Plot = { - distributions: LabeledDistribution[]; -}; - export type DistributionChartProps = { plot: Plot; width?: number; @@ -42,99 +38,6 @@ export function defaultPlot(distribution: Distribution): Plot { return { distributions: [{ name: "default", distribution }] }; } -function error(err: b): result { - return { tag: "Error", value: err }; -} - -function ok(x: a): result { - return { tag: "Ok", value: x }; -} - -function parseString(expr: squiggleExpression): result { - if (expr.tag === "string") { - return ok(expr.value); - } else { - return error("Expression was not string"); - } -} - -function parseRecord( - expr: squiggleExpression -): result<{ [key: string]: squiggleExpression }, string> { - if (expr.tag === "record") { - return ok(expr.value); - } else { - return error("Expression was not a record"); - } -} - -function parseDistribution( - expr: squiggleExpression -): result { - if (expr.tag === "distribution") { - return ok(expr.value); - } else { - return error("Expression was not a distribution"); - } -} - -function parseArray( - expr: squiggleExpression -): result { - if (expr.tag === "array") { - return ok(expr.value); - } else { - return error("Expression was not a distribution"); - } -} - -function parseField
( - record: { [key: string]: squiggleExpression }, - field: string, - parser: (expr: squiggleExpression) => result -): result { - if (record[field]) { - return parser(record[field]); - } else { - return error("record does not have field " + field); - } -} - -function resultBind( - x: result, - fn: (y: a) => result -): result { - if (x.tag === "Ok") { - return fn(x.value); - } else { - return x; - } -} - -function parseLabeledDistribution( - x: squiggleExpression -): result { - return resultBind(parseRecord(x), (record) => - resultBind(parseField(record, "name", parseString), (name) => - resultBind( - parseField(record, "distribution", parseDistribution), - (distribution) => ok({ name, distribution }) - ) - ) - ); -} - -function parsePlot(record: { - [key: string]: squiggleExpression; -}): result { - return resultBind(parseField(record, "distributions", parseArray), (array) => - resultBind( - flattenResult(array.map(parseLabeledDistribution)), - (distributions) => ok({ distributions }) - ) - ); -} - export function makePlot(record: { [key: string]: squiggleExpression; }): Plot | void { @@ -143,26 +46,6 @@ export function makePlot(record: { return plotResult.value; } } -function all(arr: boolean[]): boolean { - return arr.reduce((x, y) => x && y, true); -} - -function flattenResult(x: result[]): result { - if (x.length === 0) { - return { tag: "Ok", value: [] }; - } else { - if (x[0].tag === "Error") { - return x[0]; - } else { - let rest = flattenResult(x.splice(1)); - if (rest.tag === "Error") { - return rest; - } else { - return { tag: "Ok", value: [x[0].value].concat(rest.value) }; - } - } - } -} export const DistributionChart: React.FC = (props) => { const { diff --git a/packages/components/src/lib/plotting.ts b/packages/components/src/lib/plotting.ts new file mode 100644 index 00000000..5b7ca31d --- /dev/null +++ b/packages/components/src/lib/plotting.ts @@ -0,0 +1,90 @@ +import { Distribution, result, squiggleExpression } from "@quri/squiggle-lang"; +import { flattenResult, resultBind } from "./utility"; + +export type LabeledDistribution = { name: string; distribution: Distribution }; + +export type Plot = { + distributions: LabeledDistribution[]; +}; + +function error(err: b): result { + return { tag: "Error", value: err }; +} + +function ok(x: a): result { + return { tag: "Ok", value: x }; +} + +function parseString(expr: squiggleExpression): result { + if (expr.tag === "string") { + return ok(expr.value); + } else { + return error("Expression was not string"); + } +} + +function parseRecord( + expr: squiggleExpression +): result<{ [key: string]: squiggleExpression }, string> { + if (expr.tag === "record") { + return ok(expr.value); + } else { + return error("Expression was not a record"); + } +} + +function parseDistribution( + expr: squiggleExpression +): result { + if (expr.tag === "distribution") { + return ok(expr.value); + } else { + return error("Expression was not a distribution"); + } +} + +function parseArray( + expr: squiggleExpression +): result { + if (expr.tag === "array") { + return ok(expr.value); + } else { + return error("Expression was not a distribution"); + } +} + +function parseField( + record: { [key: string]: squiggleExpression }, + field: string, + parser: (expr: squiggleExpression) => result +): result { + if (record[field]) { + return parser(record[field]); + } else { + return error("record does not have field " + field); + } +} + +function parseLabeledDistribution( + x: squiggleExpression +): result { + return resultBind(parseRecord(x), (record) => + resultBind(parseField(record, "name", parseString), (name) => + resultBind( + parseField(record, "distribution", parseDistribution), + (distribution) => ok({ name, distribution }) + ) + ) + ); +} + +export function parsePlot(record: { + [key: string]: squiggleExpression; +}): result { + return resultBind(parseField(record, "distributions", parseArray), (array) => + resultBind( + flattenResult(array.map(parseLabeledDistribution)), + (distributions) => ok({ distributions }) + ) + ); +} diff --git a/packages/components/src/lib/utility.ts b/packages/components/src/lib/utility.ts new file mode 100644 index 00000000..4a5ecc6b --- /dev/null +++ b/packages/components/src/lib/utility.ts @@ -0,0 +1,33 @@ +import { result } from "@quri/squiggle-lang"; + +export function flattenResult(x: result[]): result { + if (x.length === 0) { + return { tag: "Ok", value: [] }; + } else { + if (x[0].tag === "Error") { + return x[0]; + } else { + let rest = flattenResult(x.splice(1)); + if (rest.tag === "Error") { + return rest; + } else { + return { tag: "Ok", value: [x[0].value].concat(rest.value) }; + } + } + } +} + +export function resultBind( + x: result, + fn: (y: a) => result +): result { + if (x.tag === "Ok") { + return fn(x.value); + } else { + return x; + } +} + +export function all(arr: boolean[]): boolean { + return arr.reduce((x, y) => x && y, true); +} From ad1d391f49d5ba34f11cfd41aadf06d746ea946c Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Thu, 14 Jul 2022 16:36:12 +1000 Subject: [PATCH 07/14] Rename plotting to plotParser --- packages/components/src/components/DistributionChart.tsx | 2 +- packages/components/src/lib/{plotting.ts => plotParser.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename packages/components/src/lib/{plotting.ts => plotParser.ts} (100%) diff --git a/packages/components/src/components/DistributionChart.tsx b/packages/components/src/components/DistributionChart.tsx index 7556da26..f3f84adf 100644 --- a/packages/components/src/components/DistributionChart.tsx +++ b/packages/components/src/components/DistributionChart.tsx @@ -17,7 +17,7 @@ import { DistributionChartSpecOptions, } from "../lib/distributionSpecBuilder"; import { NumberShower } from "./NumberShower"; -import { Plot, parsePlot } from "../lib/plotting"; +import { Plot, parsePlot } from "../lib/plotParser"; import { flattenResult, all } from "../lib/utility"; export type DistributionPlottingSettings = { diff --git a/packages/components/src/lib/plotting.ts b/packages/components/src/lib/plotParser.ts similarity index 100% rename from packages/components/src/lib/plotting.ts rename to packages/components/src/lib/plotParser.ts From 4aab78b45cd98dce56e5e38de7a48086869aaaa9 Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Thu, 11 Aug 2022 12:31:44 +0100 Subject: [PATCH 08/14] Format code --- packages/components/src/components/DistributionChart.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/components/src/components/DistributionChart.tsx b/packages/components/src/components/DistributionChart.tsx index 741e8f6a..50715229 100644 --- a/packages/components/src/components/DistributionChart.tsx +++ b/packages/components/src/components/DistributionChart.tsx @@ -46,14 +46,7 @@ export function makePlot(record: { } export const DistributionChart: React.FC = (props) => { - const { - plot, - height, - showSummary, - width, - logX, - actions = false, - } = props; + const { plot, height, showSummary, width, logX, actions = false } = props; const shape = distribution.pointSet(); const [sized] = useSize((size) => { let shapes = flattenResult( From c97d1d457ef69d49c1639b50af98036c8940962a Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Thu, 11 Aug 2022 14:22:55 +0100 Subject: [PATCH 09/14] Remove unneccesary line --- packages/components/src/components/DistributionChart.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/components/src/components/DistributionChart.tsx b/packages/components/src/components/DistributionChart.tsx index 50715229..12f52754 100644 --- a/packages/components/src/components/DistributionChart.tsx +++ b/packages/components/src/components/DistributionChart.tsx @@ -47,7 +47,6 @@ export function makePlot(record: { export const DistributionChart: React.FC = (props) => { const { plot, height, showSummary, width, logX, actions = false } = props; - const shape = distribution.pointSet(); const [sized] = useSize((size) => { let shapes = flattenResult( plot.distributions.map((x) => From 70f26a08ba66734aa9f23da6ca81e64ca792a46b Mon Sep 17 00:00:00 2001 From: Sam Nolan Date: Sat, 13 Aug 2022 10:52:56 +0100 Subject: [PATCH 10/14] Fix multiple charting --- .../src/components/DistributionChart.tsx | 2 +- .../SquiggleViewer/ExpressionViewer.tsx | 75 ++++++++++++++----- packages/components/src/lib/utility.ts | 4 + 3 files changed, 61 insertions(+), 20 deletions(-) diff --git a/packages/components/src/components/DistributionChart.tsx b/packages/components/src/components/DistributionChart.tsx index 12f52754..84bc5fef 100644 --- a/packages/components/src/components/DistributionChart.tsx +++ b/packages/components/src/components/DistributionChart.tsx @@ -80,7 +80,7 @@ export const DistributionChart: React.FC = (props) => { return (
- {logX && hasMassBelowZero(shape.value) ? ( + {logX && shapes.value.some(hasMassBelowZero) ? ( Cannot graph distribution with negative values on logarithmic scale. diff --git a/packages/components/src/components/SquiggleViewer/ExpressionViewer.tsx b/packages/components/src/components/SquiggleViewer/ExpressionViewer.tsx index 9a3e266e..51f8dcb4 100644 --- a/packages/components/src/components/SquiggleViewer/ExpressionViewer.tsx +++ b/packages/components/src/components/SquiggleViewer/ExpressionViewer.tsx @@ -1,7 +1,7 @@ import React from "react"; import { squiggleExpression, declaration } from "@quri/squiggle-lang"; import { NumberShower } from "../NumberShower"; -import { DistributionChart } from "../DistributionChart"; +import { DistributionChart, defaultPlot, makePlot } from "../DistributionChart"; import { FunctionChart, FunctionChartSettings } from "../FunctionChart"; import clsx from "clsx"; import { VariableBox } from "./VariableBox"; @@ -102,7 +102,7 @@ export const ExpressionViewer: React.FC = ({ {(settings) => { return ( = ({ case "module": { return ( - {(settings) => + {(_) => Object.entries(expression.value) - .filter(([key, r]) => !key.match(/^(Math|System)\./)) + .filter(([key, _]) => !key.match(/^(Math|System)\./)) .map(([key, r]) => ( = ({ ); } case "record": - return ( - - {(settings) => - Object.entries(expression.value).map(([key, r]) => ( - - )) - } - - ); + const plot = makePlot(expression.value); + if (plot) { + return ( + { + let disableLogX = plot.distributions.some((x) => { + let pointSet = x.distribution.pointSet(); + return ( + pointSet.tag === "Ok" && hasMassBelowZero(pointSet.value) + ); + }); + return ( + + ); + }} + > + {(settings) => { + return ( + + ); + }} + + ); + } else { + return ( + + {(_) => + Object.entries(expression.value).map(([key, r]) => ( + + )) + } + + ); + } case "array": return ( - {(settings) => + {(_) => expression.value.map((r, i) => ( ( export function all(arr: boolean[]): boolean { return arr.reduce((x, y) => x && y, true); } + +export function some(arr: boolean[]): boolean { + return arr.reduce((x, y) => x || y, false); +} From 61051ffe5fe0f3d9cf16303c9bfb261eaf2869b1 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 29 Jul 2022 18:10:57 +0400 Subject: [PATCH 11/14] multiple plots story --- .../src/stories/SquiggleChart.stories.mdx | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/components/src/stories/SquiggleChart.stories.mdx b/packages/components/src/stories/SquiggleChart.stories.mdx index bc289c36..14a3f1fe 100644 --- a/packages/components/src/stories/SquiggleChart.stories.mdx +++ b/packages/components/src/stories/SquiggleChart.stories.mdx @@ -93,6 +93,20 @@ could be continuous, discrete or mixed. +## Multiple plots + + + + {Template.bind({})} + + + ## Constants A constant is a simple number as a result. This has special formatting rules From 7866203ac49c5b07da356b1b27487ad313884688 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 19 Aug 2022 15:17:31 +0400 Subject: [PATCH 12/14] reimplement parsePlot with yup --- packages/components/src/lib/plotParser.ts | 109 ++++++++-------------- 1 file changed, 40 insertions(+), 69 deletions(-) diff --git a/packages/components/src/lib/plotParser.ts b/packages/components/src/lib/plotParser.ts index 5b7ca31d..033ac2a3 100644 --- a/packages/components/src/lib/plotParser.ts +++ b/packages/components/src/lib/plotParser.ts @@ -1,5 +1,5 @@ +import * as yup from "yup"; import { Distribution, result, squiggleExpression } from "@quri/squiggle-lang"; -import { flattenResult, resultBind } from "./utility"; export type LabeledDistribution = { name: string; distribution: Distribution }; @@ -15,76 +15,47 @@ function ok(x: a): result { return { tag: "Ok", value: x }; } -function parseString(expr: squiggleExpression): result { - if (expr.tag === "string") { - return ok(expr.value); - } else { - return error("Expression was not string"); - } -} - -function parseRecord( - expr: squiggleExpression -): result<{ [key: string]: squiggleExpression }, string> { - if (expr.tag === "record") { - return ok(expr.value); - } else { - return error("Expression was not a record"); - } -} - -function parseDistribution( - expr: squiggleExpression -): result { - if (expr.tag === "distribution") { - return ok(expr.value); - } else { - return error("Expression was not a distribution"); - } -} - -function parseArray( - expr: squiggleExpression -): result { - if (expr.tag === "array") { - return ok(expr.value); - } else { - return error("Expression was not a distribution"); - } -} - -function parseField( - record: { [key: string]: squiggleExpression }, - field: string, - parser: (expr: squiggleExpression) => result -): result { - if (record[field]) { - return parser(record[field]); - } else { - return error("record does not have field " + field); - } -} - -function parseLabeledDistribution( - x: squiggleExpression -): result { - return resultBind(parseRecord(x), (record) => - resultBind(parseField(record, "name", parseString), (name) => - resultBind( - parseField(record, "distribution", parseDistribution), - (distribution) => ok({ name, distribution }) - ) - ) - ); -} +const schema = yup + .object() + .strict() + .noUnknown() + .shape({ + distributions: yup.object().shape({ + tag: yup.mixed().oneOf(["array"]), + value: yup + .array() + .of( + yup.object().shape({ + tag: yup.mixed().oneOf(["record"]), + value: yup.object().shape({ + name: yup.object().shape({ + tag: yup.mixed().oneOf(["string"]), + value: yup.string().required(), + }), + distribution: yup.object().shape({ + tag: yup.mixed().oneOf(["distribution"]), + value: yup.mixed(), + }), + }), + }) + ) + .required(), + }), + }); export function parsePlot(record: { [key: string]: squiggleExpression; }): result { - return resultBind(parseField(record, "distributions", parseArray), (array) => - resultBind( - flattenResult(array.map(parseLabeledDistribution)), - (distributions) => ok({ distributions }) - ) - ); + try { + const plotRecord = schema.validateSync(record); + return ok({ + distributions: plotRecord.distributions.value.map((x) => ({ + name: x.value.name.value, + distribution: x.value.distribution.value, + })), + }); + } catch (e) { + const message = e instanceof Error ? e.message : "Unknown error"; + return error(message); + } } From fc29a7211ee3bccbd8c25a40f9d75ace34514caf Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 19 Aug 2022 15:17:41 +0400 Subject: [PATCH 13/14] minor improvements --- packages/components/src/components/DistributionChart.tsx | 2 +- packages/components/src/lib/distributionSpecBuilder.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/src/components/DistributionChart.tsx b/packages/components/src/components/DistributionChart.tsx index 84bc5fef..a09e8b69 100644 --- a/packages/components/src/components/DistributionChart.tsx +++ b/packages/components/src/components/DistributionChart.tsx @@ -40,7 +40,7 @@ export function makePlot(record: { [key: string]: squiggleExpression; }): Plot | void { const plotResult = parsePlot(record); - if (plotResult.tag == "Ok") { + if (plotResult.tag === "Ok") { return plotResult.value; } } diff --git a/packages/components/src/lib/distributionSpecBuilder.ts b/packages/components/src/lib/distributionSpecBuilder.ts index a0af7f0c..a6aaf915 100644 --- a/packages/components/src/lib/distributionSpecBuilder.ts +++ b/packages/components/src/lib/distributionSpecBuilder.ts @@ -83,7 +83,7 @@ export function buildVegaSpec( let spec: VisualizationSpec = { $schema: "https://vega.github.io/schema/vega/v5.json", - description: "A basic area chart example", + description: "Squiggle plot chart", width: 500, height: 100, padding: 5, From bf02f69acaa6a4dbea6806e5df2ba3f4d13318fd Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Fri, 19 Aug 2022 21:14:08 +0400 Subject: [PATCH 14/14] legends, blues colors scheme, remove color setting --- .../src/components/DistributionChart.tsx | 5 ++- .../src/components/SquigglePlayground.tsx | 7 +--- .../SquiggleViewer/ItemSettingsMenu.tsx | 7 +--- .../src/components/ViewSettings.tsx | 12 +----- .../src/lib/distributionSpecBuilder.ts | 42 ++++++++++++++----- packages/components/src/lib/plotParser.ts | 17 ++++++-- .../src/stories/SquiggleChart.stories.mdx | 15 ++++++- 7 files changed, 65 insertions(+), 40 deletions(-) diff --git a/packages/components/src/components/DistributionChart.tsx b/packages/components/src/components/DistributionChart.tsx index a09e8b69..79536e12 100644 --- a/packages/components/src/components/DistributionChart.tsx +++ b/packages/components/src/components/DistributionChart.tsx @@ -17,7 +17,7 @@ import { } from "../lib/distributionSpecBuilder"; import { NumberShower } from "./NumberShower"; import { Plot, parsePlot } from "../lib/plotParser"; -import { flattenResult, all } from "../lib/utility"; +import { flattenResult } from "../lib/utility"; import { hasMassBelowZero } from "../lib/distributionUtils"; export type DistributionPlottingSettings = { @@ -52,6 +52,7 @@ export const DistributionChart: React.FC = (props) => { plot.distributions.map((x) => resultMap(x.distribution.pointSet(), (shape) => ({ name: x.name, + // color: x.color, // not supported yet continuous: shape.continuous, discrete: shape.discrete, })) @@ -94,7 +95,7 @@ export const DistributionChart: React.FC = (props) => { /> )}
- {showSummary && plot.distributions.length == 1 && ( + {showSummary && plot.distributions.length === 1 && ( )}
diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 626fc354..c3e38b1a 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -37,10 +37,7 @@ import { InputItem } from "./ui/InputItem"; import { Text } from "./ui/Text"; import { ViewSettings, viewSettingsSchema } from "./ViewSettings"; import { HeadedSection } from "./ui/HeadedSection"; -import { - defaultColor, - defaultTickFormat, -} from "../lib/distributionSpecBuilder"; +import { defaultTickFormat } from "../lib/distributionSpecBuilder"; import { Button } from "./ui/Button"; type PlaygroundProps = SquiggleChartProps & { @@ -240,7 +237,6 @@ export const SquigglePlayground: FC = ({ title, minX, maxX, - color = defaultColor, tickFormat = defaultTickFormat, distributionChartActions, code: controlledCode, @@ -268,7 +264,6 @@ export const SquigglePlayground: FC = ({ title, minX, maxX, - color, tickFormat, distributionChartActions, showSummary, diff --git a/packages/components/src/components/SquiggleViewer/ItemSettingsMenu.tsx b/packages/components/src/components/SquiggleViewer/ItemSettingsMenu.tsx index 2c26b9aa..49c2eacc 100644 --- a/packages/components/src/components/SquiggleViewer/ItemSettingsMenu.tsx +++ b/packages/components/src/components/SquiggleViewer/ItemSettingsMenu.tsx @@ -6,10 +6,7 @@ import { Modal } from "../ui/Modal"; import { ViewSettings, viewSettingsSchema } from "../ViewSettings"; import { Path, pathAsString } from "./utils"; import { ViewerContext } from "./ViewerContext"; -import { - defaultColor, - defaultTickFormat, -} from "../../lib/distributionSpecBuilder"; +import { defaultTickFormat } from "../../lib/distributionSpecBuilder"; import { PlaygroundContext } from "../SquigglePlayground"; type Props = { @@ -46,7 +43,6 @@ const ItemSettingsModal: React.FC< tickFormat: mergedSettings.distributionPlotSettings.format || defaultTickFormat, title: mergedSettings.distributionPlotSettings.title, - color: mergedSettings.distributionPlotSettings.color || defaultColor, minX: mergedSettings.distributionPlotSettings.minX, maxX: mergedSettings.distributionPlotSettings.maxX, distributionChartActions: mergedSettings.distributionPlotSettings.actions, @@ -66,7 +62,6 @@ const ItemSettingsModal: React.FC< expY: vars.expY, format: vars.tickFormat, title: vars.title, - color: vars.color, minX: vars.minX, maxX: vars.maxX, actions: vars.distributionChartActions, diff --git a/packages/components/src/components/ViewSettings.tsx b/packages/components/src/components/ViewSettings.tsx index 9a2ce562..7d70bfc8 100644 --- a/packages/components/src/components/ViewSettings.tsx +++ b/packages/components/src/components/ViewSettings.tsx @@ -5,10 +5,7 @@ import { InputItem } from "./ui/InputItem"; import { Checkbox } from "./ui/Checkbox"; import { HeadedSection } from "./ui/HeadedSection"; import { Text } from "./ui/Text"; -import { - defaultColor, - defaultTickFormat, -} from "../lib/distributionSpecBuilder"; +import { defaultTickFormat } from "../lib/distributionSpecBuilder"; export const viewSettingsSchema = yup.object({}).shape({ chartHeight: yup.number().required().positive().integer().default(350), @@ -18,7 +15,6 @@ export const viewSettingsSchema = yup.object({}).shape({ expY: yup.boolean().required(), tickFormat: yup.string().default(defaultTickFormat), title: yup.string(), - color: yup.string().default(defaultColor).required(), minX: yup.number(), maxX: yup.number(), distributionChartActions: yup.boolean(), @@ -114,12 +110,6 @@ export const ViewSettings: React.FC<{ register={register} label="Tick Format" /> -
diff --git a/packages/components/src/lib/distributionSpecBuilder.ts b/packages/components/src/lib/distributionSpecBuilder.ts index a6aaf915..2b3ac952 100644 --- a/packages/components/src/lib/distributionSpecBuilder.ts +++ b/packages/components/src/lib/distributionSpecBuilder.ts @@ -10,8 +10,6 @@ export type DistributionChartSpecOptions = { minX?: number; /** The maximum x coordinate shown on the chart */ maxX?: number; - /** The color of the chart */ - color?: string; /** The title of the chart */ title?: string; /** The formatting of the ticks */ @@ -57,14 +55,12 @@ export let expYScale: PowScale = { }; export const defaultTickFormat = ".9~s"; -export const defaultColor = "#739ECC"; export function buildVegaSpec( specOptions: DistributionChartSpecOptions ): VisualizationSpec { const { format = defaultTickFormat, - color = defaultColor, title, minX, maxX, @@ -106,7 +102,7 @@ export function buildVegaSpec( data: "data", field: "name", }, - range: { scheme: "category10" }, + range: { scheme: "blues" }, }, ], axes: [ @@ -120,6 +116,7 @@ export function buildVegaSpec( domainOpacity: 0.0, format: format, tickCount: 10, + labelOverlap: "greedy", }, ], marks: [ @@ -259,15 +256,38 @@ export function buildVegaSpec( ], }, ], - }; - if (title) { - spec = { - ...spec, + legends: [ + { + fill: "color", + orient: "top", + labelFontSize: 12, + encode: { + symbols: { + update: { + fill: [ + { test: "length(domain('color')) == 1", value: "transparent" }, + { scale: "color", field: "value" }, + ], + }, + }, + labels: { + interactive: true, + update: { + fill: [ + { test: "length(domain('color')) == 1", value: "transparent" }, + { value: "black" }, + ], + }, + }, + }, + }, + ], + ...(title && { title: { text: title, }, - }; - } + }), + }; return spec; } diff --git a/packages/components/src/lib/plotParser.ts b/packages/components/src/lib/plotParser.ts index 033ac2a3..9d5e224a 100644 --- a/packages/components/src/lib/plotParser.ts +++ b/packages/components/src/lib/plotParser.ts @@ -1,7 +1,11 @@ import * as yup from "yup"; import { Distribution, result, squiggleExpression } from "@quri/squiggle-lang"; -export type LabeledDistribution = { name: string; distribution: Distribution }; +export type LabeledDistribution = { + name: string; + distribution: Distribution; + color?: string; +}; export type Plot = { distributions: LabeledDistribution[]; @@ -27,12 +31,18 @@ const schema = yup .of( yup.object().shape({ tag: yup.mixed().oneOf(["record"]), - value: yup.object().shape({ + value: yup.object({ name: yup.object().shape({ tag: yup.mixed().oneOf(["string"]), value: yup.string().required(), }), - distribution: yup.object().shape({ + // color: yup + // .object({ + // tag: yup.mixed().oneOf(["string"]), + // value: yup.string().required(), + // }) + // .default(undefined), + distribution: yup.object({ tag: yup.mixed().oneOf(["distribution"]), value: yup.mixed(), }), @@ -51,6 +61,7 @@ export function parsePlot(record: { return ok({ distributions: plotRecord.distributions.value.map((x) => ({ name: x.value.name.value, + // color: x.value.color?.value, // not supported yet distribution: x.value.distribution.value, })), }); diff --git a/packages/components/src/stories/SquiggleChart.stories.mdx b/packages/components/src/stories/SquiggleChart.stories.mdx index 14a3f1fe..3c272982 100644 --- a/packages/components/src/stories/SquiggleChart.stories.mdx +++ b/packages/components/src/stories/SquiggleChart.stories.mdx @@ -99,7 +99,20 @@ could be continuous, discrete or mixed.