Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions src/components/RidgelinePlot.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
id="chartWrapper"
class="m-50"
:data-test="JSON.stringify({
histogramDataRowCount: histogramData.length,
histogramDataRowCount: dataStore.histogramData.length,
x: appStore.dimensions.x,
y: appStore.dimensions.y,
withinBand: appStore.dimensions.withinBand,
Expand All @@ -12,21 +12,20 @@
<h3 class="font-bold text-lg">If a plot were plotted, it would have:</h3>
<p :key="key" v-for="(value, key) in appStore.dimensions">{{ key }} axis: {{ value ?? "none" }}</p>
<p v-if="Object.values(appStore.dimensions).includes(Dimensions.LOCATION)" class="mt-5">
Location resolutions in use: {{ geographicalResolutions.join(", ") }}
Location resolutions in use: {{ dataStore.geographicalResolutions.join(", ") }}
</p>
<p class="mt-5">Data rows: {{ histogramData.length }}</p>
<p class="mt-5">Errors: {{ fetchErrors }}</p>
<p class="mt-5">Data rows: {{ dataStore.histogramData.length }}</p>
<p class="mt-5">Errors: {{ dataStore.fetchErrors }}</p>
</div>
</template>

<script setup lang="ts">
import useData from '@/composables/useData';
import { useDataStore } from '@/stores/dataStore';
import { useAppStore } from '@/stores/appStore';
import { Dimensions } from '@/types';

const appStore = useAppStore();

const { fetchErrors, geographicalResolutions, histogramData } = useData();
const dataStore = useDataStore();
</script>

<style lang="css" scoped>
Expand Down
36 changes: 23 additions & 13 deletions src/composables/useData.ts → src/stores/dataStore.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { useAppStore } from "@/stores/appStore";
import { type DataRow, Dimensions, LocResolutions } from "@/types";
import { debounce } from "perfect-debounce";
import { computed, ref, watch } from "vue";
import { computed, ref, shallowRef, watch } from "vue";

import countryOptions from '@/data/options/countryOptions.json';
import subregionOptions from '@/data/options/subregionOptions.json';
import { defineStore } from "pinia";

export const dataDir = `/data/json`

