squiggle/packages/components/src/lib/distributionSpecBuilder.ts

376 lines
9.2 KiB
TypeScript
Raw Normal View History

import { VisualizationSpec } from "react-vega";
import type { LogScale, LinearScale, PowScale } 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;
};
2022-08-23 15:04:35 +00:00
export const linearXScale: LinearScale = {
name: "xscale",
clamp: true,
type: "linear",
range: "width",
zero: false,
nice: false,
domain: { data: "domain", field: "x" },
};
2022-08-23 15:04:35 +00:00
export const linearYScale: LinearScale = {
name: "yscale",
type: "linear",
range: "height",
2022-07-16 03:29:18 +00:00
zero: true,
domain: { data: "domain", field: "y" },
};
2022-08-23 15:04:35 +00:00
export const logXScale: LogScale = {
name: "xscale",
type: "log",
range: "width",
zero: false,
base: 10,
nice: false,
clamp: true,
domain: { data: "domain", field: "x" },
};
2022-08-23 15:04:35 +00:00
export const expYScale: PowScale = {
name: "yscale",
type: "pow",
exponent: 0.1,
range: "height",
2022-07-16 03:29:18 +00:00
zero: true,
nice: false,
domain: { data: "domain", field: "y" },
};
2022-07-22 19:24:49 +00:00
export const defaultTickFormat = ".9~s";
export function buildVegaSpec(
specOptions: DistributionChartSpecOptions
): VisualizationSpec {
const {
2022-07-22 19:24:49 +00:00
format = defaultTickFormat,
title,
minX,
maxX,
logX,
expY,
} = specOptions;
let xScale = logX ? logXScale : linearXScale;
2022-07-11 01:22:21 +00:00
if (minX !== undefined && Number.isFinite(minX)) {
xScale = { ...xScale, domainMin: minX };
}
2022-07-11 01:22:21 +00:00
if (maxX !== undefined && Number.isFinite(maxX)) {
xScale = { ...xScale, domainMax: maxX };
}
2022-08-23 15:04:35 +00:00
const spec: VisualizationSpec = {
$schema: "https://vega.github.io/schema/vega/v5.json",
2022-08-19 11:17:41 +00:00
description: "Squiggle plot chart",
width: 500,
height: 100,
padding: 5,
data: [
2022-08-23 14:05:02 +00:00
{name: "data",},
{ name: "domain",},
],
2022-08-22 15:00:28 +00:00
signals: [
{
"name": "hover",
"value": null,
"on": [
2022-08-23 14:05:02 +00:00
{"events": "mouseover", "update": "datum"},
{"events": "mouseout", "update": "null"}
2022-08-22 15:00:28 +00:00
]
},
{
2022-08-23 14:05:02 +00:00
"name": "position",
"value": "[0, 0]",
"on": [
2022-08-23 15:04:35 +00:00
{ "events": "mousemove", "update": "xy() "},
2022-08-23 14:05:02 +00:00
{ "events": "mouseout", "update": "null"},
]
},
2022-08-23 15:04:35 +00:00
{
"name": "position_scaled",
"value": 0,
"update": "position ? invert('xscale', position[0]) : null"
},
{
"name": "announcer",
"value": "",
"update": "hover ? 'Value: ' + hover.x : ''"
},
2022-08-23 14:05:02 +00:00
2022-08-22 15:00:28 +00:00
],
2022-07-12 07:09:24 +00:00
scales: [
xScale,
expY ? expYScale : linearYScale,
{
name: "color",
type: "ordinal",
domain: {
data: "data",
field: "name",
2022-07-12 07:09:24 +00:00
},
range: { scheme: "blues" },
2022-07-12 07:09:24 +00:00
},
],
axes: [
{
orient: "bottom",
scale: "xscale",
labelColor: "#727d93",
tickColor: "#fff",
tickOpacity: 0.0,
domainColor: "#fff",
domainOpacity: 0.0,
format: format,
tickCount: 10,
labelOverlap: "greedy",
},
],
marks: [
2022-08-23 14:05:02 +00:00
{
name: "all_distributions",
2022-07-12 07:09:24 +00:00
type: "group",
from: {
2022-07-12 07:09:24 +00:00
facet: {
name: "distribution_facet",
data: "data",
2022-07-12 07:09:24 +00:00
groupby: ["name"],
},
},
2022-07-12 07:09:24 +00:00
marks: [
2022-08-23 14:05:02 +00:00
2022-07-12 07:09:24 +00:00
{
name: "continuous_distribution",
type: "group",
2022-07-12 07:09:24 +00:00
from: {
facet: {
name: "continuous_facet",
data: "distribution_facet",
field: "continuous",
},
},
2022-07-12 07:09:24 +00:00
encode: {
update: {},
},
marks: [
{
name: "continuous_area",
type: "area",
from: {
data: "continuous_facet",
2022-07-12 07:09:24 +00:00
},
encode: {
update: {
interpolate: { value: "linear" },
x: {
scale: "xscale",
field: "x",
},
y: {
scale: "yscale",
field: "y",
},
fill: {
scale: "color",
field: { parent: "name" },
},
y2: {
scale: "yscale",
value: 0,
},
fillOpacity: {
value: 1,
},
},
2022-07-12 07:09:24 +00:00
},
},
],
},
{
name: "discrete_distribution",
type: "group",
from: {
facet: {
name: "discrete_facet",
data: "distribution_facet",
field: "discrete",
},
},
marks: [
2022-08-23 14:05:02 +00:00
{
"name": "samples",
"type": "rect",
"from": {"data": "discrete_facet"},
"encode": {
"enter": {
"x": {"scale": "xscale", "field":"x"},
"width": {"value": 1},
"y": {"value": 25, "offset": {"signal": "height"}},
"height": {"value": 5},
"fill": {"value": "steelblue"},
"fillOpacity": {"value": 0.8}
}
}
},
{
type: "rect",
from: {
data: "discrete_facet",
2022-07-12 07:09:24 +00:00
},
encode: {
enter: {
width: {
value: 1,
},
},
update: {
x: {
scale: "xscale",
field: "x",
},
y: {
scale: "yscale",
field: "y",
},
y2: {
scale: "yscale",
value: 0,
},
fill: {
scale: "color",
field: { parent: "name" },
},
},
2022-07-12 07:09:24 +00:00
},
},
{
type: "symbol",
from: {
data: "discrete_facet",
},
encode: {
enter: {
2022-08-23 14:05:02 +00:00
shape: {
value: "circle",
},
size: [{ value: 100 }],
tooltip: {
signal: "{ probability: datum.y, value: datum.x }",
},
},
update: {
x: {
scale: "xscale",
field: "x",
2022-08-22 15:00:28 +00:00
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" },
},
},
2022-07-12 07:09:24 +00:00
},
},
],
},
2022-07-12 07:09:24 +00:00
],
},
2022-08-22 15:00:28 +00:00
{
"type": "text",
"interactive": false,
"encode": {
"enter": {
"x": {"signal": "width", "offset": 1},
"fill": {"value": "black"},
"fontSize": {"value": 20},
"align": {"value": "right"}
},
"update": {
2022-08-24 16:57:35 +00:00
"text": {"signal": "position_scaled ? format(position_scaled, ',.4r') : ''", }
2022-08-22 15:00:28 +00:00
}
}
2022-08-23 14:05:02 +00:00
},
2022-08-23 15:04:35 +00:00
{
"type": "rule",
"encode": {
"enter": {
x: {value: 0},
"y": {"scale": "yscale", "value":0},
2022-08-23 14:05:02 +00:00
2022-08-23 15:04:35 +00:00
y2: {
signal: "height",
offset: 2
},
"strokeDash": {"value": [5, 5]},
},
2022-08-23 14:05:02 +00:00
2022-08-23 15:04:35 +00:00
"update": {
"x": {"signal": "position ? position[0] < 0 ? null : position[0] > width ? null : position[0]: null"},
2022-08-23 14:05:02 +00:00
2022-08-23 15:04:35 +00:00
"opacity": {"signal": "position ? 1 : 0"}
},
}
2022-08-23 14:05:02 +00:00
2022-08-23 15:04:35 +00:00
}
],
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;
}