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

Feature/add configurable graph domain #136

Merged
merged 18 commits into from
Nov 23, 2020
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
8657e08
Fixed bug where the chart dropdowns reverse the subindicator order
Nov 17, 2020
e09b234
Factored code and wrote tests for subindicator-filter
Nov 17, 2020
93eb6cf
Added additional configuration to the horizontal barchart
Nov 17, 2020
831d62c
Merge branch 'chore/added-utility-function' into feature/add-configur…
Nov 17, 2020
20c1996
Adding missing keys to chartConfiguration
Nov 17, 2020
57a4d2d
Merge branch 'feature/missing-chart-config-keys-added' into feature/a…
Nov 17, 2020
a8ac36b
Some refactoring of the chart object
Nov 17, 2020
6f36ac3
Merge branch 'chore/added-utility-function' into feature/add-configur…
Nov 17, 2020
56f73b0
Refactored chart code and allowed additional graph configuration
Nov 17, 2020
d3413df
code refactor
Nov 17, 2020
1a63530
Added metadata.url to the default configuration
Nov 18, 2020
e38cfdf
Merge branches 'staging' and 'feature/missing-chart-config-keys-added…
Nov 18, 2020
42dacb2
Merge branch 'staging' into feature/add-configurable-graph-domain
Nov 18, 2020
6962e4d
Merge branch 'feature/missing-chart-config-keys-added' into feature/a…
Nov 18, 2020
e3fb06c
Merge branch 'chore/added-utility-function' into feature/add-configur…
milafrerichs Nov 19, 2020
072f929
Merge branch 'feature/add-configurable-graph-domain' of https://githu…
Nov 20, 2020
ae1df61
Fixed test and replaced custom object merge function in utils with lo…
Nov 20, 2020
c478e8d
Merge branch 'staging' into feature/add-configurable-graph-domain
milafrerichs Nov 23, 2020
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
1 change: 0 additions & 1 deletion __tests__/dataobjects.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,4 @@ describe('Test IndicatorHelper static methods', () => {

defaultValues.chartConfiguration = original;
})

})
81 changes: 81 additions & 0 deletions __tests__/profile/subindicator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import {SubindicatorFilter} from "../../src/js/profile/subindicator_filter";

const indicators = {
'Age by race': {
'groups': {
'gender': {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar blocks of code found in 2 locations. Consider refactoring.

'Female': {'subindicator1': {'count': 10}, 'subindicator2': {'count': 20}},
'Male': {'subindicator1': {'count': 30}, 'subindicator2': {'count': 40}},
},
'race': {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar blocks of code found in 2 locations. Consider refactoring.

'Race1': {'subindicator1': {'count': 50}, 'subindicator2': {'count': 60}},
'Race2': {'subindicator1': {'count': 70}, 'subindicator2': {'count': 80}},
},
},
'subindicators': [
{label: 'subindicator1', "count": 90},
{label: 'subindicator2', "count": 100},
]
},
'Another indicator': {}
}

function testChartData(chartData, expectedData) {
expect(chartData[0].value).toBe(expectedData[0].value);
expect(chartData[0].label).toBe(expectedData[0].label);
expect(chartData[1].value).toBe(expectedData[1].value);
expect(chartData[1].label).toBe(expectedData[1].label);
}

describe('Testing Subindicator Filter', () => {
const si = new SubindicatorFilter();
const title = 'Age by race';
si.indicators = indicators;

test.each([
[{group: 'race', subindicator: 'Race2'}, indicators[title].groups.race.Race2],
[{group: 'gender', subindicator: 'Female'}, indicators[title].groups.gender.Female],
])('Extract groups correctly', (value, expected) => {
const chartData = si.getFilteredGroups(indicators[title].groups, value.group, value.subindicator)
testChartData(chartData, [
{label: 'subindicator1', value: expected.subindicator1.count},
{label: 'subindicator2', value: expected.subindicator2.count},
])

expect(chartData[0].value).toBe(expected.subindicator1.count);
expect(chartData[1].value).toBe(expected.subindicator2.count);
})

test('Handles missing group correctly', () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar blocks of code found in 2 locations. Consider refactoring.

const chartData = si.getFilteredGroups(indicators[title].groups, 'Missing group', 'XXXXXX')
expect(chartData.length).toBe(0)
})

test('Handles missing subindicator correctly', () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar blocks of code found in 2 locations. Consider refactoring.

const chartData = si.getFilteredGroups(indicators[title].groups, 'Gender', 'Missing subindicator')
expect(chartData.length).toBe(0)
})

