diff --git a/js/parts/Chart.js b/js/parts/Chart.js index 86be708f4f1..a29c50c32dc 100644 --- a/js/parts/Chart.js +++ b/js/parts/Chart.js @@ -90,10 +90,27 @@ Chart.prototype = { // Handle regular options var options, - seriesOptions = userOptions.series; // skip merging data points to increase performance + type, + seriesOptions = userOptions.series, // skip merging data points to increase performance + userPlotOptions = userOptions.plotOptions || {}; userOptions.series = null; options = merge(defaultOptions, userOptions); // do the merge + + // Override (by copy of user options) or clear tooltip options + // in chart.options.plotOptions (#6218) + for (type in options.plotOptions) { + options.plotOptions[type].tooltip = ( + userPlotOptions[type] && + merge(userPlotOptions[type].tooltip) // override by copy + ) || undefined; // or clear + } + // User options have higher priority than default options (#6218). + // In case of exporting: path is changed + options.tooltip.userOptions = (userOptions.chart && + userOptions.chart.forExport && userOptions.tooltip.userOptions) || + userOptions.tooltip; + options.series = userOptions.series = seriesOptions; // set back the series data this.userOptions = userOptions; diff --git a/js/parts/Dynamics.js b/js/parts/Dynamics.js index 1582ac16d56..48d8ad76e2c 100644 --- a/js/parts/Dynamics.js +++ b/js/parts/Dynamics.js @@ -191,7 +191,8 @@ extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ { * extended from plugins. */ propsRequireUpdateSeries: ['chart.inverted', 'chart.polar', - 'chart.ignoreHiddenSeries', 'chart.type', 'colors', 'plotOptions'], + 'chart.ignoreHiddenSeries', 'chart.type', 'colors', 'plotOptions', + 'tooltip'], /** * Chart.update function that takes the whole options stucture. @@ -248,6 +249,17 @@ extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ { } /*= } =*/ } + + // Moved up, because tooltip needs updated plotOptions (#6218) + /*= if (build.classic) { =*/ + if (options.colors) { + this.options.colors = options.colors; + } + /*= } =*/ + + if (options.plotOptions) { + merge(true, this.options.plotOptions, options.plotOptions); + } // Some option stuctures correspond one-to-one to chart objects that have // update methods, for example @@ -273,16 +285,6 @@ extend(Chart.prototype, /** @lends Highcharts.Chart.prototype */ { } } - /*= if (build.classic) { =*/ - if (options.colors) { - this.options.colors = options.colors; - } - /*= } =*/ - - if (options.plotOptions) { - merge(true, this.options.plotOptions, options.plotOptions); - } - // Setters for collections. For axes and series, each item is referred // by an id. If the id is not found, it defaults to the corresponding // item in the collection, so setting one series without an id, will diff --git a/js/parts/Series.js b/js/parts/Series.js index 5df681fa1c6..b1774e7d74e 100644 --- a/js/parts/Series.js +++ b/js/parts/Series.js @@ -405,16 +405,22 @@ H.Series = H.seriesType('line', null, { // base series options itemOptions ); - // The tooltip options are merged between global and series specific options + // The tooltip options are merged between global and series specific + // options. Importance order asscendingly: + // globals: (1)tooltip, (2)plotOptions.series, (3)plotOptions[this.type] + // init userOptions with possible later updates: 4-6 like 1-3 and + // (7)this series options this.tooltipOptions = merge( - defaultOptions.tooltip, - defaultOptions.plotOptions[this.type].tooltip, - userOptions.tooltip, - userPlotOptions.series && userPlotOptions.series.tooltip, - userPlotOptions[this.type] && userPlotOptions[this.type].tooltip, - itemOptions.tooltip + defaultOptions.tooltip, // 1 + defaultOptions.plotOptions.series && + defaultOptions.plotOptions.series.tooltip, // 2 + defaultOptions.plotOptions[this.type].tooltip, // 3 + chartOptions.tooltip.userOptions, // 4 + plotOptions.series && plotOptions.series.tooltip, // 5 + plotOptions[this.type].tooltip, // 6 + itemOptions.tooltip // 7 ); - + // When shared tooltip, stickyTracking is true by default, // unless user says otherwise. this.stickyTracking = pick( diff --git a/js/parts/Tooltip.js b/js/parts/Tooltip.js index 753bc00bd2a..47f3c9c3590 100644 --- a/js/parts/Tooltip.js +++ b/js/parts/Tooltip.js @@ -181,6 +181,8 @@ H.Tooltip.prototype = { update: function (options) { this.destroy(); + // Update user options (#6218) + merge(true, this.chart.options.tooltip.userOptions, options); this.init(this.chart, merge(true, this.options, options)); }, diff --git a/samples/unit-tests/axis/category/demo.js b/samples/unit-tests/axis/category/demo.js index d8c4213d9ca..6005592caf8 100644 --- a/samples/unit-tests/axis/category/demo.js +++ b/samples/unit-tests/axis/category/demo.js @@ -542,7 +542,9 @@ QUnit.test( }], plotOptions: { - animation: false + series: { + animation: false + } }, series: [{ diff --git a/samples/unit-tests/tooltip/options-order/demo.details b/samples/unit-tests/tooltip/options-order/demo.details new file mode 100644 index 00000000000..a86ee3a2f30 --- /dev/null +++ b/samples/unit-tests/tooltip/options-order/demo.details @@ -0,0 +1,6 @@ +--- + resources: + - https://code.jquery.com/qunit/qunit-2.0.1.js + - https://code.jquery.com/qunit/qunit-2.0.1.css + js_wrap: b +... \ No newline at end of file diff --git a/samples/unit-tests/tooltip/options-order/demo.html b/samples/unit-tests/tooltip/options-order/demo.html new file mode 100644 index 00000000000..8b772ab4e71 --- /dev/null +++ b/samples/unit-tests/tooltip/options-order/demo.html @@ -0,0 +1,6 @@ + + +
+
+ +
\ No newline at end of file diff --git a/samples/unit-tests/tooltip/options-order/demo.js b/samples/unit-tests/tooltip/options-order/demo.js new file mode 100644 index 00000000000..e473623ac1b --- /dev/null +++ b/samples/unit-tests/tooltip/options-order/demo.js @@ -0,0 +1,315 @@ +QUnit.test('Options importantance order static', function (assert) { + /* The importantance asscending order: + * 1) default / global -> tooltip + * 2) default / global -> plotOptions.series + * 3) default / global -> plotOptions. + * 4) user set -> tooltip + * 5) user set -> plotOptions.series + * 6) user set -> plotOptions. + * 7) user set -> series + */ + Highcharts.setOptions({ + tooltip: { + valueDecimals: '1', // 1) + padding: 1, + pointFormat: 'WRONG 1', + borderRadius: 1, + valueSuffix: 'WRONG 1', + footerFormat: 'WRONG 1', + valuePrefix: 'WRONG 1' + }, + plotOptions: { + series: { + tooltip: { + padding: 42, // 2) + pointFormat: 'WRONG 2', + borderRadius: 2, + valueSuffix: 'WRONG 2', + footerFormat: 'WRONG 2', + valuePrefix: 'WRONG 2' + } + }, + line: { + tooltip: { + pointFormat: 'point', // 3) + borderRadius: 3, + valueSuffix: 'WRONG 3', + footerFormat: 'WRONG 3', + valuePrefix: 'WRONG 3' + } + } + } + }); + + var chart = Highcharts.chart('container', { + tooltip: { + borderRadius: 20, // 4) + valueSuffix: 'WRONG 4', + footerFormat: 'WRONG 4', + valuePrefix: 'WRONG 4' + }, + plotOptions: { + series: { + tooltip: { + valueSuffix: ' suffix', // 5) + footerFormat: 'WRONG 5', + valuePrefix: 'WRONG 5' + } + }, + line: { + tooltip: { + footerFormat: 'foot', // 6) + valuePrefix: 'WRONG 6' + } + } + }, + series: [{ + data: [1.12345, 2, 3], + tooltip: { + valuePrefix: 'prefix ' // 7) + } + }] + }), + defaultOptions = Highcharts.getOptions(), + series = chart.series[0]; + + // 1) default / global -> tooltip + assert.strictEqual( + series.tooltipOptions.valueDecimals, + defaultOptions.tooltip.valueDecimals, + '1) defaultOptions.tooltip used' + ); + assert.strictEqual( + series.tooltipOptions.valueDecimals, + '1', + '...and 1) option was merged correctly' + ); + + // 2) default / global -> plotOptions.series + assert.strictEqual( + series.tooltipOptions.padding, + defaultOptions.plotOptions.series.tooltip.padding, + '2) defaultOptions.plotOptions.series used' + ); + assert.strictEqual( + series.tooltipOptions.padding, + 42, + '...and 2) option was merged correctly' + ); + + // 3) default / global -> plotOptions. + assert.strictEqual( + series.tooltipOptions.pointFormat, + defaultOptions.plotOptions.line.tooltip.pointFormat, + '3) defaultOptions.plotOptions. used' + ); + assert.strictEqual( + series.tooltipOptions.pointFormat, + 'point', + '...and 3) option was merged correctly' + ); + + // 4) user set -> tooltip + assert.strictEqual( + series.tooltipOptions.borderRadius, + chart.options.tooltip.userOptions.borderRadius, + '4) chart.options.tooltip.userOptions used' + ); + assert.strictEqual( + series.tooltipOptions.borderRadius, + 20, + '...and 4) option was merged correctly' + ); + + // 5) user set -> plotOptions.series + assert.strictEqual( + series.tooltipOptions.valueSuffix, + chart.options.plotOptions.series.tooltip.valueSuffix, + '5) chart.options.plotOptions.series used' + ); + assert.strictEqual( + series.tooltipOptions.valueSuffix, + ' suffix', + '...and 5) option was merged correctly' + ); + + // 6) user set -> plotOptions. + assert.strictEqual( + series.tooltipOptions.footerFormat, + chart.options.plotOptions.line.tooltip.footerFormat, + '6) chart.options.plotOptions. used' + ); + assert.strictEqual( + series.tooltipOptions.footerFormat, + 'foot', + '...and 6) option was merged correctly' + ); + + // 7) user set -> series + assert.strictEqual( + series.tooltipOptions.valuePrefix, + chart.series[0].userOptions.tooltip.valuePrefix, + '7) chart.series[n] used' + ); + assert.strictEqual( + series.tooltipOptions.valuePrefix, + 'prefix ', + '...and 7) option was merged correctly' + ); +}); + +QUnit.test('Options importantance order dynamic (#6218)', function (assert) { + /* The importantance asscending order: + * 1) default / global -> tooltip + * 2) default / global -> plotOptions.series + * 3) default / global -> plotOptions. + * 4) user set -> tooltip + * 5) user set -> plotOptions.series + * 6) user set -> plotOptions. + * 7) user set -> series + */ + Highcharts.setOptions({ + tooltip: { + headerFormat: '1' // 1) + }, + plotOptions: { + series: { + tooltip: { + //headerFormat: '2' // 2) + } + }, + line: { + tooltip: { + //headerFormat: '3' // 3) + } + } + } + }); + + var chart = Highcharts.chart('container', { + tooltip: { + //headerFormat: '4' // 4) + }, + plotOptions: { + series: { + tooltip: { + //headerFormat: '5' // 5) + } + }, + line: { + tooltip: { + //headerFormat: '6' // 6) + } + } + }, + series: [{ + data: [1.1234, 2, 3], + tooltip: { + //headerFormat: '7' // 7) + } + }] + }); + + // 1) default / global -> tooltip + assert.strictEqual( + chart.series[0].tooltipOptions.headerFormat, + '1', + '1) defaultOptions.tooltip used' + ); + + Highcharts.setOptions({ + plotOptions: { + series: { + tooltip: { + headerFormat: '2' + } + } + } + }); + chart.series[0].update({}); + + // 2) default / global -> plotOptions.series + assert.strictEqual( + chart.series[0].tooltipOptions.headerFormat, + '2', + '2) defaultOptions.plotOptions.series used' + ); + + Highcharts.setOptions({ + plotOptions: { + line: { + tooltip: { + headerFormat: '3' + } + } + } + }); + chart.series[0].update({}); + + // 3) default / global -> plotOptions. + assert.strictEqual( + chart.series[0].tooltipOptions.headerFormat, + '3', + '3) defaultOptions.plotOptions. used' + ); + + chart.update({ + tooltip: { + headerFormat: '4' + } + }); + + // 4) user set -> tooltip + assert.strictEqual( + chart.series[0].tooltipOptions.headerFormat, + '4', + '4) chart.options.tooltip.userOptions used' + ); + + chart.update({ + plotOptions: { + series: { + tooltip: { + headerFormat: '5' + } + } + } + }); + + // 5) user set -> plotOptions.series + assert.strictEqual( + chart.series[0].tooltipOptions.headerFormat, + '5', + '5) chart.options.plotOptions.series used' + ); + + chart.update({ + plotOptions: { + line: { + tooltip: { + headerFormat: '6' + } + } + } + }); + + // 6) user set -> plotOptions. + assert.strictEqual( + chart.series[0].tooltipOptions.headerFormat, + '6', + '6) chart.options.plotOptions. used' + ); + + chart.series[0].update({ + tooltip: { + headerFormat: '7' + } + }); + + // 7) user set -> series + assert.strictEqual( + chart.series[0].tooltipOptions.headerFormat, + '7', + '7) chart.series[n] used' + ); +}); diff --git a/samples/unit-tests/tooltip/split/demo.js b/samples/unit-tests/tooltip/split/demo.js index 77f5b8c403e..e4fa3569f8c 100644 --- a/samples/unit-tests/tooltip/split/demo.js +++ b/samples/unit-tests/tooltip/split/demo.js @@ -1,97 +1,102 @@ QUnit.test('tooltip.destroy #5855', function (assert) { - var chart = Highcharts.chart('container', { - series: [{ - data: [1, 2, 3] - }, { - data: [3, 2, 1] - }], - tooltip: { - split: true - } - }), - series1 = chart.series[0], - series2 = chart.series[1], - p1 = series1.points[0], - p2 = series2.points[0], - tooltip = chart.tooltip; - tooltip.refresh([p1, p2]); - assert.strictEqual( - typeof series1.tt, - 'object', - 'series[0].tt is exists' - ); - assert.strictEqual( - typeof series2.tt, - 'object', - 'series[1].tt is exists' - ); - assert.strictEqual( - typeof tooltip.tt, - 'object', - 'tooltip.tt is exists' - ); + var chart = Highcharts.chart('container', { + series: [{ + data: [1, 2, 3] + }, { + data: [3, 2, 1] + }], + tooltip: { + split: true + } + }), + series1 = chart.series[0], + series2 = chart.series[1], + p1 = series1.points[0], + p2 = series2.points[0], + tooltip = chart.tooltip; + tooltip.refresh([p1, p2]); + assert.strictEqual( + typeof series1.tt, + 'object', + 'series[0].tt is exists' + ); + assert.strictEqual( + typeof series2.tt, + 'object', + 'series[1].tt is exists' + ); + assert.strictEqual( + typeof tooltip.tt, + 'object', + 'tooltip.tt is exists' + ); - tooltip.destroy(); + tooltip.destroy(); - assert.strictEqual( - series1.tt, - undefined, - 'series[0].tt is destroyed' - ); - assert.strictEqual( - series2.tt, - undefined, - 'series[1].tt is destroyed' - ); - assert.strictEqual( - tooltip.tt, - undefined, - 'tooltip.tt is destroyed' - ); + assert.strictEqual( + series1.tt, + undefined, + 'series[0].tt is destroyed' + ); + assert.strictEqual( + series2.tt, + undefined, + 'series[1].tt is destroyed' + ); + assert.strictEqual( + tooltip.tt, + undefined, + 'tooltip.tt is destroyed' + ); }); QUnit.test('Split tooltip and tooltip.style. #5838', function (assert) { - var chart = Highcharts.chart('container', { - series: [{ - data: [1, 2, 3] - }, { - data: [3, 2, 1] + var chart = Highcharts.chart('container', { + series: [{ + data: [1, 2, 3] + }, { + data: [3, 2, 1] - }], - tooltip: { - split: true - } - }), - series1 = chart.series[0], - series2 = chart.series[1], - p1 = series1.points[0], - p2 = series2.points[0], - el, - value; + }], + tooltip: { + split: true + } + }), + series1 = chart.series[0], + series2 = chart.series[1], + p1 = series1.points[0], + p2 = series2.points[0], + el, + value; - chart.tooltip.refresh([p1, p2]); - el = chart.tooltip.tt.text.element; - value = window.getComputedStyle(el).getPropertyValue('color'); - assert.strictEqual( - value, - 'rgb(51, 51, 51)', - 'tooltip default color.' - ); + chart.tooltip.refresh([p1, p2]); + el = chart.tooltip.tt.text.element; + value = window.getComputedStyle(el).getPropertyValue('color'); + assert.strictEqual( + value, + 'rgb(51, 51, 51)', + 'tooltip default color.' + ); - chart.update({ - tooltip: { - style: { - color: '#FF0000' - } - } - }); - chart.tooltip.refresh([p1, p2]); - el = chart.tooltip.tt.text.element; - value = window.getComputedStyle(el).getPropertyValue('color'); - assert.strictEqual( - value, - 'rgb(255, 0, 0)', - 'tooltip color from style.' - ); + chart.update({ + tooltip: { + style: { + color: '#FF0000' + } + } + }); + + chart.tooltip.refresh([ + chart.series[0].points[0], + chart.series[1].points[0] + ]); + + el = chart.tooltip.tt.text.element; + value = window.getComputedStyle(el).getPropertyValue('color'); + assert.strictEqual( + value, + 'rgb(255, 0, 0)', + 'tooltip color from style.' + ); });