diff --git a/scripts/generateOptions.ts b/scripts/generateOptions.ts
index 4611d3f1..0201defd 100644
--- a/scripts/generateOptions.ts
+++ b/scripts/generateOptions.ts
@@ -57,7 +57,7 @@ let activityTypeOpts: Option[] = [];
if (activityTypeValue && !activityTypeOpts.find(o => o.value === activityTypeValue)) {
activityTypeOpts.push({
value: activityTypeValue,
- label: titleCase(activityTypeValue)
+ label: titleCase(activityTypeValue)!
});
}
}
diff --git a/src/assets/styles/main.css b/src/assets/styles/main.css
index 5e64a62b..c7510846 100644
--- a/src/assets/styles/main.css
+++ b/src/assets/styles/main.css
@@ -44,8 +44,9 @@
border: 1px solid black;
border-radius: 0.25em;
padding: 0.5em;
- opacity: 0.8;
+ opacity: 0.9;
position: relative;
top: 40px;
right: 100px;
+ width: max-content;
}
diff --git a/src/composables/usePlotTooltips.ts b/src/composables/usePlotTooltips.ts
index 4a45b6d9..7cb04c9f 100644
--- a/src/composables/usePlotTooltips.ts
+++ b/src/composables/usePlotTooltips.ts
@@ -1,39 +1,91 @@
import { useAppStore } from "@/stores/appStore";
import { useColorStore } from "@/stores/colorStore";
-import { Axis, type PointWithMetadata } from "@/types";
+import { useDataStore } from "@/stores/dataStore";
+import { Axis, SummaryTableColumn, type LineMetadata, type PointWithMetadata } from "@/types";
import { dimensionOptionLabel } from "@/utils/options";
import sentenceCase from "@/utils/sentenceCase";
export default () => {
const colorStore = useColorStore();
const appStore = useAppStore();
+ const dataStore = useDataStore();
+
+ const convertToScientificNotation = (num: number): string => {
+ if (!appStore.logScaleEnabled) {
+ return num.toFixed(2);
+ }
+ if (num === 0) return "0";
+ const exponent = Math.floor(Math.log10(Math.abs(num)));
+ const coefficient = (num / Math.pow(10, exponent)).toFixed(2);
+ return `${coefficient} × 10${exponent}`;
+ };
+
+ // Find the summary table row whose values for the plot row and band
+ // dimensions (and column, if set) match the values of the tooltip point metadata
+ const getSummaryDataRow = (metadata: LineMetadata) => {
+ return dataStore.summaryTableData.find(d => {
+ return Object.entries(appStore.dimensions).every(([axis, dim]) => {
+ return !dim || d[dim] === metadata?.[axis as Axis];
+ });
+ });
+ };
// Generate HTML for tooltips on ridgeline plot points.
+ // This callback is passed to skadi-chart, and is invoked when hovering over the chart.
const tooltipCallback = (point: PointWithMetadata) => {
if (!point.metadata) return "";
const { strokeColor } = colorStore.getColorsForLine(point.metadata)
- const { colorDimension } = colorStore;
+ const { colorAxis, colorDimension } = colorStore;
const { dimensions } = appStore;
- const rowOptionLabel = dimensionOptionLabel(colorDimension, point.metadata[colorStore.colorAxis]);
+ const valueForColorDimension = point.metadata[colorAxis];
+
+ const colorDimensionLabel = dimensionOptionLabel(colorDimension, valueForColorDimension);
+ const rowOptionLabel = dimensionOptionLabel(dimensions.row, point.metadata[Axis.ROW]);
const columnOptionLabel = dimensionOptionLabel(dimensions.column, point.metadata[Axis.COLUMN]);
+ const summaryDataRow = getSummaryDataRow(point.metadata);
+
+ // NB regardless of whether log scale is enabled, the summary table data are provided in non-log scale.
+ const [mean, ciLower, ciUpper] = [
+ SummaryTableColumn.MEAN,
+ SummaryTableColumn.CI_LOWER,
+ SummaryTableColumn.CI_UPPER,
+ ].map(col => summaryDataRow?.[col]);
+
+ let ciLowerStr;
+ let ciUpperStr;
+ if (appStore.logScaleEnabled) {
+ ciUpperStr = convertToScientificNotation(ciUpper!);
+ ciLowerStr = convertToScientificNotation(ciLower!);
+ } else {
+ const includePositiveSign = ciLower! < 0;
+ const ciUpperSign = (includePositiveSign && ciUpper! > 0) ? '+' : '';
+ ciUpperStr = `${ciUpperSign}${ciUpper?.toFixed(2)}`;
+ ciLowerStr = ciLower?.toFixed(2);
+ }
+
+ const meanStr = appStore.logScaleEnabled ? convertToScientificNotation(mean!) : mean?.toFixed(2)
+
return `
-
-
●
-
-
- ${sentenceCase(colorDimension)}: ${rowOptionLabel}
-
- ${dimensions.column ? `
- ${sentenceCase(dimensions.column)}: ${columnOptionLabel}
- ` : ''}
+
+ ●
+
+ ${sentenceCase(colorDimension)}: ${colorDimensionLabel}
- Tooltip content is TODO. VIMC-9196
+ ${dimensions.row !== colorDimension ? `
+ ${sentenceCase(dimensions.row)}: ${rowOptionLabel}
+ ` : ''}
+ ${dimensions.column && dimensions.column !== colorDimension ? `
+ ${sentenceCase(dimensions.column)}: ${columnOptionLabel}
+ ` : ''}
+
+ Mean: ${meanStr}
+
- Will show the median/mean values and 95% confidence interval for the whole line.
+ 95% confidence interval: ${ciLowerStr} — ${ciUpperStr}
`
}
diff --git a/src/stores/dataStore.ts b/src/stores/dataStore.ts
index b2be08ba..a27dea15 100644
--- a/src/stores/dataStore.ts
+++ b/src/stores/dataStore.ts
@@ -1,8 +1,9 @@
import { debounce } from "perfect-debounce";
-import { computed, ref, shallowRef, watch } from "vue";
+import { computed, ref, shallowRef, watch, type ShallowRef } from "vue";
import { defineStore } from "pinia";
import { useAppStore } from "@/stores/appStore";
-import { type HistDataRow, Dimension, LocResolution } from "@/types";
+import { type HistDataRow, type SummaryTableDataRow, Dimension, LocResolution } from "@/types";
+import { globalOption } from "@/utils/options";
export const dataDir = `./data/json`
@@ -11,13 +12,14 @@ export const useDataStore = defineStore("data", () => {
const fetchErrors = ref<{ e: Error, message: string }[]>([]);
const histogramData = shallowRef
([]);
- const histogramDataCache: Record = {};
+ const histogramCache: Record = {};
+ const summaryTableData = shallowRef([]);
+ const summaryTableCache: Record = {};
const isLoading = ref(true);
- const histogramDataPaths = computed(() => {
- // When we are using multiple geographical resolutions, we need to load multiple data files, to be merged together later.
+ const constructFilenames = (dataType: "hist_counts" | "summary_table"): string[] => {
return appStore.geographicalResolutions.map((geog) => {
- const fileNameParts = ["hist_counts", appStore.burdenMetric, "disease"];
+ const fileNameParts = [dataType, appStore.burdenMetric, "disease"];
// NB files containing 'global' data simply omit location from the file name (as they have no location stratification).
if (geog === LocResolution.SUBREGION) {
fileNameParts.push(LocResolution.SUBREGION);
@@ -28,26 +30,33 @@ export const useDataStore = defineStore("data", () => {
if (geog === LocResolution.COUNTRY) {
fileNameParts.push(LocResolution.COUNTRY);
}
- if (appStore.logScaleEnabled) {
+ if (dataType === "hist_counts" && appStore.logScaleEnabled) {
+ // Log scale is not applicable for summary tables, so does not appear in the filenames.
fileNameParts.push("log");
}
- return `${fileNameParts.join("_")}.json`
+ return `${fileNameParts.join("_")}.json`;
});
- });
+ }
- // Fetch and parse multiple JSONs, and merge together all data.
- const loadDataFromPaths = async (paths: string[]) => {
- isLoading.value = true;
- fetchErrors.value = [];
- await Promise.all(paths.map(async (path) => {
- if (!histogramDataCache[path]) {
+ const histFilenames = computed(() => constructFilenames("hist_counts"));
+ const summaryTableFilenames = computed(() => constructFilenames("summary_table"));
+
+ const loadData = async (
+ filenames: string[],
+ cache: Record,
+ ref: ShallowRef,
+ ) => {
+ // When we are using multiple geographical resolutions, we load multiple data files, to be merged together later.
+ await Promise.all(filenames.map(async (filename) => {
+ if (!cache[filename]) {
+ const path = `${dataDir}/${filename}`;
try {
- const response = await fetch(`${dataDir}/${path}`);
+ const response = await fetch(path);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const rows = await response.json();
- histogramDataCache[path] = rows;
+ cache[filename] = rows;
return rows;
} catch (error) {
fetchErrors.value.push({ e: error as Error, message: `Error loading data from path: ${path}. ${error}` });
@@ -55,32 +64,49 @@ export const useDataStore = defineStore("data", () => {
}
}));
- histogramData.value = paths.flatMap((path) => histogramDataCache[path] || []).map((row) => {
+ // Merge data fetched from multiple files into one array.
+ ref.value = filenames.flatMap((filename) => {
+ return (cache[filename] || []) as T[];
+ }).map((row) => {
// Collapse all geographic columns into one 'location' column
- if (row[LocResolution.COUNTRY]) {
- row[Dimension.LOCATION] = row[LocResolution.COUNTRY];
- delete row[LocResolution.COUNTRY];
- } else if (row[LocResolution.SUBREGION]) {
- row[Dimension.LOCATION] = row[LocResolution.SUBREGION];
- delete row[LocResolution.SUBREGION];
+ const [country, subregion] = [row[LocResolution.COUNTRY], row[LocResolution.SUBREGION]];
+ const newRow = { ...row };
+ if (country) {
+ newRow[Dimension.LOCATION] = country;
+ delete newRow[LocResolution.COUNTRY];
+ } else if (subregion) {
+ newRow[Dimension.LOCATION] = subregion;
+ delete newRow[LocResolution.SUBREGION];
+ } else {
+ newRow[Dimension.LOCATION] = globalOption.value;
}
- return row;
+ return newRow;
});
isLoading.value = false;
};
- const doLoadData = debounce(async () => {
- await loadDataFromPaths(histogramDataPaths.value);
+ const loadAllData = async () => {
+ isLoading.value = true;
+ fetchErrors.value = [];
+ await Promise.all([
+ loadData(histFilenames.value, histogramCache, histogramData),
+ loadData(summaryTableFilenames.value, summaryTableCache, summaryTableData),
+ ]);
+ isLoading.value = false;
+ };
+
+ const debouncedLoadAllData = debounce(async () => {
+ await loadAllData();
}, 25)
- watch(histogramDataPaths, async (_oldPaths, newPaths) => {
- if (newPaths) {
- doLoadData();
+ watch([histFilenames, summaryTableFilenames], async (_oldPaths, newPaths) => {
+ if (newPaths.length) {
+ debouncedLoadAllData();
} else {
- // This is the first time histDataPaths is calculated, so don't debounce.
- await loadDataFromPaths(histogramDataPaths.value);
+ // This is the first time the filenames are computed, so don't debounce.
+ await loadAllData();
}
}, { immediate: true });
- return { histogramData, fetchErrors, isLoading };
+ return { fetchErrors, isLoading, histogramData, summaryTableData };
});
diff --git a/src/types.ts b/src/types.ts
index 1b54eef7..78d741ff 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -24,14 +24,16 @@ export enum HistColumn {
COUNTS = "Counts",
}
-type DataRow = Record;
-export type SummaryTableDataRow = DataRow & {
- [Dimension.DISEASE]: string;
- [LocResolution.COUNTRY]?: string;
- [LocResolution.SUBREGION]?: string;
-};
-export type HistDataRow = DataRow & {
+export enum SummaryTableColumn {
+ MEAN = "mean_value",
+ MEDIAN = "median_value",
+ CI_LOWER = "lower_95",
+ CI_UPPER = "upper_95",
+}
+
+export type HistDataRow = {
[Dimension.DISEASE]: string;
+ [Dimension.ACTIVITY_TYPE]?: string;
[LocResolution.COUNTRY]?: string;
[LocResolution.SUBREGION]?: string;
[Dimension.LOCATION]?: string;
@@ -39,6 +41,17 @@ export type HistDataRow = DataRow & {
[HistColumn.UPPER_BOUND]: number;
[HistColumn.COUNTS]: number;
};
+export type SummaryTableDataRow = {
+ [Dimension.DISEASE]: string;
+ [Dimension.ACTIVITY_TYPE]?: string;
+ [LocResolution.COUNTRY]?: string;
+ [LocResolution.SUBREGION]?: string;
+ [Dimension.LOCATION]?: string;
+ [SummaryTableColumn.MEAN]: number;
+ [SummaryTableColumn.MEDIAN]: number;
+ [SummaryTableColumn.CI_LOWER]: number;
+ [SummaryTableColumn.CI_UPPER]: number;
+}
export type Option = { label: string; value: string };
diff --git a/src/utils/fileParse.ts b/src/utils/fileParse.ts
index 2f9ecf2d..4082731a 100644
--- a/src/utils/fileParse.ts
+++ b/src/utils/fileParse.ts
@@ -3,7 +3,7 @@ import { globalOption } from "./options";
// Get a data row's category for some categorical axis.
export const getDimensionCategoryValue = (dim: Dimension | null, dataRow: HistDataRow): string => {
- const value = dataRow[dim ?? ""] as string;
+ const value = dataRow[dim as keyof HistDataRow] as string;
if (dim === Dimension.LOCATION && !value) {
// A missing column for the location dimension implies 'global' category.
return globalOption.value;
diff --git a/tests/unit/composables/usePlotTooltips.spec.ts b/tests/unit/composables/usePlotTooltips.spec.ts
index e29006dd..020e6225 100644
--- a/tests/unit/composables/usePlotTooltips.spec.ts
+++ b/tests/unit/composables/usePlotTooltips.spec.ts
@@ -6,7 +6,8 @@ import { nextTick } from "vue";
import usePlotTooltips from '@/composables/usePlotTooltips';
import { useAppStore } from '@/stores/appStore';
import { useColorStore } from '@/stores/colorStore';
-import { Axis, Dimension, type PointWithMetadata } from '@/types';
+import { useDataStore } from '@/stores/dataStore';
+import { Axis, Dimension, SummaryTableColumn, type PointWithMetadata, type SummaryTableDataRow } from '@/types';
describe('usePlotTooltips', () => {
beforeEach(() => {
@@ -28,30 +29,30 @@ describe('usePlotTooltips', () => {
const appStore = useAppStore();
const colorStore = useColorStore();
- // Set up store state for location-based coloring
appStore.filters = {
[Dimension.LOCATION]: ['AFG', 'global'],
[Dimension.DISEASE]: ['Cholera'],
};
expect(colorStore.colorDimension).toBe(Dimension.LOCATION);
+ expect(appStore.dimensions.row).toBe(Dimension.DISEASE);
- // Set colors so colorStore has the mapping
colorStore.setColors([afgPointMetadata, globalPointMetadata]);
const { tooltipCallback } = usePlotTooltips();
const afgTooltip = tooltipCallback({ x: 1, y: 2, metadata: afgPointMetadata.metadata! });
- expect(afgTooltip).toContain('Location: Afghanistan');
+ expect(afgTooltip).toContain('Location: Afghanistan');
expect(afgTooltip).toContain('style="color: #009d9a'); // teal50
- expect(afgTooltip).not.toContain('Disease');
+ // Row dimension (disease) is shown because it's different from color dimension (location)
+ expect(afgTooltip).toContain('Disease: Cholera');
expect(afgTooltip).not.toContain('Activity type');
const globalTooltip = tooltipCallback({ x: 1, y: 2, metadata: globalPointMetadata.metadata! });
- expect(globalTooltip).toContain('Location: All 117 VIMC countries');
+ expect(globalTooltip).toContain('Location: All 117 VIMC countries');
expect(globalTooltip).toContain('style="color: #6929c4'); // purple70
- expect(globalTooltip).not.toContain('Disease');
+ expect(globalTooltip).toContain('Disease: Cholera');
expect(globalTooltip).not.toContain('Activity type');
});
@@ -62,12 +63,12 @@ describe('usePlotTooltips', () => {
const appStore = useAppStore();
const colorStore = useColorStore();
- // Set up store state for disease-based coloring
appStore.filters = {
[Dimension.LOCATION]: ['AFG'],
[Dimension.DISEASE]: ['Cholera', 'Measles'],
};
expect(colorStore.colorDimension).toBe(Dimension.DISEASE);
+ expect(appStore.dimensions.row).toBe(Dimension.DISEASE);
colorStore.setColors([choleraPointMetadata, measlesPointMetadata]);
@@ -75,16 +76,18 @@ describe('usePlotTooltips', () => {
const choleraTooltip = tooltipCallback({ x: 1, y: 2, metadata: choleraPointMetadata.metadata! });
- expect(choleraTooltip).toContain('Disease: Cholera');
+ expect(choleraTooltip).toContain('Disease: Cholera');
expect(choleraTooltip).toContain('style="color: #6929c4'); // purple70
- expect(choleraTooltip).not.toContain('Location');
+ // Row dimension (disease) is NOT shown separately because it's the same as color dimension
+ expect(choleraTooltip.match(/Disease:/g)?.length).toBe(1);
expect(choleraTooltip).not.toContain('Activity type');
const measlesTooltip = tooltipCallback({ x: 1, y: 2, metadata: measlesPointMetadata.metadata! });
- expect(measlesTooltip).toContain('Disease: Measles');
+ expect(measlesTooltip).toContain('Disease: Measles');
expect(measlesTooltip).toContain('style="color: #009d9a'); // teal50
- expect(measlesTooltip).not.toContain('Location');
+ // Row dimension (disease) is NOT shown separately because it's the same as color dimension
+ expect(measlesTooltip.match(/Disease:/g)?.length).toBe(1);
expect(measlesTooltip).not.toContain('Activity type');
});
@@ -99,30 +102,132 @@ describe('usePlotTooltips', () => {
await nextTick();
expect(appStore.dimensions.column).toBe(Dimension.ACTIVITY_TYPE);
- // Set up store state for disease-based coloring (single location)
appStore.filters = {
[Dimension.LOCATION]: ['AFG'],
[Dimension.DISEASE]: ['Cholera'],
[Dimension.ACTIVITY_TYPE]: ['routine', 'campaign'],
};
expect(colorStore.colorDimension).toBe(Dimension.DISEASE);
+ expect(appStore.dimensions.row).toBe(Dimension.DISEASE);
colorStore.setColors([routinePointMetadata, campaignPointMetadata]);
const { tooltipCallback } = usePlotTooltips();
const routineTooltip = tooltipCallback({ x: 1, y: 2, metadata: routinePointMetadata.metadata! });
- expect(routineTooltip).toContain('Disease: Cholera');
- expect(routineTooltip).toContain('Activity type: Routine');
+ expect(routineTooltip).toContain('Disease: Cholera');
+ expect(routineTooltip).toContain('Activity type: Routine');
+ // Row dimension (disease) is NOT shown separately because it's the same as color dimension
+ expect(routineTooltip.match(/Disease:/g)?.length).toBe(1);
expect(routineTooltip).toContain('style="color: #6929c4'); // purple70
- expect(routineTooltip).not.toContain('Location');
const campaignTooltip = tooltipCallback({ x: 1, y: 2, metadata: campaignPointMetadata.metadata! });
- expect(campaignTooltip).toContain('Disease: Cholera');
- expect(campaignTooltip).toContain('Activity type: Campaign');
+ expect(campaignTooltip).toContain('Disease: Cholera');
+ expect(campaignTooltip).toContain('Activity type: Campaign');
+ // Row dimension (disease) is NOT shown separately because it's the same as color dimension
+ expect(campaignTooltip.match(/Disease:/g)?.length).toBe(1);
expect(campaignTooltip).toContain('style="color: #6929c4'); // purple70
- expect(campaignTooltip).not.toContain('Location');
});
+
+ it('displays summary data (mean, and 95% confidence interval) in tooltip (linear scale)', () => {
+ const afgPointMetadata = { metadata: { [Axis.WITHIN_BAND]: 'AFG', [Axis.ROW]: 'Cholera', [Axis.COLUMN]: '' } };
+
+ const appStore = useAppStore();
+ const colorStore = useColorStore();
+ const dataStore = useDataStore();
+
+ appStore.filters = {
+ [Dimension.LOCATION]: ['AFG'],
+ [Dimension.DISEASE]: ['Cholera'],
+ };
+ appStore.logScaleEnabled = false;
+ colorStore.setColors([afgPointMetadata]);
+ dataStore.summaryTableData = [
+ {
+ [Dimension.LOCATION]: 'AFG',
+ [Dimension.DISEASE]: 'Cholera',
+ [SummaryTableColumn.MEAN]: 1456.78,
+ [SummaryTableColumn.CI_LOWER]: 789.12,
+ [SummaryTableColumn.CI_UPPER]: 2345.67,
+ } as SummaryTableDataRow,
+ ];
+
+ const { tooltipCallback } = usePlotTooltips();
+
+ const tooltip = tooltipCallback({ x: 1, y: 2, metadata: afgPointMetadata.metadata! });
+
+ expect(tooltip).toContain('Mean: 1456.78');
+ expect(tooltip).toContain('95% confidence interval:');
+ expect(tooltip).toContain('789.12');
+ expect(tooltip).toContain('2345.67');
+ });
+
+ it('handles negative confidence interval values with appropriate sign (linear scale)', () => {
+ const afgPointMetadata = { metadata: { [Axis.WITHIN_BAND]: 'AFG', [Axis.ROW]: 'Cholera', [Axis.COLUMN]: '' } };
+
+ const appStore = useAppStore();
+ const colorStore = useColorStore();
+ const dataStore = useDataStore();
+
+ appStore.filters = {
+ [Dimension.LOCATION]: ['AFG'],
+ [Dimension.DISEASE]: ['Cholera'],
+ };
+ appStore.logScaleEnabled = false;
+ colorStore.setColors([afgPointMetadata]);
+ // Set up summary table data with negative lower bound (crossing zero)
+ dataStore.summaryTableData = [
+ {
+ [Dimension.LOCATION]: 'AFG',
+ [Dimension.DISEASE]: 'Cholera',
+ [SummaryTableColumn.MEAN]: 55.00,
+ [SummaryTableColumn.CI_LOWER]: -100.50,
+ [SummaryTableColumn.CI_UPPER]: 200.75,
+ } as SummaryTableDataRow,
+ ];
+
+ const { tooltipCallback } = usePlotTooltips();
+
+ const tooltip = tooltipCallback({ x: 1, y: 2, metadata: afgPointMetadata.metadata! });
+
+ expect(tooltip).toContain('Mean: 55.00');
+ // Check negative lower bound and positive upper bound with + sign when interval crosses zero
+ expect(tooltip).toContain('-100.50');
+ expect(tooltip).toContain('+200.75');
+ });
+ });
+
+ it('displays summary data (mean, and 95% confidence interval) in tooltip (log scale: scientific notation)', () => {
+ const afgPointMetadata = { metadata: { [Axis.WITHIN_BAND]: 'AFG', [Axis.ROW]: 'Cholera', [Axis.COLUMN]: '' } };
+
+ const appStore = useAppStore();
+ const colorStore = useColorStore();
+ const dataStore = useDataStore();
+
+ appStore.filters = {
+ [Dimension.LOCATION]: ['AFG'],
+ [Dimension.DISEASE]: ['Cholera'],
+ };
+ appStore.logScaleEnabled = true;
+ colorStore.setColors([afgPointMetadata]);
+ dataStore.summaryTableData = [
+ {
+ [Dimension.LOCATION]: 'AFG',
+ [Dimension.DISEASE]: 'Cholera',
+ [SummaryTableColumn.MEAN]: 1456.78,
+ [SummaryTableColumn.CI_LOWER]: 789.12,
+ [SummaryTableColumn.CI_UPPER]: 2345.67,
+ } as SummaryTableDataRow,
+ ];
+
+ const { tooltipCallback } = usePlotTooltips();
+
+ const tooltip = tooltipCallback({ x: 1, y: 2, metadata: afgPointMetadata.metadata! });
+
+ expect(tooltip).toContain('Mean: 1.46 × 103');
+ expect(tooltip).toContain('95% confidence interval:');
+ expect(tooltip).toContain('7.89 × 102');
+ expect(tooltip).toContain('2.35 × 103');
});
});
diff --git a/tests/unit/stores/dataStore.spec.ts b/tests/unit/stores/dataStore.spec.ts
index 451285a9..ca8abbff 100644
--- a/tests/unit/stores/dataStore.spec.ts
+++ b/tests/unit/stores/dataStore.spec.ts
@@ -11,13 +11,21 @@ import histCountsDalysDiseaseCountryLog from "@/../public/data/json/hist_counts_
import histCountsDalysDiseaseLog from "@/../public/data/json/hist_counts_dalys_disease_log.json";
import histCountsDeathsDiseaseSubregionActivityType from "@/../public/data/json/hist_counts_deaths_disease_subregion_activity_type.json";
import histCountsDeathsDiseaseActivityType from "@/../public/data/json/hist_counts_deaths_disease_activity_type.json";
+import summaryDeathsDisease from "@/../public/data/json/summary_table_deaths_disease.json";
+import summaryDalysDiseaseSubregionActivityType from "@/../public/data/json/summary_table_dalys_disease_subregion_activity_type.json";
+import summaryDalysDiseaseActivityType from "@/../public/data/json/summary_table_dalys_disease_activity_type.json";
+import summaryDeathsDiseaseSubregionActivityType from "@/../public/data/json/summary_table_deaths_disease_subregion_activity_type.json";
+import summaryDeathsDiseaseActivityType from "@/../public/data/json/summary_table_deaths_disease_activity_type.json";
+import summaryDalysDiseaseSubregion from "@/../public/data/json/summary_table_dalys_disease_subregion.json";
+import summaryDalysDiseaseCountry from "@/../public/data/json/summary_table_dalys_disease_country.json";
+import summaryDalysDisease from "@/../public/data/json/summary_table_dalys_disease.json";
import { BurdenMetric } from '@/types';
import { useAppStore } from '@/stores/appStore';
import { useDataStore } from '@/stores/dataStore';
import { http, HttpResponse } from 'msw';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const expectLastNCallsToEqual = (spy: Mock, args: any[]) => {
+const expectLastNCallsToContain = (spy: Mock, args: any[]) => {
const calls = spy.mock.calls;
expect(calls.slice(calls.length - args.length)).toEqual(
expect.arrayContaining(args.map(a => expect.arrayContaining([a]))),
@@ -34,27 +42,40 @@ describe('data store', () => {
const appStore = useAppStore();
const dataStore = useDataStore();
expect(dataStore.histogramData).toEqual([]);
+ expect(dataStore.summaryTableData).toEqual([]);
// Initial data
- let expectedFetches = 1;
+ let expectedFetches = 2;
await vi.waitFor(() => {
expect(dataStore.isLoading).toBe(false);
expect(dataStore.histogramData).toHaveLength(histCountsDeathsDiseaseLog.length);
- expect(dataStore.histogramData[0]).toEqual({
+ expect(dataStore.histogramData[0]).toEqual(expect.objectContaining({
disease: "Cholera",
+ location: "global",
Counts: 1,
- lower_bound: -2.434,
- upper_bound: -2.422,
- });
+ lower_bound: expect.closeTo(-2.43),
+ upper_bound: expect.closeTo(-2.42),
+ }));
+ expect(dataStore.summaryTableData).toHaveLength(summaryDeathsDisease.length);
+ expect(dataStore.summaryTableData[0]).toEqual(expect.objectContaining({
+ disease: "COVID-19",
+ lower_95: expect.closeTo(0.08),
+ upper_95: expect.closeTo(0.19),
+ mean_value: expect.closeTo(0.12),
+ median_value: expect.closeTo(0.12),
+ }));
expect(fetchSpy).toBeCalledTimes(expectedFetches);
- expectLastNCallsToEqual(fetchSpy, ["./data/json/hist_counts_deaths_disease_log.json"]);
+ expectLastNCallsToContain(fetchSpy, [
+ "./data/json/hist_counts_deaths_disease_log.json",
+ "./data/json/summary_table_deaths_disease.json",
+ ]);
});
// Change options: round 1
expect(appStore.exploreBy).toEqual("location");
expect(appStore.focus).toEqual("global");
appStore.focus = "Middle Africa";
- expectedFetches += 2;
+ expectedFetches += 4;
appStore.burdenMetric = BurdenMetric.DALYS;
appStore.logScaleEnabled = false;
appStore.splitByActivityType = true;
@@ -64,13 +85,22 @@ describe('data store', () => {
expect(dataStore.histogramData).toHaveLength(
histCountsDalysDiseaseSubregionActivityType.length + histCountsDalysDiseaseActivityType.length
);
+ expect(dataStore.summaryTableData).toHaveLength(
+ summaryDalysDiseaseSubregionActivityType.length + summaryDalysDiseaseActivityType.length
+ );
});
expect(fetchSpy).toBeCalledTimes(expectedFetches);
- expectLastNCallsToEqual(fetchSpy, [
+ expectLastNCallsToContain(fetchSpy, [
"./data/json/hist_counts_dalys_disease_subregion_activity_type.json",
"./data/json/hist_counts_dalys_disease_activity_type.json",
+ "./data/json/summary_table_dalys_disease_subregion_activity_type.json",
+ "./data/json/summary_table_dalys_disease_activity_type.json",
]);
+ // Regression test: check that location columns include both global and subregional.
+ expect(dataStore.summaryTableData.map(r => r.location)).toEqual(expect.arrayContaining(["Middle Africa", "global"]));
+ expect(dataStore.histogramData.map(r => r.location)).toEqual(expect.arrayContaining(["Middle Africa", "global"]));
+
// Change options: round 2
appStore.exploreBy = "disease";
await vi.waitFor(() => {
@@ -78,7 +108,7 @@ describe('data store', () => {
expect(fetchSpy).toBeCalledTimes(expectedFetches); // No increment in expectedFetches due to cacheing.
});
appStore.focus = "Measles";
- expectedFetches += 2;
+ expectedFetches += 4;
appStore.burdenMetric = BurdenMetric.DEATHS;
appStore.logScaleEnabled = false;
appStore.splitByActivityType = true;
@@ -88,11 +118,16 @@ describe('data store', () => {
expect(dataStore.histogramData).toHaveLength(
histCountsDeathsDiseaseSubregionActivityType.length + histCountsDeathsDiseaseActivityType.length
);
+ expect(dataStore.summaryTableData).toHaveLength(
+ summaryDeathsDiseaseSubregionActivityType.length + summaryDeathsDiseaseActivityType.length
+ );
});
expect(fetchSpy).toBeCalledTimes(expectedFetches);
- expectLastNCallsToEqual(fetchSpy, [
+ expectLastNCallsToContain(fetchSpy, [
"./data/json/hist_counts_deaths_disease_subregion_activity_type.json",
"./data/json/hist_counts_deaths_disease_activity_type.json",
+ "./data/json/summary_table_deaths_disease_subregion_activity_type.json",
+ "./data/json/summary_table_deaths_disease_activity_type.json",
]);
// Change options: round 3
@@ -102,7 +137,7 @@ describe('data store', () => {
expect(fetchSpy).toBeCalledTimes(expectedFetches); // No increment in expectedFetches due to cacheing.
});
appStore.focus = "AFG";
- expectedFetches += 3;
+ expectedFetches += 6;
appStore.burdenMetric = BurdenMetric.DALYS;
appStore.logScaleEnabled = true;
appStore.splitByActivityType = false;
@@ -112,12 +147,18 @@ describe('data store', () => {
expect(dataStore.histogramData).toHaveLength(
histCountsDalysDiseaseSubregionLog.length + histCountsDalysDiseaseCountryLog.length + histCountsDalysDiseaseLog.length
);
+ expect(dataStore.summaryTableData).toHaveLength(
+ summaryDalysDiseaseSubregion.length + summaryDalysDiseaseCountry.length + summaryDalysDisease.length
+ );
}, { timeout: 2500 });
expect(fetchSpy).toBeCalledTimes(expectedFetches);
- expectLastNCallsToEqual(fetchSpy, [
+ expectLastNCallsToContain(fetchSpy, [
"./data/json/hist_counts_dalys_disease_subregion_log.json",
"./data/json/hist_counts_dalys_disease_country_log.json",
"./data/json/hist_counts_dalys_disease_log.json",
+ "./data/json/summary_table_dalys_disease_subregion.json",
+ "./data/json/summary_table_dalys_disease_country.json",
+ "./data/json/summary_table_dalys_disease.json",
]);
})
@@ -135,7 +176,7 @@ describe('data store', () => {
await vi.waitFor(() => {
expect(fetchSpy).toBeCalled();
expect(dataStore.fetchErrors).toEqual([expect.objectContaining(
- { message: `Error loading data from path: hist_counts_deaths_disease_log.json. TypeError: Failed to fetch` }
+ { message: `Error loading data from path: ./data/json/hist_counts_deaths_disease_log.json. TypeError: Failed to fetch` }
)]);
});
@@ -156,7 +197,7 @@ describe('data store', () => {
await vi.waitFor(() => {
expect(fetchSpy).toBeCalled();
expect(dataStore.fetchErrors).toEqual([expect.objectContaining(
- { message: `Error loading data from path: hist_counts_deaths_disease_log.json. Error: HTTP 404: Not Found` }
+ { message: `Error loading data from path: ./data/json/hist_counts_deaths_disease_log.json. Error: HTTP 404: Not Found` }
)]);
});