2022-06-24 07:22:31 +00:00
import { VisualizationSpec } from "react-vega" ;
2022-08-29 20:27:13 +00:00
import type { LogScale , LinearScale , PowScale , TimeScale } from "vega" ;
2022-06-24 07:22:31 +00:00
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-29 20:27:13 +00:00
/** Whether or not to show the band of sample data at the bottom */
sample? : boolean ;
/** Whether the x-axis should be dates or numbers */
xAxis ? : "number" | "dateTime" ;
2022-06-24 07:22:31 +00:00
} ;
2022-09-06 17:27:01 +00:00
/** X Scales */
2022-08-23 15:04:35 +00:00
export const linearXScale : LinearScale = {
2022-06-24 07:22:31 +00:00
name : "xscale" ,
clamp : true ,
type : "linear" ,
range : "width" ,
zero : false ,
nice : false ,
2022-07-13 04:15:07 +00:00
domain : { data : "domain" , field : "x" } ,
2022-06-24 07:22:31 +00:00
} ;
2022-08-25 00:13:50 +00:00
2022-08-23 15:04:35 +00:00
export const logXScale : LogScale = {
2022-06-24 07:22:31 +00:00
name : "xscale" ,
type : "log" ,
range : "width" ,
zero : false ,
base : 10 ,
nice : false ,
clamp : true ,
2022-07-13 04:15:07 +00:00
domain : { data : "domain" , field : "x" } ,
2022-06-24 07:22:31 +00:00
} ;
2022-08-29 20:27:13 +00:00
export const timeXScale : TimeScale = {
name : "xscale" ,
clamp : true ,
type : "time" ,
range : "width" ,
nice : false ,
2022-09-06 17:27:01 +00:00
domain : { data : "domain" , field : "x" } ,
2022-08-29 20:27:13 +00:00
} ;
2022-09-06 17:27:01 +00:00
/** Y Scales */
2022-08-29 20:27:13 +00:00
export const linearYScale : LinearScale = {
name : "yscale" ,
type : "linear" ,
range : "height" ,
zero : true ,
domain : { data : "domain" , field : "y" } ,
} ;
2022-08-23 15:04:35 +00:00
export const expYScale : PowScale = {
2022-06-24 07:22:31 +00:00
name : "yscale" ,
type : "pow" ,
exponent : 0.1 ,
range : "height" ,
2022-07-16 03:29:18 +00:00
zero : true ,
2022-06-24 07:22:31 +00:00
nice : false ,
2022-07-13 04:15:07 +00:00
domain : { data : "domain" , field : "y" } ,
2022-06-24 07:22:31 +00:00
} ;
2022-07-22 19:24:49 +00:00
export const defaultTickFormat = ".9~s" ;
2022-08-29 20:27:13 +00:00
export const timeTickFormat = "%b %d, %Y %H:%M" ;
2022-08-29 23:32:32 +00:00
const width = 500 ;
2022-07-22 19:24:49 +00:00
2022-06-24 07:22:31 +00:00
export function buildVegaSpec (
specOptions : DistributionChartSpecOptions
) : VisualizationSpec {
2022-08-31 15:15:43 +00:00
const { title , minX , maxX , logX , expY , xAxis = "number" } = specOptions ;
2022-06-24 07:22:31 +00:00
2022-08-29 20:27:13 +00:00
const dateTime = xAxis === "dateTime" ;
2022-08-29 21:01:07 +00:00
// some fallbacks
const format = specOptions ? . format
? specOptions . format
: dateTime
? timeTickFormat
: defaultTickFormat ;
2022-08-29 20:27:13 +00:00
let xScale = dateTime ? timeXScale : logX ? logXScale : linearXScale ;
2022-07-11 01:22:21 +00:00
if ( minX !== undefined && Number . isFinite ( minX ) ) {
2022-06-24 07:22:31 +00:00
xScale = { . . . xScale , domainMin : minX } ;
}
2022-07-11 01:22:21 +00:00
if ( maxX !== undefined && Number . isFinite ( maxX ) ) {
2022-06-24 07:22:31 +00:00
xScale = { . . . xScale , domainMax : maxX } ;
}
2022-08-23 15:04:35 +00:00
const spec : VisualizationSpec = {
2022-06-24 07:22:31 +00:00
$schema : "https://vega.github.io/schema/vega/v5.json" ,
2022-08-19 11:17:41 +00:00
description : "Squiggle plot chart" ,
2022-08-29 23:32:32 +00:00
width : width ,
2022-06-24 07:22:31 +00:00
height : 100 ,
padding : 5 ,
2022-08-31 14:19:11 +00:00
data : [ { name : "data" } , { name : "domain" } , { name : "samples" } ] ,
2022-08-22 15:00:28 +00:00
signals : [
{
2022-08-24 23:56:22 +00:00
name : "hover" ,
value : null ,
on : [
{ events : "mouseover" , update : "datum" } ,
{ events : "mouseout" , update : "null" } ,
] ,
2022-08-23 14:05:02 +00:00
} ,
2022-08-23 15:04:35 +00:00
{
2022-08-24 23:56:22 +00:00
name : "position" ,
value : "[0, 0]" ,
on : [
{ events : "mousemove" , update : "xy() " } ,
{ events : "mouseout" , update : "null" } ,
] ,
2022-08-23 15:04:35 +00:00
} ,
{
2022-08-24 23:56:22 +00:00
name : "position_scaled" ,
2022-08-29 21:01:07 +00:00
value : null ,
update : "isArray(position) ? invert('xscale', position[0]) : ''" ,
2022-08-23 15:04:35 +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 : {
2022-07-13 04:15:07 +00:00
data : "data" ,
field : "name" ,
2022-07-12 07:09:24 +00:00
} ,
2022-08-19 17:14:08 +00:00
range : { scheme : "blues" } ,
2022-07-12 07:09:24 +00:00
} ,
] ,
2022-06-24 07:22:31 +00:00
axes : [
{
orient : "bottom" ,
scale : "xscale" ,
labelColor : "#727d93" ,
tickColor : "#fff" ,
tickOpacity : 0.0 ,
domainColor : "#fff" ,
domainOpacity : 0.0 ,
format : format ,
2022-08-29 21:01:07 +00:00
tickCount : dateTime ? 3 : 10 ,
2022-08-19 17:14:08 +00:00
labelOverlap : "greedy" ,
2022-06-24 07:22:31 +00:00
} ,
] ,
marks : [
{
2022-07-13 04:15:07 +00:00
name : "all_distributions" ,
2022-07-12 07:09:24 +00:00
type : "group" ,
2022-06-24 07:22:31 +00:00
from : {
2022-07-12 07:09:24 +00:00
facet : {
2022-07-13 04:15:07 +00:00
name : "distribution_facet" ,
data : "data" ,
2022-07-12 07:09:24 +00:00
groupby : [ "name" ] ,
} ,
2022-06-24 07:22:31 +00:00
} ,
2022-07-12 07:09:24 +00:00
marks : [
{
2022-07-13 04:15:07 +00:00
name : "continuous_distribution" ,
type : "group" ,
2022-07-12 07:09:24 +00:00
from : {
2022-07-13 04:15:07 +00:00
facet : {
name : "continuous_facet" ,
data : "distribution_facet" ,
field : "continuous" ,
} ,
2022-06-24 07:22:31 +00:00
} ,
2022-07-12 07:09:24 +00:00
encode : {
2022-07-13 04:15:07 +00:00
update : { } ,
} ,
marks : [
{
name : "continuous_area" ,
type : "area" ,
from : {
data : "continuous_facet" ,
2022-07-12 07:09:24 +00:00
} ,
2022-07-13 04:15:07 +00:00
encode : {
update : {
interpolate : { value : "linear" } ,
x : {
scale : "xscale" ,
2022-09-06 17:27:01 +00:00
field : "x" ,
2022-07-13 04:15:07 +00:00
} ,
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
} ,
2022-07-13 04:15:07 +00:00
} ,
] ,
} ,
{
name : "discrete_distribution" ,
type : "group" ,
from : {
facet : {
name : "discrete_facet" ,
data : "distribution_facet" ,
field : "discrete" ,
} ,
} ,
marks : [
{
type : "rect" ,
from : {
data : "discrete_facet" ,
2022-07-12 07:09:24 +00:00
} ,
2022-07-13 04:15:07 +00:00
encode : {
enter : {
width : {
value : 1 ,
} ,
} ,
update : {
x : {
scale : "xscale" ,
2022-09-06 17:27:01 +00:00
field : "x" ,
2022-07-13 04:15:07 +00:00
} ,
y : {
scale : "yscale" ,
field : "y" ,
} ,
y2 : {
scale : "yscale" ,
value : 0 ,
} ,
2022-07-13 05:32:28 +00:00
fill : {
scale : "color" ,
field : { parent : "name" } ,
} ,
2022-07-13 04:15:07 +00:00
} ,
2022-07-12 07:09:24 +00:00
} ,
2022-07-13 04:15:07 +00:00
} ,
{
type : "symbol" ,
from : {
data : "discrete_facet" ,
} ,
encode : {
enter : {
shape : {
value : "circle" ,
} ,
size : [ { value : 100 } ] ,
tooltip : {
2022-08-29 20:27:13 +00:00
signal : dateTime
2022-09-06 17:27:01 +00:00
? "{ probability: datum.y, value: datetime(datum.x) }"
: "{ probability: datum.y, value: datum.x }" ,
2022-07-13 04:15:07 +00:00
} ,
} ,
update : {
x : {
scale : "xscale" ,
2022-09-06 17:27:01 +00:00
field : "x" ,
2022-08-24 23:56:22 +00:00
offset : 0.5 , // if this is not included, the circles are slightly left of center.
2022-07-13 04:15:07 +00:00
} ,
y : {
scale : "yscale" ,
field : "y" ,
} ,
2022-07-13 05:32:28 +00:00
fill : {
scale : "color" ,
field : { parent : "name" } ,
} ,
2022-07-13 04:15:07 +00:00
} ,
2022-07-12 07:09:24 +00:00
} ,
} ,
2022-07-13 04:15:07 +00:00
] ,
2022-06-24 07:22:31 +00:00
} ,
2022-08-31 14:19:11 +00:00
{
name : "sample_distributions" ,
type : "group" ,
from : {
facet : {
name : "sample_facet" ,
data : "distribution_facet" ,
field : "samples" ,
} ,
} ,
marks : [
{
name : "samples" ,
type : "rect" ,
from : { data : "sample_facet" } ,
encode : {
enter : {
2022-09-06 17:27:01 +00:00
x : { scale : "xscale" , field : "x" } ,
2022-08-31 14:19:11 +00:00
width : { value : 0.1 } ,
y : { value : 25 , offset : { signal : "height" } } ,
height : { value : 5 } ,
2022-08-31 15:15:43 +00:00
fill : {
scale : "color" ,
field : { parent : "name" } ,
} ,
2022-08-31 14:19:11 +00:00
fillOpacity : { value : 1 } ,
} ,
} ,
} ,
] ,
} ,
2022-07-12 07:09:24 +00:00
] ,
2022-06-24 07:22:31 +00:00
} ,
2022-08-22 15:00:28 +00:00
{
2022-08-24 23:56:22 +00:00
type : "text" ,
2022-08-29 18:28:31 +00:00
name : "announcer" ,
2022-08-24 23:56:22 +00:00
interactive : false ,
encode : {
enter : {
2022-08-29 23:32:32 +00:00
x : { signal : String ( width ) , offset : 1 } , // vega would prefer its internal ` "width" ` variable, but that breaks the squiggle playground. Just setting it to the same var as used elsewhere in the spec achieves the same result.
2022-08-24 23:56:22 +00:00
fill : { value : "black" } ,
fontSize : { value : 20 } ,
align : { value : "right" } ,
} ,
update : {
text : {
2022-08-29 21:01:07 +00:00
signal : dateTime
? "position_scaled ? utcyear(position_scaled) + '-' + utcmonth(position_scaled) + '-' + utcdate(position_scaled) + 'T' + utchours(position_scaled)+':' +utcminutes(position_scaled) : ''"
: "position_scaled ? format(position_scaled, ',.4r') : ''" ,
2022-08-24 23:56:22 +00:00
} ,
2022-08-22 15:00:28 +00:00
} ,
2022-08-24 23:56:22 +00:00
} ,
2022-08-23 14:05:02 +00:00
} ,
2022-08-23 15:04:35 +00:00
{
2022-08-24 23:56:22 +00:00
type : "rule" ,
2022-08-29 19:00:51 +00:00
interactive : false ,
2022-08-24 23:56:22 +00:00
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 : {
2022-08-24 23:56:22 +00:00
signal : "height" ,
offset : 2 ,
} ,
strokeDash : { value : [ 5 , 5 ] } ,
} ,
update : {
x : {
signal :
"position ? position[0] < 0 ? null : position[0] > width ? null : position[0]: null" ,
2022-08-23 15:04:35 +00:00
} ,
2022-08-23 14:05:02 +00:00
2022-08-29 18:28:31 +00:00
opacity : {
signal :
"position ? position[0] < 0 ? 0 : position[0] > width ? 0 : 1 : 0" ,
} ,
2022-08-23 15:04:35 +00:00
} ,
2022-08-24 23:56:22 +00:00
} ,
} ,
2022-06-24 07:22:31 +00:00
] ,
2022-08-19 17:14:08 +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 && {
2022-06-24 07:22:31 +00:00
title : {
text : title ,
} ,
2022-08-19 17:14:08 +00:00
} ) ,
} ;
2022-06-24 07:22:31 +00:00
return spec ;
}