Skip to content

Commit

Permalink
feat: add view option for consistent y-axis limits across a row of co…
Browse files Browse the repository at this point in the history
…mparison graphs (#548)

Fixes #547
  • Loading branch information
chrispcampbell authored Oct 8, 2024
1 parent 5a76a03 commit d8ce779
Show file tree
Hide file tree
Showing 10 changed files with 214 additions and 22 deletions.
8 changes: 8 additions & 0 deletions packages/check-ui-shell/src/_shared/user-prefs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Copyright (c) 2024 Climate Interactive / New Venture Fund

import type { Readable } from 'svelte/store'

export interface UserPrefs {
zoom: Readable<number>
consistentYRange: Readable<boolean>
}
24 changes: 22 additions & 2 deletions packages/check-ui-shell/src/app-vm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { get, writable } from 'svelte/store'
import type { ComparisonSummary, SuiteSummary } from '@sdeverywhere/check-core'
import { checkReportFromSummary, comparisonSummaryFromReport, runSuite } from '@sdeverywhere/check-core'

import { localStorageWritableBoolean, localStorageWritableNumber } from './_shared/stores'
import type { UserPrefs } from './_shared/user-prefs'

import type { AppModel } from './model/app-model'

import type { ComparisonGroupingKind } from './components/compare/_shared/comparison-grouping-kind'
Expand All @@ -31,6 +34,7 @@ export class AppViewModel {
public readonly checksInProgress: Readable<boolean>
private readonly writableProgress: Writable<string>
public readonly progress: Readable<string>
public readonly userPrefs: UserPrefs
public readonly headerViewModel: HeaderViewModel
public summaryViewModel: SummaryViewModel
private cancelRunSuite: () => void
Expand All @@ -48,8 +52,23 @@ export class AppViewModel {
this.progress = this.writableProgress

// Show the "Simplify Scenarios" checkbox if we run checks in the browser
const includeSimplifyScenarios = suiteSummary === undefined
this.headerViewModel = createHeaderViewModel(appModel.config.comparison, includeSimplifyScenarios)
let simplifyScenarios: Writable<boolean>
if (suiteSummary === undefined) {
simplifyScenarios = localStorageWritableBoolean('sde-check-simplify-scenarios', false)
} else {
simplifyScenarios = undefined
}

// Create the `UserPrefs` object that is passed down to the component hierarchy
const zoom = localStorageWritableNumber('sde-check-graph-zoom', 1)
const consistentYRange = localStorageWritableBoolean('sde-check-consistent-y-range', false)
this.userPrefs = {
zoom,
consistentYRange
}

// Create the header view model
this.headerViewModel = createHeaderViewModel(appModel.config.comparison, simplifyScenarios, zoom, consistentYRange)
}

runTestSuite(): void {
Expand Down Expand Up @@ -145,6 +164,7 @@ export class AppViewModel {
return createCompareDetailViewModel(
this.appModel.config.comparison,
this.appModel.comparisonDataCoordinator,
this.userPrefs,
groupSummary,
viewGroup,
view,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { diffDatasets } from '@sdeverywhere/check-core'
import { getBucketIndex } from '../_shared/buckets'
import { datasetSpan } from '../_shared/spans'

import type { ComparisonGraphViewModel } from '../../graphs/comparison-graph-vm'
import type { ComparisonGraphViewModel, Point } from '../../graphs/comparison-graph-vm'
import { pointsFromDataset } from '../../graphs/comparison-graph-vm'

let requestId = 1
Expand All @@ -30,10 +30,21 @@ export interface CompareDetailBoxContent {
comparisonGraphViewModel: ComparisonGraphViewModel
}

export interface AxisRange {
min: number
max: number
}

export class CompareDetailBoxViewModel {
public readonly requestKey: string
private localContent: CompareDetailBoxContent
private readonly writableContent: Writable<CompareDetailBoxContent>
public readonly content: Readable<CompareDetailBoxContent>
private readonly writableYRange: Writable<AxisRange>
public readonly yRange: Readable<AxisRange>
private activeYMin: number
private activeYMax: number

private dataRequested = false
private dataLoaded = false

Expand All @@ -48,6 +59,8 @@ export class CompareDetailBoxViewModel {
this.requestKey = `detail-box::${requestId++}::${scenario.key}::${datasetKey}`
this.writableContent = writable(undefined)
this.content = this.writableContent
this.writableYRange = writable(undefined)
this.yRange = this.writableYRange
}

requestData(): void {
Expand Down Expand Up @@ -124,28 +137,68 @@ export class CompareDetailBoxViewModel {
}
}

// Extract the data points
const pointsL = pointsFromDataset(datasetMapL?.get(this.datasetKey))
const pointsR = pointsFromDataset(datasetMapR?.get(this.datasetKey))

// Find the min and max y values for all datasets
let yMin = Number.POSITIVE_INFINITY
let yMax = Number.NEGATIVE_INFINITY
function setExtents(points: Point[]): void {
for (const p of points) {
if (p.y < yMin) {
yMin = p.y
}
if (p.y > yMax) {
yMax = p.y
}
}
}
setExtents(pointsL)
setExtents(pointsR)
this.writableYRange.set({
min: yMin,
max: yMax
})

// Create the graph view model
const comparisonGraphViewModel: ComparisonGraphViewModel = {
key: this.requestKey,
refPlots: [],
pointsL: pointsFromDataset(datasetMapL?.get(this.datasetKey)),
pointsR: pointsFromDataset(datasetMapR?.get(this.datasetKey)),
pointsL,
pointsR,
xMin,
xMax
xMax,
yMin: this.activeYMin,
yMax: this.activeYMax
}

this.writableContent.set({
this.localContent = {
bucketClass: `bucket-border-${bucketIndex !== undefined ? bucketIndex : 'undefined'}`,
message,
diffReport,
comparisonGraphViewModel
})
}
this.writableContent.set(this.localContent)
this.dataLoaded = true
}
)
}

updateYAxisRange(yRange: AxisRange | undefined): void {
this.activeYMin = yRange?.min
this.activeYMax = yRange?.max
if (this.localContent) {
const graphViewModel = this.localContent.comparisonGraphViewModel
graphViewModel.yMin = this.activeYMin
graphViewModel.yMax = this.activeYMax
graphViewModel.onUpdated?.()
}
}

clearData(): void {
if (this.dataRequested) {
this.localContent = undefined
this.writableContent.set(undefined)
if (!this.dataLoaded) {
this.dataCoordinator.cancelRequest(this.requestKey)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
// Copyright (c) 2021-2022 Climate Interactive / New Venture Fund

import { derived, type Readable } from 'svelte/store'

import type {
BundleGraphSpec,
ComparisonConfig,
ComparisonDataCoordinator,
ComparisonScenario
} from '@sdeverywhere/check-core'

import type { UserPrefs } from '../../../_shared/user-prefs'

import { ContextGraphViewModel } from '../../graphs/context-graph-vm'

import { CompareDetailBoxViewModel } from './compare-detail-box-vm'
import { CompareDetailBoxViewModel, type AxisRange } from './compare-detail-box-vm'
import type { ComparisonDetailItem } from './compare-detail-item'

export interface CompareDetailContextGraphRowViewModel {
Expand All @@ -27,6 +31,7 @@ export interface CompareDetailRowViewModel {
export function createCompareDetailRowViewModel(
comparisonConfig: ComparisonConfig,
dataCoordinator: ComparisonDataCoordinator,
userPrefs: UserPrefs,
kind: 'scenarios' | 'datasets',
title: string | undefined,
subtitle: string | undefined,
Expand Down Expand Up @@ -57,6 +62,45 @@ export function createCompareDetailRowViewModel(
)
}

// This resolves the overall min and max values for all boxes; this will be updated
// asynchronously (N times for a row of N boxes) as the data is loaded for each box
const yRangeForRow: Readable<AxisRange> = derived(
boxes.map(b => b.yRange),
yRanges => {
let min = Number.POSITIVE_INFINITY
let max = Number.NEGATIVE_INFINITY
for (const range of yRanges) {
if (range?.min < min) {
min = range.min
}
if (range?.max > max) {
max = range.max
}
}
return {
min,
max
}
}
)

// This resolves to either `yRangeForRow` or undefined, depending on whether the
// user has the "Consistent Y-Axis Range" checkbox enabled
const activeYRangeForRow: Readable<AxisRange | undefined> = derived(
[userPrefs.consistentYRange, yRangeForRow],
([$enabled, $yRange]) => {
return $enabled ? $yRange : undefined
}
)

// Update the boxes to use the active y-axis range values
// TODO: Unsubscribe when the component is unmounted
activeYRangeForRow.subscribe(yRange => {
for (const box of boxes) {
box.updateYAxisRange(yRange)
}
})

return {
title,
subtitle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import type {
} from '@sdeverywhere/check-core'
import { diffGraphs } from '@sdeverywhere/check-core'

import type { UserPrefs } from '../../../_shared/user-prefs'

import { getAnnotationsForDataset, getAnnotationsForScenario } from '../_shared/annotations'
import { getBucketIndex } from '../_shared/buckets'
import type { ComparisonGroupingKind } from '../_shared/comparison-grouping-kind'
Expand Down Expand Up @@ -72,6 +74,7 @@ export interface CompareDetailViewModel {
export function createCompareDetailViewModel(
comparisonConfig: ComparisonConfig,
dataCoordinator: ComparisonDataCoordinator,
userPrefs: UserPrefs,
groupSummary: ComparisonGroupSummary,
viewGroup: ComparisonViewGroup | undefined,
view: ComparisonView | undefined,
Expand All @@ -83,6 +86,7 @@ export function createCompareDetailViewModel(
return createCompareDetailViewModelForDataset(
comparisonConfig,
dataCoordinator,
userPrefs,
groupSummary,
previousRowIndex,
nextRowIndex
Expand All @@ -91,6 +95,7 @@ export function createCompareDetailViewModel(
return createCompareDetailViewModelForScenario(
comparisonConfig,
dataCoordinator,
userPrefs,
groupSummary,
viewGroup,
view,
Expand All @@ -105,6 +110,7 @@ export function createCompareDetailViewModel(
function createCompareDetailViewModelForDataset(
comparisonConfig: ComparisonConfig,
dataCoordinator: ComparisonDataCoordinator,
userPrefs: UserPrefs,
groupSummary: ComparisonGroupSummary,
previousRowIndex: number | undefined,
nextRowIndex: number | undefined
Expand Down Expand Up @@ -157,6 +163,7 @@ function createCompareDetailViewModelForDataset(
const detailRow = createCompareDetailRowViewModel(
comparisonConfig,
dataCoordinator,
userPrefs,
'scenarios',
group.title,
undefined, // TODO: Subtitle?
Expand Down Expand Up @@ -190,6 +197,7 @@ function createCompareDetailViewModelForDataset(
function createCompareDetailViewModelForScenario(
comparisonConfig: ComparisonConfig,
dataCoordinator: ComparisonDataCoordinator,
userPrefs: UserPrefs,
groupSummary: ComparisonGroupSummary,
viewGroup: ComparisonViewGroup | undefined,
view: ComparisonView | undefined,
Expand Down Expand Up @@ -263,6 +271,7 @@ function createCompareDetailViewModelForScenario(
const rowViewModel = createCompareDetailRowViewModel(
comparisonConfig,
dataCoordinator,
userPrefs,
'datasets',
title,
subtitle,
Expand Down
Loading

0 comments on commit d8ce779

Please sign in to comment.