announce x-axis date properly

This commit is contained in:
Conor Barnes 2022-08-24 20:56:22 -03:00
parent f8e5396d57
commit a99ee96f5c
3 changed files with 545 additions and 85 deletions

View File

@ -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<DistributionChartProps> = (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 (
<ErrorAlert heading="Distribution Error">
{distributionErrorToString(shapes.value)}
</ErrorAlert>
);
}
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 (
<div style={{ width: widthProp }}>
{logX && shapes.value.some(hasMassBelowZero) ? (
<ErrorAlert heading="Log Domain Error">
Cannot graph distribution with negative values on logarithmic scale.
</ErrorAlert>
) : (
<Vega
spec={spec}
data={{ data: dateData, domain: dateDomain }}
width={widthProp - 10}
height={height}
actions={actions}
/>
)}
<div className="flex justify-center">
{showSummary && plot.distributions.length === 1 && (
<SummaryTable distribution={plot.distributions[0].distribution} />
)}
</div>
</div>
);
});
return sized;
};
export const DistributionChart: React.FC<DistributionChartProps> = (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<DistributionChartProps> = (props) => {
shape.discrete.concat(shape.continuous)
);
console.log({domain, data: shapes.value})
return (
<div style={{ width: widthProp }}>
{logX && shapes.value.some(hasMassBelowZero) ? (
@ -89,6 +184,7 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
Cannot graph distribution with negative values on logarithmic scale.
</ErrorAlert>
) : (
<>
<Vega
spec={spec}
data={{ data: shapes.value, domain }}
@ -96,6 +192,18 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => {
height={height}
actions={actions}
/>
<div id="CONORDELETETHIS">
<DateDistributionChart
plot={plot}
height={height}
showSummary={showSummary}
width={width}
logX={logX}
actions={actions}
expY={props.expY}
/>
</div>
</>
)}
<div className="flex justify-center">
{showSummary && plot.distributions.length === 1 && (

View File

@ -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;
}

View File

@ -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,7 +137,7 @@ export function buildVegaSpec(
marks: [
{
name: "sample_distributions",
"type": "group",
type: "group",
from: {
facet: {
name: "distribution_facet",
@ -157,22 +147,22 @@ export function buildVegaSpec(
},
marks: [
{
"name": "samples",
"type": "rect",
"from": {"data": "distribution_facet"},
"encode": {
"enter": {
"x": {"scale": "xscale", "field":"x"},
"width": {"value": 0.5},
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}
}
}
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",
},
@ -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": {
type: "rule",
encode: {
enter: {
x: { value: 0 },
"y": {"scale": "yscale", "value":0},
y: { scale: "yscale", value: 0 },
y2: {
signal: "height",
offset: 2
offset: 2,
},
"strokeDash": {"value": [5, 5]},
strokeDash: { value: [5, 5] },
},
"update": {
"x": {"signal": "position ? position[0] < 0 ? null : position[0] > width ? null : position[0]: null"},
"opacity": {"signal": "position ? 1 : 0"}
update: {
x: {
signal:
"position ? position[0] < 0 ? null : position[0] > width ? null : position[0]: null",
},
}
}
opacity: { signal: "position ? 1 : 0" },
},
},
},
],
legends: [
{