From 461bcddd5d74211e3baf032ee46779b27a717cfe Mon Sep 17 00:00:00 2001 From: Anthony Pessy Date: Thu, 9 Feb 2017 13:45:45 +0100 Subject: [PATCH] Clearer handling of tooltip ordering with data_order option (#1813) If data is grouped: - the tooltip will keep the same ordering as the stacked values If data is not grouped: - the tooltip will use the data_order option to sort the values Also adds an optional 'tooltip_order' option. If set, it will override the data_order option. --- spec/tooltip-spec.js | 340 ++++++++++++++++++++++++++++++++++++++++--- src/config.js | 3 +- src/data.js | 6 +- src/tooltip.js | 92 +++++++++--- src/util.js | 6 + 5 files changed, 407 insertions(+), 40 deletions(-) diff --git a/spec/tooltip-spec.js b/spec/tooltip-spec.js index 15f2772..ea73dbf 100644 --- a/spec/tooltip-spec.js +++ b/spec/tooltip-spec.js @@ -2,16 +2,20 @@ describe('c3 chart tooltip', function () { 'use strict'; var chart; - var tooltipConfiguration; + var tooltipConfiguration = {}; + var dataOrder = 'desc'; + var dataGroups; var args = function () { return { data: { columns: [ - ['data1', 30, 200, 100, 400, 150, 250], - ['data2', 50, 20, 10, 40, 15, 25], - ['data3', 150, 120, 110, 140, 115, 125] + ['data1', 30, 200, 100, 400, 150, 250], // 1130 + ['data2', 50, 20, 10, 40, 15, 25], // 160 + ['data3', 150, 120, 110, 140, 115, 125] // 760 ], + order: dataOrder, + groups: dataGroups }, tooltip: tooltipConfiguration }; @@ -19,6 +23,8 @@ describe('c3 chart tooltip', function () { beforeEach(function (done) { chart = window.initChart(chart, args(), done); + dataOrder = 'desc'; + dataGroups = undefined; }); describe('tooltip position', function () { @@ -99,24 +105,320 @@ describe('c3 chart tooltip', function () { }); }); - describe('tooltip getTooltipContent', function () { - beforeAll(function () { + describe('tooltip with data_order as desc with grouped data', function() { + beforeAll(function() { + dataOrder = 'desc'; + dataGroups = [ [ 'data1', 'data2', 'data3' ]]; + }); + + it('should display each data in descending order', function() { + window.setMouseEvent(chart, 'mousemove', 100, 100, d3.select('.c3-event-rect-2').node()); + + var classes = d3.selectAll('.c3-tooltip tr')[0].map(function(node) { + return node.className; + }); + + expect(classes[0]).toBe(''); // header + expect(classes[1]).toBe('c3-tooltip-name--data1'); // 1130 + expect(classes[2]).toBe('c3-tooltip-name--data3'); // 760 + expect(classes[3]).toBe('c3-tooltip-name--data2'); // 160 + }); + }); + + describe('tooltip with data_order as asc with grouped data', function() { + beforeAll(function() { + dataOrder = 'asc'; + dataGroups = [ [ 'data1', 'data2', 'data3' ]]; + }); + + it('should display each data in ascending order', function() { + window.setMouseEvent(chart, 'mousemove', 100, 100, d3.select('.c3-event-rect-2').node()); + + var classes = d3.selectAll('.c3-tooltip tr')[0].map(function(node) { + return node.className; + }); + + expect(classes[0]).toBe(''); // header + expect(classes[1]).toBe('c3-tooltip-name--data2'); // 160 + expect(classes[2]).toBe('c3-tooltip-name--data3'); // 760 + expect(classes[3]).toBe('c3-tooltip-name--data1'); // 1130 + }); + }); + + describe('tooltip with data_order as NULL with grouped data', function() { + beforeAll(function() { + dataOrder = null; + dataGroups = [ [ 'data1', 'data2', 'data3' ]]; + }); + + it('should display each data in given order', function() { + window.setMouseEvent(chart, 'mousemove', 100, 100, d3.select('.c3-event-rect-2').node()); + + var classes = d3.selectAll('.c3-tooltip tr')[0].map(function(node) { + return node.className; + }); + + expect(classes[0]).toBe(''); // header + expect(classes[1]).toBe('c3-tooltip-name--data1'); + expect(classes[2]).toBe('c3-tooltip-name--data2'); + expect(classes[3]).toBe('c3-tooltip-name--data3'); + }); + }); + + describe('tooltip with data_order as Function with grouped data', function() { + beforeAll(function() { + var order = [ 'data2', 'data1', 'data3' ]; + dataOrder = function(data1, data2) { + return order.indexOf(data1.id) - order.indexOf(data2.id); + }; + dataGroups = [ [ 'data1', 'data2', 'data3' ]]; + }); + + it('should display each data in order given by function', function() { + window.setMouseEvent(chart, 'mousemove', 100, 100, d3.select('.c3-event-rect-2').node()); + + var classes = d3.selectAll('.c3-tooltip tr')[0].map(function(node) { + return node.className; + }); + + expect(classes[0]).toBe(''); // header + expect(classes[1]).toBe('c3-tooltip-name--data2'); + expect(classes[2]).toBe('c3-tooltip-name--data1'); + expect(classes[3]).toBe('c3-tooltip-name--data3'); + }); + }); + + describe('tooltip with data_order as Array with grouped data', function() { + beforeAll(function() { + dataOrder = [ 'data2', 'data1', 'data3' ]; + dataGroups = [ [ 'data1', 'data2', 'data3' ]]; + }); + + it('should display each data in order given by array', function() { + window.setMouseEvent(chart, 'mousemove', 100, 100, d3.select('.c3-event-rect-2').node()); + + var classes = d3.selectAll('.c3-tooltip tr')[0].map(function(node) { + return node.className; + }); + + expect(classes[0]).toBe(''); // header + expect(classes[1]).toBe('c3-tooltip-name--data2'); + expect(classes[2]).toBe('c3-tooltip-name--data1'); + expect(classes[3]).toBe('c3-tooltip-name--data3'); + }); + }); + + describe('tooltip with data_order as desc with un-grouped data', function() { + beforeAll(function() { + dataOrder = 'desc'; + }); + + it('should display each tooltip value descending order', function() { + window.setMouseEvent(chart, 'mousemove', 100, 100, d3.select('.c3-event-rect-2').node()); + + var classes = d3.selectAll('.c3-tooltip tr')[0].map(function(node) { + return node.className; + }); + + expect(classes[0]).toBe(''); // header + expect(classes[1]).toBe('c3-tooltip-name--data3'); // 110 + expect(classes[2]).toBe('c3-tooltip-name--data1'); // 100 + expect(classes[3]).toBe('c3-tooltip-name--data2'); // 10 + }); + }); + + describe('tooltip with data_order as asc with un-grouped data', function() { + beforeAll(function() { + dataOrder = 'asc'; + }); + + it('should display each tooltip value in ascending order', function() { + window.setMouseEvent(chart, 'mousemove', 100, 100, d3.select('.c3-event-rect-2').node()); + + var classes = d3.selectAll('.c3-tooltip tr')[0].map(function(node) { + return node.className; + }); + + expect(classes[0]).toBe(''); // header + expect(classes[1]).toBe('c3-tooltip-name--data2'); // 10 + expect(classes[2]).toBe('c3-tooltip-name--data1'); // 100 + expect(classes[3]).toBe('c3-tooltip-name--data3'); // 110 + }); + }); + + describe('tooltip with data_order as NULL with un-grouped data', function() { + beforeAll(function() { + dataOrder = null; + }); + + it('should display each tooltip value in given data order', function() { + window.setMouseEvent(chart, 'mousemove', 100, 100, d3.select('.c3-event-rect-2').node()); + + var classes = d3.selectAll('.c3-tooltip tr')[0].map(function(node) { + return node.className; + }); + + expect(classes[0]).toBe(''); // header + expect(classes[1]).toBe('c3-tooltip-name--data1'); + expect(classes[2]).toBe('c3-tooltip-name--data2'); + expect(classes[3]).toBe('c3-tooltip-name--data3'); + }); + }); + + describe('tooltip with data_order as Function with un-grouped data', function() { + beforeAll(function() { + var order = [ 'data2', 'data1', 'data3' ]; + dataOrder = function(data1, data2) { + return order.indexOf(data1.id) - order.indexOf(data2.id); + }; + }); + + it('should display each tooltip value in data order given by function', function() { + window.setMouseEvent(chart, 'mousemove', 100, 100, d3.select('.c3-event-rect-2').node()); + + var classes = d3.selectAll('.c3-tooltip tr')[0].map(function(node) { + return node.className; + }); + + expect(classes[0]).toBe(''); // header + expect(classes[1]).toBe('c3-tooltip-name--data2'); + expect(classes[2]).toBe('c3-tooltip-name--data1'); + expect(classes[3]).toBe('c3-tooltip-name--data3'); + }); + }); + + describe('tooltip with data_order as Array with un-grouped data', function() { + beforeAll(function() { + dataOrder = [ 'data2', 'data1', 'data3' ]; + }); + + it('should display each tooltip value in data order given by array', function() { + window.setMouseEvent(chart, 'mousemove', 100, 100, d3.select('.c3-event-rect-2').node()); + + var classes = d3.selectAll('.c3-tooltip tr')[0].map(function(node) { + return node.className; + }); + + expect(classes[0]).toBe(''); // header + expect(classes[1]).toBe('c3-tooltip-name--data2'); + expect(classes[2]).toBe('c3-tooltip-name--data1'); + expect(classes[3]).toBe('c3-tooltip-name--data3'); + }); + }); + + describe('tooltip with tooltip_order as desc', function() { + beforeAll(function() { tooltipConfiguration = { - data_order: 'desc' - }; + order: 'desc' + }; + + // this should be ignored + dataOrder = 'asc'; + dataGroups = [ [ 'data1', 'data2', 'data3' ]]; + }); + + it('should display each tooltip value descending order', function() { + window.setMouseEvent(chart, 'mousemove', 100, 100, d3.select('.c3-event-rect-2').node()); + + var classes = d3.selectAll('.c3-tooltip tr')[0].map(function(node) { + return node.className; + }); + + expect(classes[0]).toBe(''); // header + expect(classes[1]).toBe('c3-tooltip-name--data3'); // 110 + expect(classes[2]).toBe('c3-tooltip-name--data1'); // 100 + expect(classes[3]).toBe('c3-tooltip-name--data2'); // 10 }); + }); + + describe('tooltip with tooltip_order as asc', function() { + beforeAll(function() { + tooltipConfiguration = { + order: 'asc' + }; - it('should sort values desc', function () { - var eventRect = d3.select('.c3-event-rect-2').node(); - window.setMouseEvent(chart, 'mousemove', 100, 100, eventRect); + // this should be ignored + dataOrder = 'desc'; + dataGroups = [ [ 'data1', 'data2', 'data3' ]]; + }); + + it('should display each tooltip value in ascending order', function() { + window.setMouseEvent(chart, 'mousemove', 100, 100, d3.select('.c3-event-rect-2').node()); + + var classes = d3.selectAll('.c3-tooltip tr')[0].map(function(node) { + return node.className; + }); - var tooltipTable = d3.select('.c3-tooltip')[0]; - var expected = ["", "c3-tooltip-name--data3", - "c3-tooltip-name--data1", "c3-tooltip-name--data2"]; - var i; - for (i = 0; i < tooltipTable[0].rows.length; i++) { - expect(tooltipTable[0].rows[i].className).toBe(expected[i]); - } + expect(classes[0]).toBe(''); // header + expect(classes[1]).toBe('c3-tooltip-name--data2'); // 10 + expect(classes[2]).toBe('c3-tooltip-name--data1'); // 100 + expect(classes[3]).toBe('c3-tooltip-name--data3'); // 110 + }); + }); + + describe('tooltip with tooltip_order as NULL', function() { + beforeAll(function() { + tooltipConfiguration = { + order: null + }; + }); + + it('should display each tooltip value in given order', function() { + window.setMouseEvent(chart, 'mousemove', 100, 100, d3.select('.c3-event-rect-2').node()); + + var classes = d3.selectAll('.c3-tooltip tr')[0].map(function(node) { + return node.className; + }); + + expect(classes[0]).toBe(''); // header + expect(classes[1]).toBe('c3-tooltip-name--data1'); + expect(classes[2]).toBe('c3-tooltip-name--data2'); + expect(classes[3]).toBe('c3-tooltip-name--data3'); + }); + }); + + describe('tooltip with tooltip_order as Function', function() { + beforeAll(function() { + var order = [ 'data2', 'data1', 'data3' ]; + tooltipConfiguration = { + order: function(data1, data2) { + return order.indexOf(data1.id) - order.indexOf(data2.id); + } + }; + }); + + it('should display each tooltip value in data order given by function', function() { + window.setMouseEvent(chart, 'mousemove', 100, 100, d3.select('.c3-event-rect-2').node()); + + var classes = d3.selectAll('.c3-tooltip tr')[0].map(function(node) { + return node.className; + }); + + expect(classes[0]).toBe(''); // header + expect(classes[1]).toBe('c3-tooltip-name--data2'); + expect(classes[2]).toBe('c3-tooltip-name--data1'); + expect(classes[3]).toBe('c3-tooltip-name--data3'); + }); + }); + + describe('tooltip with tooltip_order as Array', function() { + beforeAll(function() { + tooltipConfiguration = { + order: [ 'data2', 'data1', 'data3' ] + }; + }); + + it('should display each tooltip value in data order given by array', function() { + window.setMouseEvent(chart, 'mousemove', 100, 100, d3.select('.c3-event-rect-2').node()); + + var classes = d3.selectAll('.c3-tooltip tr')[0].map(function(node) { + return node.className; + }); + + expect(classes[0]).toBe(''); // header + expect(classes[1]).toBe('c3-tooltip-name--data2'); + expect(classes[2]).toBe('c3-tooltip-name--data1'); + expect(classes[3]).toBe('c3-tooltip-name--data3'); + }); }); - }); }); diff --git a/src/config.js b/src/config.js index 6899cbd..dc75086 100644 --- a/src/config.js +++ b/src/config.js @@ -121,7 +121,7 @@ c3_chart_internal_fn.getDefaultConfig = function () { axis_y_label: {}, axis_y_tick_format: undefined, axis_y_tick_outer: true, - axis_y_tick_values: null, + axis_y_tick_values: null, axis_y_tick_rotate: 0, axis_y_tick_count: undefined, axis_y_tick_time_value: undefined, @@ -203,6 +203,7 @@ c3_chart_internal_fn.getDefaultConfig = function () { regions: [], // tooltip - show when mouseover on each data tooltip_show: true, + tooltip_order: undefined, tooltip_grouped: true, tooltip_format_title: undefined, tooltip_format_name: undefined, diff --git a/src/data.js b/src/data.js index 531ab69..2ea5fcd 100644 --- a/src/data.js +++ b/src/data.js @@ -233,7 +233,11 @@ c3_chart_internal_fn.orderTargets = function (targets) { }); } else if (isFunction(config.data_order)) { targets.sort(config.data_order); - } // TODO: accept name array for order + } else if (isArray(config.data_order)) { + targets.sort(function (t1, t2) { + return config.data_order.indexOf(t1.id) - config.data_order.indexOf(t2.id); + }); + } return targets; }; c3_chart_internal_fn.filterByX = function (targets, x) { diff --git a/src/tooltip.js b/src/tooltip.js index 1ceafe4..e31db6e 100644 --- a/src/tooltip.js +++ b/src/tooltip.js @@ -24,31 +24,85 @@ c3_chart_internal_fn.initTooltip = function () { .style("display", "block"); } }; +c3_chart_internal_fn.getTooltipSortFunction = function() { + var $$ = this, config = $$.config; + + if (config.data_groups.length === 0 || config.tooltip_order !== undefined) { + // if data are not grouped or if an order is specified + // for the tooltip values we sort them by their values + + var order = config.tooltip_order; + if (order === undefined) { + order = config.data_order; + } + + var valueOf = function(obj) { + return obj ? obj.value : null; + }; + + // if data are not grouped, we sort them by their value + if (isString(order) && order.toLowerCase() === 'asc') { + return function(a, b) { + return valueOf(a) - valueOf(b); + }; + } else if (isString(order) && order.toLowerCase() === 'desc') { + return function (a, b) { + return valueOf(b) - valueOf(a); + }; + } else if (isFunction(order)) { + + // if the function is from data_order we need + // to wrap the returned function in order to format + // the sorted value to the expected format + + var sortFunction = order; + + if (config.tooltip_order === undefined) { + sortFunction = function (a, b) { + return order(a ? { + id: a.id, + values: [ a ] + } : null, b ? { + id: b.id, + values: [ b ] + } : null) + }; + } + + return sortFunction; + + } else if (isArray(order)) { + return function(a, b) { + return order.indexOf(a.id) - order.indexOf(b.id); + } + } + } else { + // if data are grouped, we follow the order of grouped targets + var ids = $$.orderTargets($$.data.targets).map(function(i) { + return i.id; + }); + + // if it was either asc or desc we need to invert the order + // returned by orderTargets + if ($$.isOrderAsc() || $$.isOrderDesc()) { + ids = ids.reverse(); + } + + return function(a, b) { + return ids.indexOf(a.id) - ids.indexOf(b.id); + } + } +}; c3_chart_internal_fn.getTooltipContent = function (d, defaultTitleFormat, defaultValueFormat, color) { var $$ = this, config = $$.config, titleFormat = config.tooltip_format_title || defaultTitleFormat, nameFormat = config.tooltip_format_name || function (name) { return name; }, valueFormat = config.tooltip_format_value || defaultValueFormat, - text, i, title, value, name, bgcolor, - orderAsc = $$.isOrderAsc(); + text, i, title, value, name, bgcolor; - if (config.data_groups.length === 0) { - d.sort(function(a, b){ - var v1 = a ? a.value : null, v2 = b ? b.value : null; - return orderAsc ? v1 - v2 : v2 - v1; - }); - } else { - var ids = $$.orderTargets($$.data.targets).map(function (i) { - return i.id; - }); - d.sort(function(a, b) { - var v1 = a ? a.value : null, v2 = b ? b.value : null; - if (v1 > 0 && v2 > 0) { - v1 = a ? ids.indexOf(a.id) : null; - v2 = b ? ids.indexOf(b.id) : null; - } - return orderAsc ? v1 - v2 : v2 - v1; - }); + var tooltipSortFunction = this.getTooltipSortFunction(); + if (tooltipSortFunction) { + d.sort(tooltipSortFunction); } for (i = 0; i < d.length; i++) { diff --git a/src/util.js b/src/util.js index 1365ab3..0076fc2 100644 --- a/src/util.js +++ b/src/util.js @@ -7,6 +7,12 @@ var isValue = c3_chart_internal_fn.isValue = function (v) { isString = c3_chart_internal_fn.isString = function (o) { return typeof o === 'string'; }, + isArray = c3_chart_internal_fn.isArray = function (o) { + if (Array.isArray) { + return Array.isArray(o); + } + return Object.prototype.toString.call(o) === '[object Array]'; + }, isUndefined = c3_chart_internal_fn.isUndefined = function (v) { return typeof v === 'undefined'; },