diff --git a/__tests__/dataobjects.test.js b/__tests__/dataobjects.test.js index 9fbb313e3..045dd0dfa 100644 --- a/__tests__/dataobjects.test.js +++ b/__tests__/dataobjects.test.js @@ -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) @@ -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; }) - }) diff --git a/__tests__/profile/subindicator.test.js b/__tests__/profile/subindicator.test.js index 36c52c50e..639bde3c6 100644 --- a/__tests__/profile/subindicator.test.js +++ b/__tests__/profile/subindicator.test.js @@ -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) }) diff --git a/src/js/dataobjects.js b/src/js/dataobjects.js index b77919df1..7277af453 100644 --- a/src/js/dataobjects.js +++ b/src/js/dataobjects.js @@ -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 } } diff --git a/src/js/defaultValues.js b/src/js/defaultValues.js index 841e099c9..f73d0dd1f 100644 --- a/src/js/defaultValues.js +++ b/src/js/defaultValues.js @@ -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 } \ No newline at end of file diff --git a/src/js/profile/chart.js b/src/js/profile/chart.js index cc3dbeed7..4611cafa2 100644 --- a/src/js/profile/chart.js +++ b/src/js/profile/chart.js @@ -1,26 +1,29 @@ -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; @@ -28,14 +31,10 @@ export class Chart extends Observable { 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(); @@ -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) }) } @@ -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'); @@ -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'); @@ -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') } diff --git a/src/js/profile/indicator.js b/src/js/profile/indicator.js index 1d7db48d5..468b69f94 100644 --- a/src/js/profile/indicator.js +++ b/src/js/profile/indicator.js @@ -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', diff --git a/src/js/profile/subindicator_filter.js b/src/js/profile/subindicator_filter.js index 9e7613ce4..9bf006a6f 100644 --- a/src/js/profile/subindicator_filter.js +++ b/src/js/profile/subindicator_filter.js @@ -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, diff --git a/src/js/reusable-charts/horizontal-bar-chart.js b/src/js/reusable-charts/horizontal-bar-chart.js index ac8401ed2..b95e6b666 100644 --- a/src/js/reusable-charts/horizontal-bar-chart.js +++ b/src/js/reusable-charts/horizontal-bar-chart.js @@ -27,6 +27,9 @@ export function horizontalBarChart() { left: 100, }, reverse: false, + minX: 0, + maxX: null, + tooltipFormatter: (d) => { return `${d.data.label}: ${d.data.value}`; }, @@ -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(() => { @@ -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]) @@ -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; @@ -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; @@ -437,5 +464,6 @@ export function horizontalBarChart() { return chart; }; + return chart; } diff --git a/src/js/utils.js b/src/js/utils.js index 5703c4902..244766ff3 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -1,3 +1,4 @@ +import {merge} from 'lodash'; import {format as d3format} from 'd3-format'; const queryCache = {}; @@ -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) } diff --git a/yarn.lock b/yarn.lock index 89ff08d4b..d1d972dfe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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"