test('Extracts subindicators correctly', () => {
const subindicators = si.getFilteredSubindicators(indicators[title].subindicators)
testChartData(subindicators, [
{label: 'subindicator1', value: 90},
{label: 'subindicator2', value: 100}
])
})

test('Filters correctly', () => {
let chartData = si.getFilteredData('All values', '', title)
testChartData(chartData, [
{label: 'subindicator1', value: 90},
{label: 'subindicator2', value: 100}
])

chartData = si.getFilteredData('Female', 'gender', title)
testChartData(chartData, [
{label: 'subindicator1', value: 10},
{label: 'subindicator2', value: 20}
])

})
})
1 change: 1 addition & 0 deletions src/js/dataobjects.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class Geography {
}

export class IndicatorHelper {

static getMetadata(indicator) {
return indicator.metadata || {
source: '',
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.chart.types[this.graphValueType]
this.setChartDomain(chart, this.config.chart, 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.chart.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,19 +168,19 @@ 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 siFilter = new SubindicatorFilter();
this.bubbleEvent(siFilter, 'point_tray.subindicator_filter.filter')

let dropdowns = $(this.subCategoryNode).find('.filter__dropdown_wrap');
const filterArea = $(this.subCategoryNode).find('.profile-indicator__filters');

siFilter.handleFilter(detail.indicators, filterArea, groups, title, this, dropdowns);
siFilter.handleFilter(indicators, filterArea, groups, title, this, dropdowns);
}

applyFilter = (chartData) => {
Expand Down
10 changes: 9 additions & 1 deletion src/js/profile/indicator.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,15 @@ 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 config = {
chart: detail.indicators[title].chartConfiguration,
formatting: this.formattingConfig
}
const indicators = detail.indicators;

let c = new Chart(config, this.subindicators, this.groups, indicators, 'Percentage', indicator, title);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of creating the variable indicators in line 47, you could just use it here

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detail needs to be refactored out of this class entirely

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
50 changes: 30 additions & 20 deletions src/js/profile/subindicator_filter.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,25 @@ export class SubindicatorFilter extends Observable {
this.populateDropdown(subindicatorDd, subindicators, callback);
}

getFilteredGroups(groups, selectedGroup, selectedFilter) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Identical blocks of code found in 2 locations. Consider refactoring.

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 All @@ -143,27 +162,18 @@ export class SubindicatorFilter extends Observable {
});

let chartData = [];
const indicatorEntries = Object.entries(this.indicators)
const indicator = indicatorEntries.find(el => el[0] == title)

if (selectedFilter !== allValues) {
for (const [indicatorTitle, subindicator] of Object.entries(this.indicators)) {
//filter indicatorTitle
if (indicatorTitle === title) {
for (const [key, value] of Object.entries(subindicator.groups[selectedGroup])) {
if (key === selectedFilter) {
Object.entries(value).forEach((cd) => {
chartData.push(new SubIndicator(cd))
})
}
}
}
}
} else {
for (const [indicatorTitle, subindicator] of Object.entries(this.indicators)) {
if (indicatorTitle === title) {
chartData = subindicator.subindicators;
}
}
}
if (indicator == undefined)
return chartData

const subindicatorData = indicator[1]

if (selectedFilter !== allValues)
return this.getFilteredGroups(subindicatorData.groups, selectedGroup, selectedFilter)
else
return this.getFilteredSubindicators(subindicatorData.subindicators)

return chartData;
}
Expand Down
Loading