combined distribution chart component
This commit is contained in:
		
							parent
							
								
									7c00897833
								
							
						
					
					
						commit
						0292c66c80
					
				|  | @ -15,7 +15,6 @@ 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"; | ||||
|  | @ -31,6 +30,7 @@ export type DistributionChartProps = { | |||
|   plot: Plot; | ||||
|   width?: number; | ||||
|   height: number; | ||||
|   sample: boolean; | ||||
|   xAxis?: "number" | "dateTime"; | ||||
| } & DistributionPlottingSettings; | ||||
| 
 | ||||
|  | @ -77,8 +77,7 @@ export const DistributionChart: React.FC<DistributionChartProps> = (props) => { | |||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     const spec = | ||||
|       xAxis === "dateTime" ? buildDateVegaSpec(props) : buildVegaSpec(props); | ||||
|     const spec = buildVegaSpec(props); | ||||
| 
 | ||||
|     let widthProp = width ? width : size.width; | ||||
|     if (widthProp < 20) { | ||||
|  |  | |||
|  | @ -1,384 +0,0 @@ | |||
| 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 linearYScale: LinearScale = { | ||||
|   name: "yscale", | ||||
|   type: "linear", | ||||
|   range: "height", | ||||
|   zero: true, | ||||
|   domain: { data: "domain", field: "y" }, | ||||
| }; | ||||
| 
 | ||||
| export const logXScale: LogScale = { | ||||
|   name: "xscale", | ||||
|   type: "log", | ||||
|   range: "width", | ||||
|   zero: false, | ||||
|   base: 10, | ||||
|   nice: false, | ||||
|   clamp: true, | ||||
|   domain: { data: "domain", field: "x" }, | ||||
| }; | ||||
| 
 | ||||
| export const expYScale: PowScale = { | ||||
|   name: "yscale", | ||||
|   type: "pow", | ||||
|   exponent: 0.1, | ||||
|   range: "height", | ||||
|   zero: true, | ||||
|   nice: false, | ||||
|   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, | ||||
|       expY ? expYScale : linearYScale, | ||||
|       { | ||||
|         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; | ||||
| } | ||||
|  | @ -1,5 +1,5 @@ | |||
| import { VisualizationSpec } from "react-vega"; | ||||
| import type { LogScale, LinearScale, PowScale } from "vega"; | ||||
| import type { LogScale, LinearScale, PowScale, TimeScale } from "vega"; | ||||
| 
 | ||||
| export type DistributionChartSpecOptions = { | ||||
|   /** Set the x scale to be logarithmic by deault */ | ||||
|  | @ -14,6 +14,12 @@ export type DistributionChartSpecOptions = { | |||
|   title?: string; | ||||
|   /** The formatting of the ticks */ | ||||
|   format?: string; | ||||
| 
 | ||||
|   /** 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"; | ||||
| }; | ||||
| 
 | ||||
| export const linearXScale: LinearScale = { | ||||
|  | @ -26,14 +32,6 @@ export const linearXScale: LinearScale = { | |||
|   domain: { data: "domain", field: "x" }, | ||||
| }; | ||||
| 
 | ||||
| export const linearYScale: LinearScale = { | ||||
|   name: "yscale", | ||||
|   type: "linear", | ||||
|   range: "height", | ||||
|   zero: true, | ||||
|   domain: { data: "domain", field: "y" }, | ||||
| }; | ||||
| 
 | ||||
| export const logXScale: LogScale = { | ||||
|   name: "xscale", | ||||
|   type: "log", | ||||
|  | @ -45,6 +43,23 @@ export const logXScale: LogScale = { | |||
|   domain: { data: "domain", field: "x" }, | ||||
| }; | ||||
| 
 | ||||
| export const timeXScale: TimeScale = { | ||||
|   name: "xscale", | ||||
|   clamp: true, | ||||
|   type: "time", | ||||
|   range: "width", | ||||
|   nice: false, | ||||
|   domain: { data: "domain", field: "dateTime" }, | ||||
| }; | ||||
| 
 | ||||
| export const linearYScale: LinearScale = { | ||||
|   name: "yscale", | ||||
|   type: "linear", | ||||
|   range: "height", | ||||
|   zero: true, | ||||
|   domain: { data: "domain", field: "y" }, | ||||
| }; | ||||
| 
 | ||||
| export const expYScale: PowScale = { | ||||
|   name: "yscale", | ||||
|   type: "pow", | ||||
|  | @ -56,6 +71,7 @@ export const expYScale: PowScale = { | |||
| }; | ||||
| 
 | ||||
| export const defaultTickFormat = ".9~s"; | ||||
| export const timeTickFormat = "%b %d, %Y %H:%M"; | ||||
| 
 | ||||
| export function buildVegaSpec( | ||||
|   specOptions: DistributionChartSpecOptions | ||||
|  | @ -67,9 +83,14 @@ export function buildVegaSpec( | |||
|     maxX, | ||||
|     logX, | ||||
|     expY, | ||||
|     sample = false, | ||||
|     xAxis = "number", | ||||
|   } = specOptions; | ||||
| 
 | ||||
|   let xScale = logX ? logXScale : linearXScale; | ||||
|   const dateTime = xAxis === "dateTime"; | ||||
| 
 | ||||
|   let xScale = dateTime ? timeXScale : logX ? logXScale : linearXScale; | ||||
| 
 | ||||
|   if (minX !== undefined && Number.isFinite(minX)) { | ||||
|     xScale = { ...xScale, domainMin: minX }; | ||||
|   } | ||||
|  | @ -105,9 +126,7 @@ export function buildVegaSpec( | |||
|       { | ||||
|         name: "position_scaled", | ||||
|         value: 0, | ||||
|         update: "position ? position[0] < 0 ? null : position[0] > width ? null : invert('xscale', position[0]) : null", | ||||
|         // "position ? position[0] < 0 ? 0 : position[0] > width ? 0 : 1 : 0",
 | ||||
| 
 | ||||
|         update: "position ? invert('xscale', position[0]) : null", | ||||
|       }, | ||||
|     ], | ||||
|     scales: [ | ||||
|  | @ -138,7 +157,6 @@ export function buildVegaSpec( | |||
|       }, | ||||
|     ], | ||||
|     marks: [ | ||||
| 
 | ||||
|       { | ||||
|         name: "all_distributions", | ||||
|         type: "group", | ||||
|  | @ -175,7 +193,7 @@ export function buildVegaSpec( | |||
|                     interpolate: { value: "linear" }, | ||||
|                     x: { | ||||
|                       scale: "xscale", | ||||
|                       field: "x", | ||||
|                       field: dateTime ? "dateTime" : "x", | ||||
|                     }, | ||||
|                     y: { | ||||
|                       scale: "yscale", | ||||
|  | @ -222,7 +240,7 @@ export function buildVegaSpec( | |||
|                   update: { | ||||
|                     x: { | ||||
|                       scale: "xscale", | ||||
|                       field: "x", | ||||
|                       field: dateTime ? "dateTime" : "x", | ||||
|                     }, | ||||
|                     y: { | ||||
|                       scale: "yscale", | ||||
|  | @ -251,13 +269,15 @@ export function buildVegaSpec( | |||
|                     }, | ||||
|                     size: [{ value: 100 }], | ||||
|                     tooltip: { | ||||
|                       signal: "{ probability: datum.y, value: datum.x }", | ||||
|                       signal: dateTime | ||||
|                         ? "{ probability: datum.y, value: datetime(datum.dateTime) }" | ||||
|                         : "{ probability: datum.y, value: datum.x }", | ||||
|                     }, | ||||
|                   }, | ||||
|                   update: { | ||||
|                     x: { | ||||
|                       scale: "xscale", | ||||
|                       field: "x", | ||||
|                       field: dateTime ? "dateTime" : "x", | ||||
|                       offset: 0.5, // if this is not included, the circles are slightly left of center.
 | ||||
|                     }, | ||||
|                     y: { | ||||
|  | @ -356,5 +376,37 @@ export function buildVegaSpec( | |||
|     }), | ||||
|   }; | ||||
| 
 | ||||
|   // include the band at the bottom if specified in the React component
 | ||||
|   if (sample) { | ||||
|     spec.marks?.push({ | ||||
|       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 ? "dateTime" : "x" }, | ||||
|               width: { value: 0.5 }, | ||||
| 
 | ||||
|               y: { value: 25, offset: { signal: "height" } }, | ||||
|               height: { value: 5 }, | ||||
|               fill: { value: "steelblue" }, | ||||
|               fillOpacity: { value: 0.8 }, | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       ], | ||||
|     }); | ||||
|   } | ||||
|   return spec; | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	Block a user