diff --git a/packages/components/src/components/DistributionChart.tsx b/packages/components/src/components/DistributionChart.tsx index 52172d22..6b4bdbd6 100644 --- a/packages/components/src/components/DistributionChart.tsx +++ b/packages/components/src/components/DistributionChart.tsx @@ -15,6 +15,7 @@ import { buildVegaSpec, DistributionChartSpecOptions, } from "../lib/distributionSpecBuilder"; +import { buildDateVegaSpec } from "../lib/dateDistributionSpecBuilder"; import { NumberShower } from "./NumberShower"; import { Plot, parsePlot } from "../lib/plotParser"; import { flattenResult } from "../lib/utility"; @@ -30,6 +31,7 @@ export type DistributionChartProps = { plot: Plot; width?: number; height: number; + xAxis?: "number" | "dateTime"; } & DistributionPlottingSettings; export function defaultPlot(distribution: Distribution): Plot { @@ -45,8 +47,102 @@ export function makePlot(record: { } } +const DateDistributionChart: React.FC = (props) => { + const { + plot, + height, + showSummary, + width, + logX, + actions = false, + xAxis = "dateTime", + } = props; + // const [xAxis, setXAxis] = React.useState<"dateAndTime" | "numbers">("dateAndTime") + const [sized] = useSize((size) => { + const shapes = flattenResult( + plot.distributions.map((x) => + resultMap(x.distribution.pointSet(), (shape) => ({ + name: x.name, + // color: x.color, // not supported yet + continuous: shape.continuous, + discrete: shape.discrete, + })) + ) + ); + + if (shapes.tag === "Error") { + return ( + + {distributionErrorToString(shapes.value)} + + ); + } + + const spec = buildDateVegaSpec(props); + + let widthProp = width ? width : size.width; + if (widthProp < 20) { + console.warn( + `Width of Distribution is set to ${widthProp}, which is too small` + ); + widthProp = 20; + } + const domain = shapes.value.flatMap((shape) => + shape.discrete.concat(shape.continuous) + ); + + const dateData = { + name: "default", + continuous: [], + discrete: [ + { dateTime: new Date().getTime() - 1000000, y: 0.3 }, + { dateTime: new Date().getTime(), y: 0.5 }, + { dateTime: new Date().getTime() + 1000000, y: 0.7 }, + ], + }; + + const dateDomain = [ + { dateTime: new Date().getTime() - 1000000, y: 0.2 }, + { dateTime: new Date().getTime(), y: 0.5 }, + { dateTime: new Date().getTime() + 1000000, y: 0.7 }, + ]; + + return ( +
+ {logX && shapes.value.some(hasMassBelowZero) ? ( + + Cannot graph distribution with negative values on logarithmic scale. + + ) : ( + + )} +
+ {showSummary && plot.distributions.length === 1 && ( + + )} +
+
+ ); + }); + return sized; +}; + export const DistributionChart: React.FC = (props) => { - const { plot, height, showSummary, width, logX, actions = false } = props; + const { + plot, + height, + showSummary, + width, + logX, + actions = false, + xAxis = "number", + } = props; // const [xAxis, setXAxis] = React.useState<"dateAndTime" | "numbers">("dateAndTime") const [sized] = useSize((size) => { const shapes = flattenResult( @@ -81,7 +177,6 @@ export const DistributionChart: React.FC = (props) => { shape.discrete.concat(shape.continuous) ); - console.log({domain, data: shapes.value}) return (
{logX && shapes.value.some(hasMassBelowZero) ? ( @@ -89,13 +184,26 @@ export const DistributionChart: React.FC = (props) => { Cannot graph distribution with negative values on logarithmic scale. ) : ( - + <> + +
+ +
+ )}
{showSummary && plot.distributions.length === 1 && ( diff --git a/packages/components/src/lib/dateDistributionSpecBuilder.ts b/packages/components/src/lib/dateDistributionSpecBuilder.ts new file mode 100644 index 00000000..140dd954 --- /dev/null +++ b/packages/components/src/lib/dateDistributionSpecBuilder.ts @@ -0,0 +1,361 @@ +import { VisualizationSpec } from "react-vega"; +import type { LogScale, LinearScale, PowScale, TimeScale } from "vega"; + +export type DistributionChartSpecOptions = { + /** Set the x scale to be logarithmic by deault */ + logX: boolean; + /** Set the y scale to be exponential by deault */ + expY: boolean; + /** The minimum x coordinate shown on the chart */ + minX?: number; + /** The maximum x coordinate shown on the chart */ + maxX?: number; + /** The title of the chart */ + title?: string; + /** The formatting of the ticks */ + format?: string; +}; + +export const timeXScale: TimeScale = { + name: "xscale", + clamp: true, + type: "time", + range: "width", + nice: false, + domain: { data: "domain", field: "dateTime" }, +}; +export const timeYScale: TimeScale = { + name: "yscale", + type: "time", + range: "height", + domain: { data: "domain", field: "y" }, +}; + +export const defaultTickFormat = "%b %d, %Y %H:%M"; + +export function buildDateVegaSpec( + specOptions: DistributionChartSpecOptions +): VisualizationSpec { + const { + format = defaultTickFormat, + title, + minX, + maxX, + logX, + expY, + } = specOptions; + + let xScale = timeXScale; + if (minX !== undefined && Number.isFinite(minX)) { + xScale = { ...xScale, domainMin: minX }; + } + + if (maxX !== undefined && Number.isFinite(maxX)) { + xScale = { ...xScale, domainMax: maxX }; + } + + const spec: VisualizationSpec = { + $schema: "https://vega.github.io/schema/vega/v5.json", + description: "Squiggle plot chart", + width: 500, + height: 100, + padding: 5, + data: [{ name: "data" }, { name: "domain" }], + signals: [ + { + name: "hover", + value: null, + on: [ + { events: "mouseover", update: "datum" }, + { events: "mouseout", update: "null" }, + ], + }, + { + name: "position", + value: "[0, 0]", + on: [ + { events: "mousemove", update: "xy() " }, + { events: "mouseout", update: "null" }, + ], + }, + { + name: "position_scaled", + value: null, + update: "isArray(position) ? invert('xscale', position[0]) : ''", + }, + ], + scales: [ + xScale, + timeYScale, + { + name: "color", + type: "ordinal", + domain: { + data: "data", + field: "name", + }, + range: { scheme: "blues" }, + }, + ], + axes: [ + { + orient: "bottom", + scale: "xscale", + labelColor: "#727d93", + tickColor: "#fff", + tickOpacity: 0.0, + domainColor: "#fff", + domainOpacity: 0.0, + format: format, + tickCount: 3, + labelOverlap: "greedy", + }, + ], + marks: [ + { + name: "sample_distributions", + type: "group", + from: { + facet: { + name: "distribution_facet", + data: "domain", + groupby: ["name"], + }, + }, + marks: [ + { + name: "samples", + type: "rect", + from: { data: "distribution_facet" }, + encode: { + enter: { + x: { scale: "xscale", field: "dateTime" }, + width: { value: 0.5 }, + + y: { value: 25, offset: { signal: "height" } }, + height: { value: 5 }, + fill: { value: "steelblue" }, + fillOpacity: { value: 0.8 }, + }, + }, + }, + ], + }, + { + name: "all_distributions", + type: "group", + from: { + facet: { + name: "distribution_facet", + data: "data", + groupby: ["name"], + }, + }, + marks: [ + { + name: "continuous_distribution", + type: "group", + from: { + facet: { + name: "continuous_facet", + data: "distribution_facet", + field: "continuous", + }, + }, + encode: { + update: {}, + }, + marks: [ + { + name: "continuous_area", + type: "area", + from: { + data: "continuous_facet", + }, + encode: { + update: { + // interpolate: { value: "linear" }, + x: { + scale: "xscale", + field: "dateTime", + }, + y: { + scale: "yscale", + field: "y", + }, + fill: { + scale: "color", + field: { parent: "name" }, + }, + y2: { + scale: "yscale", + value: 0, + }, + fillOpacity: { + value: 1, + }, + }, + }, + }, + ], + }, + { + name: "discrete_distribution", + type: "group", + from: { + facet: { + name: "discrete_facet", + data: "distribution_facet", + field: "discrete", + }, + }, + marks: [ + { + type: "rect", + from: { + data: "discrete_facet", + }, + encode: { + enter: { + width: { + value: 1, + }, + }, + update: { + x: { + scale: "xscale", + field: "dateTime", + }, + y: { + scale: "yscale", + field: "y", + }, + y2: { + scale: "yscale", + value: 0, + }, + fill: { + scale: "color", + field: { parent: "name" }, + }, + }, + }, + }, + { + type: "symbol", + from: { + data: "discrete_facet", + }, + encode: { + enter: { + shape: { + value: "circle", + }, + size: [{ value: 100 }], + tooltip: { + signal: + "{ probability: datum.y, value: datetime(datum.dateTime) }", + }, + }, + update: { + x: { + scale: "xscale", + field: "dateTime", + offset: 0.5, // if this is not included, the circles are slightly left of center. + }, + y: { + scale: "yscale", + field: "y", + }, + fill: { + scale: "color", + field: { parent: "name" }, + }, + }, + }, + }, + ], + }, + ], + }, + { + type: "text", + interactive: false, + encode: { + enter: { + text: { + signal: "", + }, + x: { signal: "width", offset: 1 }, + fill: { value: "black" }, + fontSize: { value: 20 }, + align: { value: "right" }, + }, + update: { + text: { + signal: + "position_scaled ? utcyear(position_scaled) + '-' + utcmonth(position_scaled) + '-' + utcdate(position_scaled) + 'T' + utchours(position_scaled)+':' +utcminutes(position_scaled) : ''", + }, + }, + }, + }, + { + type: "rule", + encode: { + enter: { + x: { value: 0 }, + y: { scale: "yscale", value: 0 }, + + y2: { + signal: "height", + offset: 2, + }, + strokeDash: { value: [5, 5] }, + }, + + update: { + x: { + signal: + "position ? position[0] < 0 ? null : position[0] > width ? null : position[0]: null", + }, + + opacity: { signal: "position ? 1 : 0" }, + }, + }, + }, + ], + 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/distributionSpecBuilder.ts b/packages/components/src/lib/distributionSpecBuilder.ts index 65820cf2..03673882 100644 --- a/packages/components/src/lib/distributionSpecBuilder.ts +++ b/packages/components/src/lib/distributionSpecBuilder.ts @@ -83,39 +83,29 @@ export function buildVegaSpec( width: 500, height: 100, padding: 5, - data: [ - {name: "data",}, - { name: "domain",}, - ], + data: [{ name: "data" }, { name: "domain" }], signals: [ { - "name": "hover", - "value": null, - "on": [ - {"events": "mouseover", "update": "datum"}, - {"events": "mouseout", "update": "null"} - ] + name: "hover", + value: null, + on: [ + { events: "mouseover", update: "datum" }, + { events: "mouseout", update: "null" }, + ], }, { - "name": "position", - "value": "[0, 0]", - "on": [ - { "events": "mousemove", "update": "xy() "}, - { "events": "mouseout", "update": "null"}, - ] + name: "position", + value: "[0, 0]", + on: [ + { events: "mousemove", update: "xy() " }, + { events: "mouseout", update: "null" }, + ], }, { - "name": "position_scaled", - "value": 0, - "update": "position ? invert('xscale', position[0]) : null" + name: "position_scaled", + value: 0, + update: "position ? invert('xscale', position[0]) : null", }, - { - "name": "announcer", - "value": "", - "update": "hover ? 'Value: ' + hover.x : ''" - }, - - ], scales: [ xScale, @@ -147,32 +137,32 @@ export function buildVegaSpec( marks: [ { name: "sample_distributions", - "type": "group", + type: "group", from: { facet: { name: "distribution_facet", data: "domain", groupby: ["name"], }, - }, + }, marks: [ { - "name": "samples", - "type": "rect", - "from": {"data": "distribution_facet"}, - "encode": { - "enter": { - "x": {"scale": "xscale", "field":"x"}, - "width": {"value": 0.5}, - - "y": {"value": 25, "offset": {"signal": "height"}}, - "height": {"value": 5}, - "fill": {"value": "steelblue"}, - "fillOpacity": {"value": 0.8} - } - } + name: "samples", + type: "rect", + from: { data: "distribution_facet" }, + encode: { + enter: { + x: { scale: "xscale", field: "x" }, + width: { value: 0.5 }, + + y: { value: 25, offset: { signal: "height" } }, + height: { value: 5 }, + fill: { value: "steelblue" }, + fillOpacity: { value: 0.8 }, + }, + }, }, - ] + ], }, { name: "all_distributions", @@ -185,7 +175,6 @@ export function buildVegaSpec( }, }, marks: [ - { name: "continuous_distribution", type: "group", @@ -244,7 +233,6 @@ export function buildVegaSpec( }, }, marks: [ - { type: "rect", from: { @@ -283,7 +271,6 @@ export function buildVegaSpec( }, encode: { enter: { - shape: { value: "circle", }, @@ -296,7 +283,7 @@ export function buildVegaSpec( x: { scale: "xscale", field: "x", - offset: 0.5, // if this is not included, the circles are slightly left of center. + offset: 0.5, // if this is not included, the circles are slightly left of center. }, y: { scale: "yscale", @@ -314,42 +301,46 @@ export function buildVegaSpec( ], }, { - "type": "text", - "interactive": false, - "encode": { - "enter": { - "x": {"signal": "width", "offset": 1}, - "fill": {"value": "black"}, - "fontSize": {"value": 20}, - "align": {"value": "right"} + type: "text", + interactive: false, + encode: { + enter: { + x: { signal: "width", offset: 1 }, + fill: { value: "black" }, + fontSize: { value: 20 }, + align: { value: "right" }, }, - "update": { - "text": {"signal": "position_scaled ? format(position_scaled, ',.4r') : ''", } - } - } + update: { + text: { + signal: "position_scaled ? format(position_scaled, ',.4r') : ''", + }, + }, + }, }, { - "type": "rule", - "encode": { - "enter": { - x: {value: 0}, - "y": {"scale": "yscale", "value":0}, + type: "rule", + encode: { + enter: { + x: { value: 0 }, + y: { scale: "yscale", value: 0 }, y2: { - signal: "height", - offset: 2 + signal: "height", + offset: 2, }, - "strokeDash": {"value": [5, 5]}, - }, - - "update": { - "x": {"signal": "position ? position[0] < 0 ? null : position[0] > width ? null : position[0]: null"}, - - "opacity": {"signal": "position ? 1 : 0"} + strokeDash: { value: [5, 5] }, }, - } - - } + + update: { + x: { + signal: + "position ? position[0] < 0 ? null : position[0] > width ? null : position[0]: null", + }, + + opacity: { signal: "position ? 1 : 0" }, + }, + }, + }, ], legends: [ {