Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NM-93] Add cascade plot #1040

Merged
merged 6 commits into from
Nov 28, 2024
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
3 changes: 3 additions & 0 deletions src/app/static/src/app/components/modelOutput/ModelOutput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
<Table class="col-md-9" v-if="selectedPlot === 'table'"
:plotName="'table'"
:download-enabled="true"/>
<barchart class="col-md-9" v-if="selectedPlot === 'cascade'"
:plot="selectedPlot"
:show-error-bars="true"/>
</div>
</div>
</template>
Expand Down
1 change: 0 additions & 1 deletion src/app/static/src/app/components/plots/bar/Barchart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ export default defineComponent({
borderWidth: 2,
borderDash: [5],
drawTime: 'beforeDatasetsDraw'
}
}
}
Expand Down
20 changes: 17 additions & 3 deletions src/app/static/src/app/components/plots/bar/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface ErrorBars {
}
}

type ChartDataSetsWithErrors<Data = BarChartDefaultData> = BarChartDataset<Data> & {
export type ChartDataSetsWithErrors<Data = BarChartDefaultData> = BarChartDataset<Data> & {
errorBars?: ErrorBars,
tooltipExtraText: string[]
}
Expand All @@ -35,14 +35,14 @@ export interface BarChartData<Data = BarChartDefaultData> extends ChartDataWithE
* @param areaLevel The current area level
* @param disaggregateSelections The selected options for the disaggregate control
* @param xAxisSelections The selected options for the x-axis control
* @param xAxisOptions The avaialble options for the x-axis control. We need this as we want
* @param xAxisOptions The available options for the x-axis control. We need this as we want
* to show the x-axis in the same order as the options are in the dropdown
* @param areaIdToLevelMap Mapping of area IDs to area levels
* @return Data required for chartJS. This includes:
* labels - array of labels for the x-axis
* datasets - array of datasets, each of them represents a bar in the barchart, with a label, a colour,
* the data values themselves and error bars
* maxValuePlusError - value for the highest value in the barchart + error, used for the height of the plot
* maxValuePlusError - value for the highest value in the barchart + error, used for the height of the plot
*/
export const plotDataToChartData = function (plotData: PlotData,
indicatorMetadata: IndicatorMetadata,
Expand Down Expand Up @@ -357,6 +357,20 @@ const initialBarChartDataset = (datasetLabel: string, backgroundColor: string):
}
};

export const sortDatasets = (datasets: ChartDataSetsWithErrors[], disaggregateSelections: FilterOption[], order: string[]) => {
const labelIdxMap = new Map<string, number>();
order.forEach((id: string, index) => {
const filterOpt = disaggregateSelections.find((opt: FilterOption) => opt.id === id)
if (filterOpt?.label) labelIdxMap.set(filterOpt.label, index)
});

const getLabelIndex = (label?: string): number => labelIdxMap.get(label ?? "") ?? Infinity;

datasets.sort((a: ChartDataSetsWithErrors, b: ChartDataSetsWithErrors) => {
return getLabelIndex(a.label) - getLabelIndex(b.label);
});
}

