From 4f0495a65914ebde39af5558bd5f62d73b28fa13 Mon Sep 17 00:00:00 2001 From: Chris Campbell Date: Tue, 8 Oct 2024 15:15:31 -0700 Subject: [PATCH] feat: allow for adding reference plots to a comparison graph (#550) Fixes #549 --- examples/sample-check-tests/src/index.ts | 15 +++++- .../comparison/config/comparison-config.ts | 21 ++++++++ .../comparison/config/comparison-datasets.ts | 23 +++++++- packages/check-core/src/index.ts | 3 +- .../summary/check-summary-graph-box-vm.ts | 54 +++++++++++++------ .../compare/detail/compare-detail-box-vm.ts | 47 +++++++++++++--- .../graphs/comparison-graph-view.ts | 38 +++++-------- .../components/graphs/comparison-graph-vm.ts | 12 ++--- 8 files changed, 156 insertions(+), 57 deletions(-) diff --git a/examples/sample-check-tests/src/index.ts b/examples/sample-check-tests/src/index.ts index 7a306fa3..c7355f51 100644 --- a/examples/sample-check-tests/src/index.ts +++ b/examples/sample-check-tests/src/index.ts @@ -56,7 +56,20 @@ export function getConfigOptions(bundleL: Bundle, bundleR: Bundle, opts?: Config thresholds: [1, 5, 10], specs: comparisonSpecs, datasets: { - renamedDatasetKeys + renamedDatasetKeys, + referencePlotsForDataset: (dataset, scenario) => { + if (dataset.key === 'Model__output_x' && scenario.title.startsWith('Input A')) { + return [ + { + datasetKey: 'StaticData__static_s', + color: 'orange', + style: 'dashed', + lineWidth: 2 + } + ] + } + return [] + } } } } diff --git a/packages/check-core/src/comparison/config/comparison-config.ts b/packages/check-core/src/comparison/config/comparison-config.ts index 6ff7ca8e..fb3e2435 100644 --- a/packages/check-core/src/comparison/config/comparison-config.ts +++ b/packages/check-core/src/comparison/config/comparison-config.ts @@ -12,6 +12,20 @@ import { parseComparisonSpecs } from './parse/comparison-parser' import type { ComparisonResolvedDefs } from './resolve/comparison-resolver' import { resolveComparisonSpecs } from './resolve/comparison-resolver' +/** + * Describes an extra plot to be shown in a comparison graph. + */ +export interface ComparisonPlot { + /** The dataset key for the plot. */ + datasetKey: DatasetKey + /** The plot color. */ + color: string + /** The plot style. If undefined, defaults to 'normal'. */ + style?: 'normal' | 'dashed' + /** The plot line width, in px units. If undefined, a default width will be used. */ + lineWidth?: number +} + export interface ComparisonDatasetOptions { /** * The mapping of renamed dataset keys (old or "left" name as the map key, @@ -25,6 +39,13 @@ export interface ComparisonDatasetOptions { * datasets (for example, to omit datasets that are not relevant). */ datasetKeysForScenario?: (allDatasetKeys: DatasetKey[], scenario: ComparisonScenario) => DatasetKey[] + /** + * An optional function that allows for including additional reference plots + * on a comparison graph for a given dataset and scenario. By default, no + * additional reference plots are included, but if a custom function is + * provided, it can return an array of `ComparisonPlot` objects. + */ + referencePlotsForDataset?: (dataset: ComparisonDataset, scenario: ComparisonScenario) => ComparisonPlot[] /** * An optional function that allows for customizing the set of context graphs * that are shown for a given dataset and scenario. By default, all graphs in diff --git a/packages/check-core/src/comparison/config/comparison-datasets.ts b/packages/check-core/src/comparison/config/comparison-datasets.ts index cd430843..a274e452 100644 --- a/packages/check-core/src/comparison/config/comparison-datasets.ts +++ b/packages/check-core/src/comparison/config/comparison-datasets.ts @@ -4,7 +4,7 @@ import type { BundleGraphId, ModelSpec } from '../../bundle/bundle-types' import type { OutputVar } from '../../bundle/var-types' import type { DatasetKey } from '../../_shared/types' import type { ComparisonDataset, ComparisonScenario } from '../_shared/comparison-resolved-types' -import type { ComparisonDatasetOptions } from './comparison-config' +import type { ComparisonDatasetOptions, ComparisonPlot } from './comparison-config' /** * Provides access to the set of dataset definitions (`ComparisonDataset` instances) that are used @@ -30,6 +30,15 @@ export interface ComparisonDatasets { */ getDatasetKeysForScenario(scenario: ComparisonScenario): DatasetKey[] + /** + * Return the reference plots that should be shown in the comparison graph for the + * given dataset and scenario. + * + * @param datasetKey The key for the dataset. + * @param scenario The scenario for which the dataset will be displayed. + */ + getReferencePlotsForDataset(datasetKey: DatasetKey, scenario: ComparisonScenario): ComparisonPlot[] + /** * Return the context graph IDs that should be shown for the given dataset and scenario. * @@ -157,6 +166,18 @@ class ComparisonDatasetsImpl implements ComparisonDatasets { } } + // from ComparisonDatasets interface + getReferencePlotsForDataset(datasetKey: DatasetKey, scenario: ComparisonScenario): ComparisonPlot[] { + if (this.datasetOptions?.referencePlotsForDataset !== undefined) { + // Delegate to the custom function + const dataset = this.getDataset(datasetKey) + if (dataset !== undefined) { + return this.datasetOptions.referencePlotsForDataset(dataset, scenario) + } + } + return [] + } + // from ComparisonDatasets interface getContextGraphIdsForDataset(datasetKey: DatasetKey, scenario: ComparisonScenario): BundleGraphId[] { const dataset = this.getDataset(datasetKey) diff --git a/packages/check-core/src/index.ts b/packages/check-core/src/index.ts index be144b11..5db15865 100644 --- a/packages/check-core/src/index.ts +++ b/packages/check-core/src/index.ts @@ -81,7 +81,8 @@ export * from './comparison/config/comparison-spec-types' export type { ComparisonConfig, ComparisonDatasetOptions, - ComparisonOptions + ComparisonOptions, + ComparisonPlot } from './comparison/config/comparison-config' export type { ComparisonScenarios } from './comparison/config/comparison-scenarios' export type { ComparisonDatasets } from './comparison/config/comparison-datasets' diff --git a/packages/check-ui-shell/src/components/check/summary/check-summary-graph-box-vm.ts b/packages/check-ui-shell/src/components/check/summary/check-summary-graph-box-vm.ts index 3914cffe..f7293d76 100644 --- a/packages/check-ui-shell/src/components/check/summary/check-summary-graph-box-vm.ts +++ b/packages/check-ui-shell/src/components/check/summary/check-summary-graph-box-vm.ts @@ -14,7 +14,12 @@ import type { ScenarioSpec } from '@sdeverywhere/check-core' -import type { ComparisonGraphViewModel, PlotStyle, Point, RefPlot } from '../../graphs/comparison-graph-vm' +import type { + ComparisonGraphPlot, + ComparisonGraphPlotStyle, + ComparisonGraphViewModel, + Point +} from '../../graphs/comparison-graph-vm' import { pointsFromDataset } from '../../graphs/comparison-graph-vm' let requestId = 1 @@ -219,26 +224,45 @@ export class CheckSummaryGraphBoxViewModel { } } - // Add reference lines - const refPlots: RefPlot[] = [] + // Add the primary plot + const plots: ComparisonGraphPlot[] = [] + plots.push({ + points: primaryPoints, + color: 'deepskyblue', + style: 'normal' + }) - const addRefPlot = (op: CheckPredicateOp, style: PlotStyle | undefined, delta = 0) => { + // Add the primary and reference plots + const addRefPlot = ( + op: CheckPredicateOp, + style: ComparisonGraphPlotStyle | undefined, + delta = 0, + lineWidth?: number + ) => { + const color = 'green' + if (lineWidth === undefined) { + lineWidth = 1 + } const constantRef = this.opConstantRefs.get(op) if (constantRef !== undefined) { if (minPredTime === maxPredTime) { // Add a single point - refPlots.push({ + plots.push({ points: [{ x: minPredTime, y: constantRef + delta }], - style + color, + style, + lineWidth }) } else { // Add a line segment for the constant - refPlots.push({ + plots.push({ points: [ { x: minPredTime, y: constantRef + delta }, { x: maxPredTime, y: constantRef + delta } ], - style + color, + style, + lineWidth }) } return @@ -254,9 +278,11 @@ export class CheckSummaryGraphBoxViewModel { return { x: p.x, y: p.y + delta } }) } - refPlots.push({ + plots.push({ points: filtered, - style + color, + style, + lineWidth }) } } @@ -271,21 +297,19 @@ export class CheckSummaryGraphBoxViewModel { addRefPlot('gte', hasLt ? 'fill-to-next' : 'fill-above') addRefPlot('lt', hasGt ? 'normal' : 'fill-below') addRefPlot('lte', hasGt ? 'normal' : 'fill-below') - addRefPlot('eq', 'wide') + addRefPlot('eq', 'normal', 0, 5) // Handle `approx` specially by adding two reference lines (one for the // lower bound and one for the upper bound) const tolerance = this.predicateReport.tolerance || 0.1 addRefPlot('approx', 'fill-to-next', -tolerance) addRefPlot('approx', 'normal', tolerance) - addRefPlot('approx', 'dashed') + addRefPlot('approx', 'dashed', 0) // Create the comparison graph view model const comparisonGraphViewModel: ComparisonGraphViewModel = { key: this.baseRequestKey, - refPlots, - pointsL: [], - pointsR: primaryPoints, + plots, xMin: undefined, xMax: undefined } diff --git a/packages/check-ui-shell/src/components/compare/detail/compare-detail-box-vm.ts b/packages/check-ui-shell/src/components/compare/detail/compare-detail-box-vm.ts index 72a7fab3..ab410786 100644 --- a/packages/check-ui-shell/src/components/compare/detail/compare-detail-box-vm.ts +++ b/packages/check-ui-shell/src/components/compare/detail/compare-detail-box-vm.ts @@ -18,7 +18,12 @@ import { diffDatasets } from '@sdeverywhere/check-core' import { getBucketIndex } from '../_shared/buckets' import { datasetSpan } from '../_shared/spans' -import type { ComparisonGraphViewModel, Point } from '../../graphs/comparison-graph-vm' +import type { + ComparisonGraphPlot, + ComparisonGraphPlotStyle, + ComparisonGraphViewModel, + Point +} from '../../graphs/comparison-graph-vm' import { pointsFromDataset } from '../../graphs/comparison-graph-vm' let requestId = 1 @@ -69,11 +74,15 @@ export class CompareDetailBoxViewModel { } this.dataRequested = true + const datasetKeys: DatasetKey[] = [this.datasetKey] + const refPlots = this.comparisonConfig.datasets.getReferencePlotsForDataset(this.datasetKey, this.scenario) + datasetKeys.push(...refPlots.map(plot => plot.datasetKey)) + this.dataCoordinator.requestDatasetMaps( this.requestKey, this.scenario.specL, this.scenario.specR, - [this.datasetKey], + datasetKeys, (datasetMapL, datasetMapR) => { if (!this.dataRequested) { return @@ -141,6 +150,31 @@ export class CompareDetailBoxViewModel { const pointsL = pointsFromDataset(datasetMapL?.get(this.datasetKey)) const pointsR = pointsFromDataset(datasetMapR?.get(this.datasetKey)) + const plots: ComparisonGraphPlot[] = [] + function addPlot(points: Point[], color: string, style?: ComparisonGraphPlotStyle, lineWidth?: number): void { + plots.push({ + points, + color, + style: style || 'normal', + lineWidth + }) + } + + // Add the primary plots. We add the right data points first so that they are drawn + // on top of the left data points. + // TODO: Use the colors defined in CSS (or make them configurable through other means); + // these should not be hardcoded here + addPlot(pointsR, 'deepskyblue') + addPlot(pointsL, 'crimson') + + // Add the secondary/reference plots + // TODO: Currently these are always shown behind the primary plots; might need to make + // this configurable + for (const refPlot of refPlots) { + const points = pointsFromDataset(datasetMapR?.get(refPlot.datasetKey)) + addPlot(points, refPlot.color, refPlot.style, refPlot.lineWidth) + } + // Find the min and max y values for all datasets let yMin = Number.POSITIVE_INFINITY let yMax = Number.NEGATIVE_INFINITY @@ -154,8 +188,9 @@ export class CompareDetailBoxViewModel { } } } - setExtents(pointsL) - setExtents(pointsR) + for (const plot of plots) { + setExtents(plot.points) + } this.writableYRange.set({ min: yMin, max: yMax @@ -164,9 +199,7 @@ export class CompareDetailBoxViewModel { // Create the graph view model const comparisonGraphViewModel: ComparisonGraphViewModel = { key: this.requestKey, - refPlots: [], - pointsL, - pointsR, + plots, xMin, xMax, yMin: this.activeYMin, diff --git a/packages/check-ui-shell/src/components/graphs/comparison-graph-view.ts b/packages/check-ui-shell/src/components/graphs/comparison-graph-view.ts index 323ad1da..3261ec13 100644 --- a/packages/check-ui-shell/src/components/graphs/comparison-graph-view.ts +++ b/packages/check-ui-shell/src/components/graphs/comparison-graph-view.ts @@ -2,7 +2,7 @@ import type { ChartDataSets } from 'chart.js' import { Chart } from 'chart.js' -import type { ComparisonGraphViewModel, PlotStyle, Point } from './comparison-graph-vm' +import type { ComparisonGraphPlot, ComparisonGraphViewModel, Point } from './comparison-graph-vm' const gridColor = '#444' const fontFamily = 'Roboto Condensed' @@ -119,20 +119,10 @@ function createChart(canvas: HTMLCanvasElement, viewModel: ComparisonGraphViewMo } let dataMaxX = Number.NEGATIVE_INFINITY - function addPlot(points: Point[], color: string, style?: PlotStyle): void { - const normalWidth = 3 - let borderWidth = normalWidth - if (style) { - // Use thin reference lines - borderWidth = 1 - } - + function addPlot(plot: ComparisonGraphPlot): void { let borderDash: number[] let fill: string | boolean = false - switch (style) { - case 'wide': - borderWidth = normalWidth * 2 - break + switch (plot.style) { case 'dashed': borderDash = [8, 2] break @@ -152,28 +142,28 @@ function createChart(canvas: HTMLCanvasElement, viewModel: ComparisonGraphViewMo let backgroundColor = undefined if (fill !== false) { // Make the fill less translucent when there is only a single point - const opacity = points.length > 1 ? 0.1 : 0.3 + const opacity = plot.points.length > 1 ? 0.1 : 0.3 backgroundColor = `rgba(0, 128, 0, ${opacity})` } let pointRadius = 0 let pointBackgroundColor = undefined - if (points.length === 1 && style !== 'dashed') { + if (plot.points.length === 1 && plot.style !== 'dashed') { pointRadius = 5 - pointBackgroundColor = color + pointBackgroundColor = plot.color } // Find the maximum x value in the datasets - for (const p of points) { + for (const p of plot.points) { if (p.x > dataMaxX) { dataMaxX = p.x } } datasets.push({ - data: points, - borderColor: color, - borderWidth, + data: plot.points, + borderColor: plot.color, + borderWidth: plot.lineWidth !== undefined ? plot.lineWidth : 3, borderDash, backgroundColor, fill, @@ -187,12 +177,8 @@ function createChart(canvas: HTMLCanvasElement, viewModel: ComparisonGraphViewMo // Add the right data points first so that they are drawn on top of the // left data points - // TODO: Use the colors defined in CSS (or make them configurable through other means); - // these should not be hardcoded here - addPlot(viewModel.pointsR, 'deepskyblue') - addPlot(viewModel.pointsL, 'crimson') - for (const refPlot of viewModel.refPlots) { - addPlot(refPlot.points, 'green', refPlot.style || 'normal') + for (const plot of viewModel.plots) { + addPlot(plot) } // Customize the x-axis range diff --git a/packages/check-ui-shell/src/components/graphs/comparison-graph-vm.ts b/packages/check-ui-shell/src/components/graphs/comparison-graph-vm.ts index e4f571fe..9cabebd7 100644 --- a/packages/check-ui-shell/src/components/graphs/comparison-graph-vm.ts +++ b/packages/check-ui-shell/src/components/graphs/comparison-graph-vm.ts @@ -7,18 +7,18 @@ export interface Point { y: number } -export type PlotStyle = 'normal' | 'wide' | 'dashed' | 'fill-to-next' | 'fill-above' | 'fill-below' +export type ComparisonGraphPlotStyle = 'normal' | 'dashed' | 'fill-to-next' | 'fill-above' | 'fill-below' -export interface RefPlot { +export interface ComparisonGraphPlot { points: Point[] - style: PlotStyle + color: string + style?: ComparisonGraphPlotStyle + lineWidth?: number } export interface ComparisonGraphViewModel { key: string - refPlots: RefPlot[] - pointsL: Point[] - pointsR: Point[] + plots: ComparisonGraphPlot[] xMin?: number xMax?: number yMin?: number