Skip to content

Commit

Permalink
Feature/add configurable graph domain (#136)
Browse files Browse the repository at this point in the history
* Added additional configuration to the horizontal barchart
Can now specify the minX and maxX values

* Adding missing keys to chartConfiguration
previously the expectation was that all keys were present if a custom config was provided via the api

* Refactored chart code and allowed additional graph configuration
Some cleanup of chart.js. Removed some if statement that checked for percentage or value - this is now driven from the config. Also added the ability to set the domain of the axis. This allows users to for instance for the percentage graphs to range between 0 and 100

* Fixed test and replaced custom object merge function in utils with lodash
Lodash has a better deep merge function which deals with cases that the existing code didn't.

Co-authored-by: Adi Eyal <adi@openup.org.za>
Co-authored-by: Mila Frerichs <mila.frerichs@gmail.com>
  • Loading branch information
3 people authored Nov 23, 2020
1 parent 0709a80 commit 471df61
Show file tree
Hide file tree
Showing 10 changed files with 116 additions and 64 deletions.
14 changes: 7 additions & 7 deletions __tests__/dataobjects.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@ describe('Test IndicatorHelper static methods', () => {
const original = defaultValues.chartConfiguration
defaultValues.chartConfiguration = {test1: 1, test2: 2}

let indicator = {test1: 3}
let chartConfiguration = {test1: 3}
let chartConfiguration2 = IndicatorHelper.getChartConfiguration(chartConfiguration)

indicator.chartConfiguration = IndicatorHelper.getChartConfiguration(indicator)
expect(indicator.chartConfiguration).toBeDefined()
expect(indicator.chartConfiguration.test1).toBe(1)
expect(indicator.chartConfiguration.test2).toBe(2)
expect(chartConfiguration2).toBeDefined()
expect(chartConfiguration2.test1).toBe(3)
expect(chartConfiguration2.test2).toBe(2)

defaultValues.chartConfiguration = original;
})

test('fix indicator applies all manipulations to indicator', () => {
const original = defaultValues.chartConfiguration
defaultValues.chartConfiguration = {test1: 1, test2: 2}
defaultValues.chartConfiguration = {test1: 1, test2: 2, test3: {test4: 4}}

let indicator = {}
indicator = IndicatorHelper.fixIndicator(indicator)
Expand All @@ -41,8 +41,8 @@ describe('Test IndicatorHelper static methods', () => {
expect(indicator.chartConfiguration).toBeDefined()
expect(indicator.chartConfiguration.test1).toBe(1)
expect(indicator.chartConfiguration.test2).toBe(2)
expect(indicator.chartConfiguration.test3.test4).toBe(4)

defaultValues.chartConfiguration = original;
})

})
2 changes: 2 additions & 0 deletions __tests__/profile/subindicator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,13 @@ describe('SubindicatorFilter', () => {

test('Handles missing group correctly', () => {
const chartData = si.getFilteredGroups(INDICATORS[title].groups, 'Missing group', 'XXXXXX')

expect(chartData.length).toBe(0)
})

test('Handles missing subindicator correctly', () => {
const chartData = si.getFilteredGroups(INDICATORS[title].groups, 'Gender', 'Missing subindicator')

expect(chartData.length).toBe(0)
})

Expand Down
6 changes: 3 additions & 3 deletions src/js/dataobjects.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ export class IndicatorHelper {
}
}

static getChartConfiguration(indicator) {
return fillMissingKeys(defaultValues.chartConfiguration, indicator.chartConfiguration || {})
static getChartConfiguration(chart_configuration) {
return fillMissingKeys(chart_configuration, defaultValues.chartConfiguration || {})
}

static fixIndicator(indicator) {
indicator.metadata = this.getMetadata(indicator)
indicator.chartConfiguration = this.getChartConfiguration(indicator)
indicator.chartConfiguration = this.getChartConfiguration(indicator.chart_configuration)
return indicator
}
}
Expand Down
12 changes: 9 additions & 3 deletions src/js/defaultValues.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
let chartConfiguration = [{"label": "Value", "formatting": "~s"}];

const DEFAULT_CONFIG = 'default'
let chartConfiguration = {
types: {
Value: {formatting: '~s', minX: DEFAULT_CONFIG, maxX: DEFAULT_CONFIG},
Percentage: {formatting: '.0%', minX: DEFAULT_CONFIG, maxX: DEFAULT_CONFIG}
}
};
export const defaultValues = {
chartConfiguration
chartConfiguration,
DEFAULT_CONFIG
}
67 changes: 35 additions & 32 deletions src/js/profile/chart.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,40 @@
import {numFmtAlt, Observable, formatNumericalValue} from "../utils";
import {format as d3format} from "d3-format/src/defaultLocale";
import {horizontalBarChart} from "../reusable-charts/horizontal-bar-chart";
import {select as d3select} from "d3-selection";
import {SubindicatorFilter} from "./subindicator_filter";
import {format as d3format} from 'd3-format/src/defaultLocale';
import {select as d3select} from 'd3-selection';

const graphValueTypes = ['Percentage', 'Value'];
import {Observable} from '../utils';
import {defaultValues} from '../defaultValues';

import {horizontalBarChart} from '../reusable-charts/horizontal-bar-chart';
import {SubindicatorFilter} from './subindicator_filter';

const PERCENTAGE_TYPE = 'Percentage';
const VALUE_TYPE = 'Value'
const graphValueTypes = [PERCENTAGE_TYPE, VALUE_TYPE];
const chartContainerClass = '.indicator__chart';
const tooltipClass = '.bar-chart__row_tooltip';

let tooltipClone = null;

export class Chart extends Observable {
constructor(formattingConfig, subindicators, groups, detail, graphValueType, _subCategoryNode, title) {
//we need the detail parameter to be able to filter
constructor(config, subindicators, groups, indicators, graphValueType, _subCategoryNode, title) {
//we need the subindicators and groups too even though we have detail parameter. they are used for the default chart data
super();

this.subindicators = subindicators;
this.graphValueType = graphValueType;
this.title = title;
this.formattingConfig = formattingConfig;
this.chartConfiguration = this.getChartConfiguration(detail, title);
this.config = config;

tooltipClone = $(tooltipClass)[0].cloneNode(true);
this.subCategoryNode = _subCategoryNode;

const chartContainer = $(chartContainerClass, this.subCategoryNode);
this.container = chartContainer[0];

this.handleChartFilter(detail, groups, title);
this.handleChartFilter(indicators, groups, title);
this.addChart();
}

getChartConfiguration = (detail, title) => {
return detail.indicators[title].chartConfiguration;
}

addChart = () => {
$('.bar-chart', this.container).remove();
$('svg', this.container).remove();
Expand Down Expand Up @@ -77,27 +76,33 @@ export class Chart extends Observable {
})
.xLabel("")

if (this.graphValueType === graphValueTypes[0]) {
chart.xAxisFormatter((d) => {
return d + '%';
})
} else {
chart.xAxisFormatter((d) => {
return d3format(this.chartConfiguration[0].formatting)(d);
})
}
this.chartConfig = this.config.types[this.graphValueType]
this.setChartDomain(chart, this.config, this.graphValueType)

chart.xAxisFormatter(d => {
return d3format(this.chartConfig.formatting)(d)
})
}

setChartDomain(chart, config, chartType) {
const chartConfig = config.types[chartType]
if (chartConfig.minX != defaultValues.DEFAULT_CONFIG)
chart.minX(chartConfig.minX)
if (chartConfig.maxX != defaultValues.DEFAULT_CONFIG)
chart.maxX(chartConfig.maxX)
}

getValuesFromSubindicators = () => {
let arr = [];
const chartConfig = this.config.types[this.graphValueType]

for (const [label, subindicator] of Object.entries(this.subindicators)) {
let count = subindicator.count;
let val = this.graphValueType === graphValueTypes[0] ? this.getPercentageValue(count, this.subindicators) : count;
let val = this.graphValueType === PERCENTAGE_TYPE ? this.getPercentageValue(count, this.subindicators) : count;
arr.push({
label: subindicator.keys,
value: val,
valueText: this.graphValueType === graphValueTypes[0] ?
formatNumericalValue(val / 100, this.formattingConfig, 'percentage') : d3format(this.chartConfiguration[0].formatting)(val)
valueText: d3format(chartConfig.formatting)(val)
})
}

Expand All @@ -108,7 +113,6 @@ export class Chart extends Observable {
const self = this;
const containerParent = $(this.container).closest('.profile-indicator');

//save as image button
const saveImgButton = $(containerParent).find('.hover-menu__content a.hover-menu__content_item:nth-child(1)');

$(saveImgButton).off('click');
Expand All @@ -117,7 +121,6 @@ export class Chart extends Observable {
this.triggerEvent('profile.chart.saveAsPng', this);
})

//show as percentage / value
//todo:don't use index, specific class names should be used here when the classes are ready
$(containerParent).find('.hover-menu__content_list a').each(function (index) {
$(this).off('click');
Expand Down Expand Up @@ -165,16 +168,16 @@ export class Chart extends Observable {
total += value.count;
}

percentage = currentValue / total * 100;
percentage = currentValue / total;

return percentage;
}

handleChartFilter = (detail, groups, title) => {
handleChartFilter = (indicators, groups, title) => {
let dropdowns = $(this.subCategoryNode).find('.filter__dropdown_wrap');
const filterArea = $(this.subCategoryNode).find('.profile-indicator__filters');

let siFilter = new SubindicatorFilter(detail.indicators, filterArea, groups, title, this, dropdowns);
let siFilter = new SubindicatorFilter(indicators, filterArea, groups, title, this, dropdowns);
this.bubbleEvent(siFilter, 'point_tray.subindicator_filter.filter')

}
Expand Down
6 changes: 5 additions & 1 deletion src/js/profile/indicator.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@ export class Indicator extends Observable {
}
}

let c = new Chart(this.formattingConfig, this.subindicators, this.groups, detail, 'Percentage', indicator, title);

const configuration = detail.indicators[title].chartConfiguration;
const indicators = detail.indicators;

let c = new Chart(configuration, this.subindicators, this.groups, indicators, 'Percentage', indicator, title);
this.bubbleEvents(c, [
'profile.chart.saveAsPng', 'profile.chart.valueTypeChanged',
'profile.chart.download_csv', 'profile.chart.download_excel', 'profile.chart.download_json', 'profile.chart.download_kml',
Expand Down
19 changes: 19 additions & 0 deletions src/js/profile/subindicator_filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,25 @@ export class SubindicatorFilter extends Observable {
})
}

getFilteredGroups(groups, selectedGroup, selectedFilter) {
const group = groups[selectedGroup]
if (group == undefined)
return []

const groupValue = Object.entries(group).find(g => g[0] == selectedFilter)
if (groupValue == undefined)
return []

const subindicators = Object.entries(groupValue[1])
return subindicators.map(cd => new SubIndicator(cd))
}

getFilteredSubindicators(subindicators) {
return subindicators.map(el => {
return new SubIndicator([el.label, el])
})
}

getFilteredData = (selectedFilter, selectedGroup, title) => {
this.triggerEvent('point_tray.subindicator_filter.filter', {
indicator: title,
Expand Down
30 changes: 29 additions & 1 deletion src/js/reusable-charts/horizontal-bar-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ export function horizontalBarChart() {
left: 100,
},
reverse: false,
minX: 0,
maxX: null,

tooltipFormatter: (d) => {
return `${d.data.label}: ${d.data.value}`;
},
Expand Down Expand Up @@ -56,6 +59,8 @@ export function horizontalBarChart() {
let xLabel = initialConfiguration.xLabel;
let barLabelLength = initialConfiguration.barLabelLength;
let reverse = initialConfiguration.reverse;
let minX = initialConfiguration.minX;
let maxX = initialConfiguration.maxX;

function chart(selection) {
selection.each(() => {
Expand All @@ -73,7 +78,7 @@ export function horizontalBarChart() {

const x = scaleLinear()
.range([0, width]) //**
.domain([0, max(data, (d) => d.value)]);
.domain([minX, _maxX()])

const y = scaleBand()
.rangeRound([height, 0])
Expand Down Expand Up @@ -312,6 +317,10 @@ export function horizontalBarChart() {
return exportData;
}

function _maxX() {
return maxX || max(data, d => d.value);
}

chart.width = function (value) {
if (!arguments.length) return width;
width = value;
Expand Down Expand Up @@ -405,6 +414,24 @@ export function horizontalBarChart() {
}
};

chart.minX = function(value) {
if (!arguments.length) {
return minX
}
minX = value;

return chart;
}

chart.maxX = function(value) {
if (!arguments.length) {
return _maxX()
}
maxX = value;

return chart;
}

chart.yAxisFormatter = function (value) {
if (!arguments.length) {
return yAxisFormatter;
Expand Down Expand Up @@ -437,5 +464,6 @@ export function horizontalBarChart() {
return chart;
};


return chart;
}
15 changes: 2 additions & 13 deletions src/js/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {merge} from 'lodash';
import {format as d3format} from 'd3-format';

const queryCache = {};
Expand Down Expand Up @@ -290,17 +291,5 @@ export function saveAs(uri, filename) {
}

export function fillMissingKeys(obj, defaultObj, deep_copy = false) {
let filledObject = {...defaultObj, ...obj}

Object.entries(filledObject).forEach(entry => {
const key = entry[0]
const val = entry[1]
const defaultObj2 = defaultObj[key]

if (typeof val === 'object' && defaultObj2 != undefined && deep_copy)
filledObject[key] = fillMissingKeys(val, defaultObj2, deep_copy)
})

return filledObject;

return merge({}, defaultObj, obj)
}
9 changes: 5 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3961,9 +3961,10 @@ html-tags@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-1.2.0.tgz#c78de65b5663aa597989dd2b7ab49200d7e4db98"

html2canvas@^1.0.0-rc.5:
version "1.0.0-rc.5"
resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.0.0-rc.5.tgz#4ee3cac9f6e20a0fa0c2f35a6f99c960ae7ec4c1"
html2canvas@^1.0.0-rc.7:
version "1.0.0-rc.7"
resolved "https://registry.yarnpkg.com/html2canvas/-/html2canvas-1.0.0-rc.7.tgz#70c159ce0e63954a91169531894d08ad5627ac98"
integrity sha512-yvPNZGejB2KOyKleZspjK/NruXVQuowu8NnV2HYG7gW7ytzl+umffbtUI62v2dCHQLDdsK6HIDtyJZ0W3neerA==
dependencies:
css-line-break "1.1.1"

Expand Down Expand Up @@ -4982,7 +4983,7 @@ lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.4:
version "4.17.19"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"

lodash@^4.17.19:
lodash@^4.17.19, lodash@^4.17.20:
version "4.17.20"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"

Expand Down

0 comments on commit 471df61

Please sign in to comment.