const colors = [
//d3 chromatic schemeSet1
'#e41a1c',
Expand Down
44 changes: 44 additions & 0 deletions src/app/static/src/app/generated.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,50 @@ export interface CalibrateMetadataResponse {
hidden?: boolean;
}[];
};
cascade: {
defaultEffect?: {
setFilters?: {
filterId: string;
label: string;
stateFilterId: string;
}[];
setMultiple?: string[];
setFilterValues?: {
[k: string]: string[];
};
setHidden?: string[];
customPlotEffect?: {
row: string[];
column: string[];
};
};
plotSettings: {
id: string;
label: string;
options: {
id: string;
label: string;
effect: {
setFilters?: {
filterId: string;
label: string;
stateFilterId: string;
}[];
setMultiple?: string[];
setFilterValues?: {
[k: string]: string[];
};
setHidden?: string[];
customPlotEffect?: {
row: string[];
column: string[];
};
};
}[];
value?: string;
hidden?: boolean;
}[];
};
};
warnings: {
text: string;
Expand Down
3 changes: 2 additions & 1 deletion src/app/static/src/app/hintVersion.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const currentHintVersion = "3.11.6";
export const currentHintVersion = "3.12.0";

8 changes: 7 additions & 1 deletion src/app/static/src/app/store/modelCalibrate/getters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,11 @@ export const getters = {
tableMetadata = metadata.plotSettingsControl[plotName].defaultEffect?.customPlotEffect
}
return tableMetadata
}
},
cascadePlotIndicators: (state: ModelCalibrateState) => {
// In the cascade plot, we want to show the bars in the specified order, as we want the bars to
// actually "cascade". This is in contrast to normal plots in which we show the selected in order
// that is in the drop down.
return state.metadata?.plotSettingsControl.cascade.defaultEffect?.setFilterValues?.indicator
}
};
25 changes: 21 additions & 4 deletions src/app/static/src/app/store/plotSelections/getters.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import {ControlSelection, FilterSelection, PlotName, PlotSelectionsState} from "./plotSelections";
import {IndicatorMetadata, FilterOption, InputComparisonData} from "../../generated";
import {BarChartData, inputComparisonPlotDataToChartData, plotDataToChartData} from "../../components/plots/bar/utils";
import {
BarChartData,
ChartDataSetsWithErrors,

Check warning on line 5 in src/app/static/src/app/store/plotSelections/getters.ts

View workflow job for this annotation

GitHub Actions / test

'ChartDataSetsWithErrors' is defined but never used. Allowed unused vars must match /^_/u
inputComparisonPlotDataToChartData,
plotDataToChartData,
sortDatasets
} from "../../components/plots/bar/utils";
import {RootState} from "../../root";
import {PlotData} from "../plotData/plotData";
import {Dict} from "../../types";
Expand All @@ -25,7 +31,7 @@
filterSelections: FilterSelection[], currentLanguage: string): BarChartData => {

const metadata = getMetadataFromPlotName(rootState, plotName);
if (plotName == "inputComparisonBarchart") {
if (plotName === "inputComparisonBarchart") {
const xAxisId = "year";
const xAxisSelections = getters.filterSelectionFromId(plotName, xAxisId);
const xAxisOptions = metadata.filterTypes.find(f => f.id === xAxisId)!.options;
Expand All @@ -47,15 +53,26 @@
// can just keep these as empty.
let areaIdToLevelMap = {} as Dict<number>;
let areaLevel = null;
if (plotName === "barchart" || plotName === "comparison") {
if (plotName === "barchart" || plotName === "comparison" || plotName === "cascade") {
areaIdToLevelMap = rootGetters["baseline/areaIdToLevelMap"];
areaLevel = filterSelections.find(f => f.filterId == "detail")?.selection[0]?.id;
}
if (disaggregateId && xAxisId && xAxisOptions) {
return plotDataToChartData(plotData, indicatorMetadata,
const data = plotDataToChartData(plotData, indicatorMetadata,
disaggregateId, disaggregateSelections,
xAxisId, xAxisSelections, xAxisOptions,
areaLevel, areaIdToLevelMap);
if (plotName === "cascade") {
// This is a bit ugly that it is here...
// In general this code is becoming a bit hard to follow, perhaps worth refactoring at some point?
// We want to sort the datasets here based on the order in the disaggregate selections
// The data is fetched in the order of the filter, and then shown in this order. This is fine for
// most plots but we want to override the order here and hard code it so that the indicators are shown
// in the order specified in the "cascade" instead of in the default indicator order
const indicatorOrder = rootGetters["modelCalibrate/cascadePlotIndicators"];
sortDatasets(data.datasets, disaggregateSelections, indicatorOrder)
}
return data
} else {
return emptyData
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export type InputPlotName = keyof ReviewInputFilterMetadataResponse["plotSetting
keyof InputComparisonResponse["metadata"]["plotSettingsControl"]
export type CalibratePlotName = keyof CalibratePlotResponse["metadata"]["plotSettingsControl"]

export const outputPlotNames: OutputPlotName[] = ["choropleth", "barchart", "table", "comparison", "bubble"];
export const outputPlotNames: OutputPlotName[] = ["choropleth", "barchart", "table", "comparison", "bubble", "cascade"];
export const inputPlotNames: InputPlotName[] = ["timeSeries", "inputChoropleth", "inputComparisonTable", "inputComparisonBarchart"];
export const calibratePlotName: CalibratePlotName = "calibrate";

Expand All @@ -32,6 +32,7 @@ export const plotNameToDataType: Record<PlotName, PlotDataType> = {
bubble: PlotDataType.Output,
choropleth: PlotDataType.Output,
table: PlotDataType.Output,
cascade: PlotDataType.Output,
timeSeries: PlotDataType.TimeSeries,
inputChoropleth: PlotDataType.InputChoropleth,
calibrate: PlotDataType.Calibrate,
Expand Down
4 changes: 1 addition & 3 deletions src/app/static/src/app/store/plotSelections/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,7 @@ export const commitPlotDefaultSelections = async (
});
});
payload.selections.controls = selectedSettingOptions

const filtersInfo = filtersInfoFromEffects(effects, rootState, metadata);
payload.selections.filters = filtersInfo;
payload.selections.filters = filtersInfoFromEffects(effects, rootState, metadata);

await getPlotData(payload, commit, rootState);

Expand Down
4 changes: 4 additions & 0 deletions src/app/static/src/app/store/translations/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export type Translations = {
cancelEdit: string,
cancelEditLoggedIn: string,
cancelFitting: string,
cascade: string,
clearText: string,
clickHere: string,
close: string,
Expand Down Expand Up @@ -431,6 +432,7 @@ const en: Translations = {
cancelEdit: "Cancel editing so I can save my work",
cancelEditLoggedIn: "Cancel editing",
cancelFitting: "Cancel fitting",
cascade: "Cascade",
clearText: "Clear",
clickHere: "Click here",
close: "Close",
Expand Down Expand Up @@ -822,6 +824,7 @@ const fr: Partial<Translations> = {
cancelEdit: "Annuler l'édition pour que je puisse sauvegarder mon travail",
cancelEditLoggedIn: "Annuler l'édition",
cancelFitting: "Annuler l'ajustement",
cascade: "Cascade",
clearText: "Effacer",
clickHere: "Cliquer ici",
close: "Fermer",
Expand Down Expand Up @@ -1212,6 +1215,7 @@ const pt: Partial<Translations> = {
cancelEdit: "Cancelar a edição para que eu possa guardar o meu trabalho",
cancelEditLoggedIn: "Cancelar edição",
cancelFitting: "Cancelar encaixe",
cascade: "Cascata",
clearText: "Limpar",
clickHere: "Clique aqui",
close: "Fechar",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,13 @@ describe("Model Output page", () => {
const wrapper = getWrapper(store);

const plotTabs = wrapper.findAll(".nav-link");
expect(plotTabs.length).toBe(5);
expect(plotTabs.length).toBe(6);
expect(plotTabs[0].classes()).contains("active");
expect(plotTabs[1].classes()).not.contains("active");
expect(plotTabs[2].classes()).not.contains("active");
expect(plotTabs[3].classes()).not.contains("active");
expect(plotTabs[4].classes()).not.contains("active");
expect(plotTabs[5].classes()).not.contains("active");
expect(wrapper.findComponent(PlotControlSet).exists()).toBeTruthy();
expect(wrapper.findComponent(FilterSet).exists()).toBeTruthy();
expect(wrapper.findComponent(Choropleth).exists()).toBeTruthy();
Expand Down
6 changes: 6 additions & 0 deletions src/app/static/src/tests/e2e/outputPlots.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,10 @@ test("can view output plots", async ({ projectPage }) => {

// bubble plot is updated
await expect(page.locator("#review-output")).toHaveScreenshot("bubble-reset.png");

// When I switch to cascade tab
await page.getByText('Cascade').click();

// Cascade plot is shown
await expect(page.locator("#review-output")).toHaveScreenshot("cascade-landing.png");
})
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/app/static/src/tests/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,9 @@ export const mockCalibrateMetadataResponse = (props: Partial<CalibrateMetadataRe
bubble: {
plotSettings: []
},
cascade: {
plotSettings: []
}
},
warnings: [],
...props
Expand Down
20 changes: 14 additions & 6 deletions src/app/static/src/tests/modelCalibrate/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ describe("ModelCalibrate actions", () => {

await actions.getResult({commit, state, rootState, dispatch} as any);

expect(commit.mock.calls.length).toBe(9);
expect(commit.mock.calls.length).toBe(10);
expect(commit.mock.calls[0][0]).toStrictEqual({
type: "MetadataFetched",
payload: testResult
Expand Down Expand Up @@ -230,28 +230,36 @@ describe("ModelCalibrate actions", () => {
filters: []
}
});
expect(commit.mock.calls[5][0]).toBe("plotSelections/updatePlotSelection");
expect(commit.mock.calls[5][1]["payload"]).toStrictEqual({
plot: "cascade",
selections: {
controls: [],
filters: []
}
});

// Commits initial scale selections
expect(commit.mock.calls[5][0]).toStrictEqual({
expect(commit.mock.calls[6][0]).toStrictEqual({
type: "plotState/setOutputScale",
payload: {
scale: Scale.Colour,
selections: {}
}
});
expect(commit.mock.calls[6][0]).toStrictEqual({
expect(commit.mock.calls[7][0]).toStrictEqual({
type: "plotState/setOutputScale",
payload: {
scale: Scale.Size,
selections: {}
}
});

expect(commit.mock.calls[7][0]).toBe("Calibrated");
expect(commit.mock.calls[8][0]).toBe("Ready");
expect(commit.mock.calls[8][0]).toBe("Calibrated");
expect(commit.mock.calls[9][0]).toBe("Ready");

// Dispatches to get plot data
expect(getOutputFilteredDataSpy.mock.calls.length).toBe(4) // The number of plots we have
expect(getOutputFilteredDataSpy.mock.calls.length).toBe(5) // The number of plots we have
expect(dispatch.mock.calls[0][0]).toBe("getCalibratePlot");
expect(dispatch.mock.calls[1][0]).toBe("getComparisonPlot");
});
Expand Down
39 changes: 39 additions & 0 deletions src/app/static/src/tests/modelCalibrate/getters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ describe("modelCalibrate getters", () => {
bubble: {
plotSettings: []
},
cascade: {
plotSettings: []
},
}
})
const calibrateState = mockModelCalibrateState({
Expand Down Expand Up @@ -196,6 +199,9 @@ describe("modelCalibrate getters", () => {
bubble: {
plotSettings: []
},
cascade: {
plotSettings: []
},
}
})
const calibrateState = mockModelCalibrateState({
Expand All @@ -217,4 +223,37 @@ describe("modelCalibrate getters", () => {

expect(getter("table")).toBe(undefined);
});

it("can get indicator for cascade plot in hard coded order", () => {
const calibrateResponse = mockCalibrateMetadataResponse({
plotSettingsControl: {
choropleth: {
plotSettings: []
},
barchart: {
plotSettings: []
},
table: {
plotSettings: []
},
bubble: {
plotSettings: []
},
cascade: {
defaultEffect: {
setFilterValues: {
indicator: ["a", "b"]
}
},
plotSettings: []
},
}
});
const state = mockModelCalibrateState({
metadata: calibrateResponse
});
const indicator = getters.cascadePlotIndicators(state);

expect(indicator).toStrictEqual(["a", "b"]);
});
});
3 changes: 3 additions & 0 deletions src/app/static/src/tests/plotSelections/actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,9 @@ describe("Projects actions", () => {
bubble: {
plotSettings: []
},
cascade: {
plotSettings: []
},
}
});
const rootState = mockRootState({
Expand Down
Loading
Loading