export default () => {
export const useDataStore = defineStore("data", () => {
const appStore = useAppStore();

const fetchErrors = ref<{ e: Error, message: string }[]>([]);
const histogramData = ref<DataRow[]>([]);
const histogramData = shallowRef<DataRow[]>([]);
const histogramDataCache: Record<string, DataRow[]> = {};

// The geographical resolutions to use based on current exploreBy and focus selections.
// This is currently exposed by the composable but that's only for manual testing purposes.
Expand Down Expand Up @@ -54,18 +57,25 @@ export default () => {
});

// Fetch and parse multiple JSONs, and merge together all data.
// TODO: cacheing strategies, e.g. if the new histDataPaths are a superset of the previous ones, only load the new paths
const loadDataFromPaths = async (paths: string[]) => {
fetchErrors.value = [];
const allRows = await Promise.all(paths.map(async (path) => {
const response = await fetch(`${dataDir}/${path}`);
const rows = await response.json();
return rows;
})).catch((error) => {
fetchErrors.value.push({ e: error, message: `Error loading data from paths: ${paths.join(", ")}. ${error}` });
});
await Promise.all(paths.map(async (path) => {
if (!histogramDataCache[path]) {
try {
const response = await fetch(`${dataDir}/${path}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const rows = await response.json();
histogramDataCache[path] = rows;
return rows;
} catch (error) {
fetchErrors.value.push({ e: error as Error, message: `Error loading data from path: ${path}. ${error}` });
}
}
}));

histogramData.value = allRows?.flat() ?? [];
histogramData.value = paths.flatMap((path) => histogramDataCache[path] || []);
};

const doLoadData = debounce(async () => {
Expand All @@ -82,4 +92,4 @@ export default () => {
}, { immediate: true });

return { histogramData, fetchErrors, geographicalResolutions };
}
});
2 changes: 1 addition & 1 deletion tests/unit/mocks/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { dataDir } from "@/composables/useData"
import { dataDir } from "@/stores/dataStore"
import { http, HttpResponse } from "msw"

const jsonDataFiles = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import histCountsDalysDiseaseLog from "@/../public/data/json/hist_counts_dalys_d
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 { BurdenMetrics } from '@/types';
import useData from '@/composables/useData';
import { useAppStore } from '@/stores/appStore';
import { useDataStore } from '@/stores/dataStore';
import { http, HttpResponse } from 'msw';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand All @@ -31,14 +31,14 @@ describe('useData', () => {
it('should initialize with correct data, and request correct data as store selections change', async () => {
const fetchSpy = vi.spyOn(global, 'fetch')
const appStore = useAppStore();
const { histogramData } = useData();
expect(histogramData.value).toEqual([]);
const dataStore = useDataStore();
expect(dataStore.histogramData).toEqual([]);

// Initial data
let expectedFetches = 1;
await vi.waitFor(() => {
expect(histogramData.value).toHaveLength(histCountsDeathsDiseaseLog.length);
expect(histogramData.value[0]).toEqual({
expect(dataStore.histogramData).toHaveLength(histCountsDeathsDiseaseLog.length);
expect(dataStore.histogramData[0]).toEqual({
disease: "Cholera",
Counts: 1,
lower_bound: -2.434,
Expand All @@ -57,7 +57,7 @@ describe('useData', () => {
appStore.logScaleEnabled = false;
appStore.splitByActivityType = true;
await vi.waitFor(() => {
expect(histogramData.value).toHaveLength(
expect(dataStore.histogramData).toHaveLength(
histCountsDalysDiseaseSubregionActivityType.length + histCountsDalysDiseaseActivityType.length
);
});
Expand All @@ -69,18 +69,17 @@ describe('useData', () => {

// Change options: round 2
appStore.exploreBy = "disease";
expectedFetches += 2;
await vi.waitFor(() => {
expect(appStore.focus).toEqual("Cholera")
expect(fetchSpy).toBeCalledTimes(expectedFetches);
expect(fetchSpy).toBeCalledTimes(expectedFetches); // No increment in expectedFetches due to cacheing.
});
appStore.focus = "Measles";
expectedFetches += 2;
appStore.burdenMetric = BurdenMetrics.DEATHS;
appStore.logScaleEnabled = false;
appStore.splitByActivityType = true;
await vi.waitFor(() => {
expect(histogramData.value).toHaveLength(
expect(dataStore.histogramData).toHaveLength(
histCountsDeathsDiseaseSubregionActivityType.length + histCountsDeathsDiseaseActivityType.length
);
});
Expand All @@ -92,18 +91,17 @@ describe('useData', () => {

// Change options: round 3
appStore.exploreBy = "location";
expectedFetches += 1;
await vi.waitFor(() => {
expect(appStore.focus).toEqual("global")
expect(fetchSpy).toBeCalledTimes(expectedFetches);
expect(fetchSpy).toBeCalledTimes(expectedFetches); // No increment in expectedFetches due to cacheing.
});
appStore.focus = "AFG";
expectedFetches += 3;
appStore.burdenMetric = BurdenMetrics.DALYS;
appStore.logScaleEnabled = true;
appStore.splitByActivityType = false;
await vi.waitFor(() => {
expect(histogramData.value).toHaveLength(
expect(dataStore.histogramData).toHaveLength(
histCountsDalysDiseaseSubregionLog.length + histCountsDalysDiseaseCountryLog.length + histCountsDalysDiseaseLog.length
);
}, { timeout: 2500 });
Expand All @@ -121,18 +119,39 @@ describe('useData', () => {
return HttpResponse.error();
}),
);
const { histogramData, fetchErrors } = useData();
const dataStore = useDataStore();

expect(dataStore.fetchErrors).toEqual([]);

const fetchSpy = vi.spyOn(global, 'fetch')
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` }
)]);
});

expect(dataStore.histogramData).toEqual([]);
});

it('should handle non-OK HTTP statuses gracefully', async () => {
server.use(
http.get("/data/json/hist_counts_deaths_disease_log.json", async () => {
return HttpResponse.json(null, { status: 404 });
}),
);
const dataStore = useDataStore();

expect(fetchErrors.value).toEqual([]);
expect(dataStore.fetchErrors).toEqual([]);

const fetchSpy = vi.spyOn(global, 'fetch')
await vi.waitFor(() => {
expect(fetchSpy).toBeCalled();
expect(fetchErrors.value).toEqual([expect.objectContaining(
{ message: `Error loading data from paths: hist_counts_deaths_disease_log.json. TypeError: Failed to fetch` }
expect(dataStore.fetchErrors).toEqual([expect.objectContaining(
{ message: `Error loading data from path: hist_counts_deaths_disease_log.json. Error: HTTP 404: Not Found` }
)]);
});

expect(histogramData.value).toEqual([]);
expect(dataStore.histogramData).toEqual([]);
});
});