Skip to content

Commit

Permalink
Merge pull request #614 from mrc-ide/mrc-2771
Browse files Browse the repository at this point in the history
mrc-2771 BUG fix - handle case where indicator is null in choropleth
  • Loading branch information
EmmaLRussell committed Nov 16, 2021
2 parents 2a6df24 + 81ed789 commit 31d1c4a
Show file tree
Hide file tree
Showing 9 changed files with 121 additions and 84 deletions.
4 changes: 4 additions & 0 deletions NEWS.md
@@ -1,3 +1,7 @@
# hint 1.70.2

* bug fix for undefined indicator in choropleth while new data is loading

# hint 1.70.1

* correct Portuguese translation for "Steps to reproduce"
Expand Down
Expand Up @@ -35,7 +35,6 @@
:countryAreaFilterOption="countryAreaFilterOption"
:indicators="filteredChoroplethIndicators"
:selections="choroplethSelections"

:selectedFilterOptions="choroplethSelections.selectedFilterOptions"
></area-indicators-table>
</div>
Expand Down
6 changes: 4 additions & 2 deletions src/app/static/src/app/components/plots/MapLegend.vue
Expand Up @@ -39,7 +39,7 @@
import numeral from 'numeral';
interface Props {
metadata: ChoroplethIndicatorMetadata,
metadata: ChoroplethIndicatorMetadata | undefined,
colourScale: ScaleSettings,
colourRange: NumericRange
}
Expand Down Expand Up @@ -98,12 +98,14 @@
const indexes = max == min ? [0] : [5, 4, 3, 2, 1, 0];
const invertScale = this.metadata.invert_scale
return indexes.map((i) => {
let val: any = min + (i * step);
val = roundToContext(val, [min, max]);
let valAsProportion = (max != min) ? (val - min) / (max - min) : 0;
if (this.metadata.invert_scale) {
if (invertScale) {
valAsProportion = 1 - valAsProportion;
}
Expand Down
105 changes: 59 additions & 46 deletions src/app/static/src/app/components/plots/choropleth/Choropleth.vue
Expand Up @@ -22,7 +22,8 @@
:level-labels="featureLevels"
@detail-changed="onDetailChange"
@indicator-changed="onIndicatorChange"></map-control>
<map-legend v-show="!emptyFeature" :metadata="colorIndicator"
<map-legend v-show="!emptyFeature"
:metadata="colorIndicator"
:colour-scale="indicatorColourScale"
:colour-range="colourRange"
@update="updateColourScale"></map-legend>
Expand Down Expand Up @@ -100,7 +101,7 @@
flattenedAreas: Dict<NestedFilterOption>,
selectedAreaFeatures: Feature[],
selectedAreaIds: string[],
colorIndicator: ChoroplethIndicatorMetadata,
colorIndicator: ChoroplethIndicatorMetadata | undefined,
options: GeoJSONOptions,
emptyFeature: boolean
}
Expand Down Expand Up @@ -159,43 +160,44 @@
initialised() {
const unsetFilters = this.nonAreaFilters.filter((f: Filter) => !this.selections.selectedFilterOptions[f.id]);
return unsetFilters.length == 0 && this.selections.detail > -1 &&
!!this.selections.indicatorId;
!!this.selections.indicatorId && !!this.colorIndicator;
},
emptyFeature() {
const nonEmptyFeature = (this.currentFeatures.filter(filtered => !!this.featureIndicators[filtered.properties!.area_id]))
return nonEmptyFeature.length == 0
},
colourRange() {
const indicator = this.selections.indicatorId;
const type = this.colourScales[indicator] && this.colourScales[indicator].type;
switch (type) {
case ScaleType.DynamicFull:
if (!this.fullIndicatorRanges.hasOwnProperty(indicator)) {
// cache the result in the fullIndicatorRanges object for future lookups
/* eslint vue/no-side-effects-in-computed-properties: "off" */
this.fullIndicatorRanges[indicator] =
getIndicatorRange(this.chartdata, this.colorIndicator)
}
return this.fullIndicatorRanges[indicator];
case ScaleType.DynamicFiltered:
return getIndicatorRange(
this.chartdata,
this.colorIndicator,
this.nonAreaFilters,
this.selections.selectedFilterOptions,
this.selectedAreaIds.filter(a => this.currentLevelFeatureIds.indexOf(a) > -1)
);
case ScaleType.Custom:
return {
min: this.colourScales[indicator].customMin,
max: this.colourScales[indicator].customMax
};
case ScaleType.Default:
default:
if (!this.initialised) {
return {max: 1, min: 0}
}
return {max: this.colorIndicator.max, min: this.colorIndicator.min}
if (!this.colorIndicator) {
return {max: 1, min: 0}
} else {
const indicator = this.selections.indicatorId;
const type = this.colourScales[indicator] && this.colourScales[indicator].type;
switch (type) {
case ScaleType.DynamicFull:
if (!this.fullIndicatorRanges.hasOwnProperty(indicator)) {
// cache the result in the fullIndicatorRanges object for future lookups
/* eslint vue/no-side-effects-in-computed-properties: "off" */
this.fullIndicatorRanges[indicator] =
getIndicatorRange(this.chartdata, this.colorIndicator)
}
return this.fullIndicatorRanges[indicator];
case ScaleType.DynamicFiltered:
return getIndicatorRange(
this.chartdata,
this.colorIndicator,
this.nonAreaFilters,
this.selections.selectedFilterOptions,
this.selectedAreaIds.filter(a => this.currentLevelFeatureIds.indexOf(a) > -1)
);
case ScaleType.Custom:
return {
min: this.colourScales[indicator].customMin,
max: this.colourScales[indicator].customMax
};
case ScaleType.Default:
default:
return {max: this.colorIndicator.max, min: this.colorIndicator.min}
}
}
},
selectedAreaIds() {
Expand All @@ -210,14 +212,18 @@
return Array.from(selectedAreaIdSet);
},
featureIndicators() {
return getFeatureIndicator(
this.chartdata,
this.selectedAreaIds,
this.colorIndicator,
this.colourRange,
this.nonAreaFilters,
this.selections.selectedFilterOptions
);
if (!this.colorIndicator) {
return {}
} else {
return getFeatureIndicator(
this.chartdata,
this.selectedAreaIds,
this.colorIndicator,
this.colourRange,
this.nonAreaFilters,
this.selections.selectedFilterOptions
);
}
},
featuresByLevel() {
const result = {} as any;
Expand Down Expand Up @@ -272,8 +278,8 @@
}
return [];
},
colorIndicator(): ChoroplethIndicatorMetadata {
return this.indicators.find(i => i.indicator == this.selections.indicatorId)!;
colorIndicator(): ChoroplethIndicatorMetadata | undefined {
return this.indicators.find(i => i.indicator == this.selections.indicatorId);
},
indicatorColourScale(): ScaleSettings | null {
const current = this.colourScales[this.selections.indicatorId];
Expand All @@ -287,7 +293,14 @@
},
options() {
const featureIndicators = this.featureIndicators;
const {format, scale, accuracy} = this.colorIndicator!;
let format = "";
let scale = 1;
let accuracy: number | null = null;
if (this.colorIndicator) {
format = this.colorIndicator.format;
scale = this.colorIndicator.scale;
accuracy = this.colorIndicator.accuracy;
}
return {
onEachFeature: function onEachFeature(feature: Feature, layer: Layer) {
const area_id = feature.properties && feature.properties["area_id"];
Expand All @@ -306,10 +319,10 @@
layer.bindTooltip(`<div>
<strong>${area_name}</strong>
<br/>${formatOutput(stringVal, format, scale, accuracy)}
<br/>(${formatOutput(stringLower, format, scale, accuracy)+" - "+
<br/>(${formatOutput(stringLower, format, scale, accuracy) + " - " +
formatOutput(stringUpper, format, scale, accuracy)})
</div>`);
}else {
} else {
layer.bindTooltip(`<div>
<strong>${area_name}</strong>
<br/>${formatOutput(stringVal, format, scale, accuracy)}
Expand Down
Expand Up @@ -24,7 +24,7 @@ export const getFeatureIndicator = function (data: any[],
return result;
};

export const initialiseScaleFromMetadata = function (meta: ChoroplethIndicatorMetadata) {
export const initialiseScaleFromMetadata = function (meta: ChoroplethIndicatorMetadata | undefined) {
const result = initialScaleSettings();
if (meta) {
result.customMin = meta.min;
Expand Down
61 changes: 31 additions & 30 deletions src/app/static/src/app/components/plots/utils.ts
Expand Up @@ -3,7 +3,8 @@ import {ChoroplethIndicatorMetadata, FilterOption} from "../../generated";
import {Dict, Filter, NumericRange} from "../../types";
import numeral from 'numeral';

export const getColor = (value: number, metadata: ChoroplethIndicatorMetadata,
export const getColor = (value: number,
metadata: ChoroplethIndicatorMetadata,
colourRange: NumericRange) => {

const min = colourRange.min;
Expand Down Expand Up @@ -163,54 +164,54 @@ const roundToPlaces = function (value: number, decPl: number) {
// Iteratively passes through the layers of a FilterOption object to find the regional hierarchy above the supplied id
// Takes param any for obj and returns any because it will iterate through both objects (the NestedFilterOption) and arrays (the array of child options), treating array indices as keys
export const findPath = function (id: string, obj: any): any {
for(const key in obj) {
if(obj.hasOwnProperty(key)) {
if(id === obj[key]) return "";
else if(obj[key] && typeof obj[key] === "object") {
const path = findPath(id, obj[key]);
if (path != undefined) {
return ((obj.label ? obj.label + "/": "") + path).replace(/\/$/, '');
}
}
}
}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
if (id === obj[key]) return "";
else if (obj[key] && typeof obj[key] === "object") {
const path = findPath(id, obj[key]);
if (path != undefined) {
return ((obj.label ? obj.label + "/" : "") + path).replace(/\/$/, '');
}
}
}
}
};

export const formatOutput = function (value: number | string, format: string, scale: number, accuracy: number | null) {
let ans: number

if (typeof(value) === 'string'){
if (typeof (value) === 'string') {
ans = parseFloat(value)
} else ans = value

if (!format.includes('%') && scale){
if (!format.includes('%') && scale) {
ans = ans * scale
}

if (!format.includes('%') && accuracy){
if (!format.includes('%') && accuracy) {
ans = Math.round(ans / accuracy) * accuracy
}

if (format){
if (format) {
return numeral(ans).format(format)
} else return ans
};

export const formatLegend = function(text: string | number, format: string, scale: number): string {
export const formatLegend = function (text: string | number, format: string, scale: number): string {
text = formatOutput(text, format, scale, null)

if (typeof(text) === "string" && text.includes(',')) {
text = text.replace(/,/g, '');
}
if (typeof(text) === "string" && !text.includes('%')) {
text = parseFloat(text)
}
if (typeof text == "number") {
if (text >= 1000 && text < 10000 || text >= 1000000 && text < 10000000) {
text = numeral(text).format("0.0a")
} else if (text >= 1000) {
text = numeral(text).format("0a")
} else text = text.toString()
}
if (typeof (text) === "string" && text.includes(',')) {
text = text.replace(/,/g, '');
}
if (typeof (text) === "string" && !text.includes('%')) {
text = parseFloat(text)
}
if (typeof text == "number") {
if (text >= 1000 && text < 10000 || text >= 1000000 && text < 10000000) {
text = numeral(text).format("0.0a")
} else if (text >= 1000) {
text = numeral(text).format("0a")
} else text = text.toString()
}
return text
}
2 changes: 1 addition & 1 deletion src/app/static/src/app/hintVersion.ts
@@ -1 +1 @@
export const currentHintVersion = "1.70.1";
export const currentHintVersion = "1.70.2";
Expand Up @@ -7,12 +7,13 @@ import registerTranslations from "../../../../app/store/translations/registerTra
import Vuex from "vuex";
import {emptyState} from "../../../../app/root";
import MapLegend from "../../../../app/components/plots/MapLegend.vue";
import {prev, testData} from "../testHelpers";
import {plhiv, prev, testData} from "../testHelpers";
import Filters from "../../../../app/components/plots/Filters.vue";
import {ScaleType} from "../../../../app/store/plottingSelections/plottingSelections";
import {ChoroplethSelections, ScaleType} from "../../../../app/store/plottingSelections/plottingSelections";
import Vue from "vue";
import MapEmptyFeature from "../../../../app/components/plots/MapEmptyFeature.vue";
import ResetMap from "../../../../app/components/plots/ResetMap.vue";
import {ChoroplethIndicatorMetadata} from "../../../../app/generated";

const localVue = createLocalVue();
const store = new Vuex.Store({
Expand Down Expand Up @@ -309,6 +310,24 @@ describe("Choropleth component", () => {
expect((uninitialisableWrapper.vm as any).initialised).toBe(false);
});

it("computes initialised to false if selected indicator is not found in the provided metadata", () => {
const wrapper = getWrapper();
const vm = wrapper.vm as any;
expect(vm.initialised).toBe(true);

const indicators: ChoroplethIndicatorMetadata[] = [{...plhiv}];
const selections: ChoroplethSelections = {
...propsData.selections,
indicatorId: "badid"
}
const uninitialisableWrapper = getWrapper({
indicators,
selections
});

expect((uninitialisableWrapper.vm as any).initialised).toBe(false);
});

it("computes areaFilter", () => {
const wrapper = getWrapper();
expect((wrapper.vm as any).areaFilter).toBe(propsData.filters[0]);
Expand Down
1 change: 0 additions & 1 deletion src/app/static/src/tests/components/plots/utils.test.ts
Expand Up @@ -398,7 +398,6 @@ describe("plot utils", () => {
expect(roundRange({min: 10, max: 10})).toStrictEqual({min: 10, max: 10});
});


it("can iterate data values and filter rows", () => {

const indicators = [
Expand Down

0 comments on commit 31d1c4a

Please sign in to comment.