Skip to content
This repository was archived by the owner on Nov 25, 2021. It is now read-only.

Commit 3ef0424

Browse files
author
Christian Lehner
authored
Merge pull request #52 from datavisyn/sgratzl/outliertooltip
Outlier Tooltips
2 parents 1468051 + 2278f02 commit 3ef0424

File tree

13 files changed

+342
-21
lines changed

13 files changed

+342
-21
lines changed

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,14 @@ interface IBaseStyling {
116116
* @indexable
117117
*/
118118
hitPadding: number;
119+
120+
/**
121+
* hit radius for hit test of outliers
122+
* @default 4
123+
* @scriptable
124+
* @indexable
125+
*/
126+
outlierHitRadius: number;
119127
}
120128

121129
interface IBoxPlotStyling extends IBaseStyling {
@@ -218,6 +226,42 @@ interface IViolinItem extends IBaseItem {
218226

219227
**Note**: The statistics will be cached within the array. Thus, if you manipulate the array content without creating a new instance the changes won't be reflected in the stats. See also [CodePen](https://codepen.io/sgratzl/pen/JxQVaZ?editors=0010) for a comparison.
220228

229+
## Tooltips
230+
231+
In order to simplify the customization of the tooltips, two additional tooltip callback methods are available. Internally the `label` callback will call the correspondig callback depending on the type.
232+
233+
```js
234+
arr = {
235+
options: {
236+
tooltips: {
237+
callbacks: {
238+
/**
239+
* custom callback for boxplot tooltips
240+
* @param item see label callback
241+
* @param data see label callback
242+
* @param stats {IBoxPlotItem} the stats of the hovered element
243+
* @param hoveredOutlierIndex {number} the hovered outlier index or -1 if none
244+
* @return {string} see label callback
245+
*/
246+
boxplotLabel: function(item, data, stats, hoveredOutlierIndex) {
247+
return 'Custom tooltip';
248+
},
249+
/**
250+
* custom callback for violin tooltips
251+
* @param item see label callback
252+
* @param data see label callback
253+
* @param stats {IViolinItem} the stats of the hovered element
254+
* @return {string} see label callback
255+
*/
256+
violinLabel: function(item, data, stats) {
257+
return 'Custom tooltip';
258+
},
259+
}
260+
}
261+
}
262+
}
263+
```
264+
221265
## Building
222266

223267
```sh

samples/chartjs291.html

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<!doctype html>
2+
<html>
3+
4+
<head>
5+
<title>Box Plot Chart</title>
6+
<script src="https://unpkg.com/[email protected]"></script>
7+
<script src="../build/Chart.BoxPlot.js" type="text/javascript"></script>
8+
<script src="https://unpkg.com/d3-random@latest/dist/d3-random.js"></script>
9+
<script src="./utils.js"></script>
10+
<style>
11+
canvas {
12+
-moz-user-select: none;
13+
-webkit-user-select: none;
14+
-ms-user-select: none;
15+
}
16+
</style>
17+
</head>
18+
19+
<body>
20+
<div id="container" style="width: 75%;">
21+
<canvas id="canvas"></canvas>
22+
</div>
23+
<button id="randomizeData">Randomize Data</button>
24+
<button id="addDataset">Add Dataset</button>
25+
<button id="removeDataset">Remove Dataset</button>
26+
<button id="addData">Add Data</button>
27+
<button id="removeData">Remove Data</button>
28+
<script>
29+
const samples = this.Samples.utils;
30+
var color = Chart.helpers.color;
31+
var b = d3.randomNormal();
32+
var random = (min, max) => () => (b() * ((max || 1) - (min || 0))) + (min || 0);
33+
var boxplotData = {
34+
labels: samples.months({count: 7}),
35+
datasets: [{
36+
label: 'Dataset 1',
37+
backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(),
38+
borderColor: window.chartColors.red,
39+
borderWidth: 1,
40+
data: samples.boxplots({count: 7, random: random}),
41+
outlierColor: '#999999'
42+
}, {
43+
label: 'Dataset 2',
44+
backgroundColor: color(window.chartColors.blue).alpha(0.5).rgbString(),
45+
borderColor: window.chartColors.blue,
46+
borderWidth: 1,
47+
data: samples.boxplotsArray({count: 7, random: random}),
48+
outlierColor: '#999999'
49+
}]
50+
51+
};
52+
53+
window.onload = function() {
54+
var ctx = document.getElementById("canvas").getContext("2d");
55+
window.myBar = new Chart(ctx, {
56+
type: 'boxplot',
57+
data: boxplotData,
58+
options: {
59+
responsive: true,
60+
legend: {
61+
position: 'top',
62+
},
63+
title: {
64+
display: true,
65+
text: 'Chart.js Box Plot Chart'
66+
},
67+
scales: {
68+
xAxes: [{
69+
// Specific to Bar Controller
70+
categoryPercentage: 0.9,
71+
barPercentage: 0.8
72+
}]
73+
}
74+
}
75+
});
76+
77+
};
78+
79+
document.getElementById('randomizeData').addEventListener('click', function() {
80+
boxplotData.datasets.forEach(function(dataset) {
81+
dataset.data = samples.boxplots({count: 7});
82+
83+
});
84+
window.myBar.update();
85+
});
86+
87+
var colorNames = Object.keys(window.chartColors);
88+
document.getElementById('addDataset').addEventListener('click', function() {
89+
var colorName = colorNames[boxplotData.datasets.length % colorNames.length];
90+
var dsColor = window.chartColors[colorName];
91+
var newDataset = {
92+
label: 'Dataset ' + boxplotData.datasets.length,
93+
backgroundColor: color(dsColor).alpha(0.5).rgbString(),
94+
borderColor: dsColor,
95+
borderWidth: 1,
96+
data: samples.boxplots({count: boxplotData.labels.length})
97+
};
98+
99+
boxplotData.datasets.push(newDataset);
100+
window.myBar.update();
101+
});
102+
103+
document.getElementById('addData').addEventListener('click', function() {
104+
if (boxplotData.datasets.length > 0) {
105+
var month = samples.nextMonth(boxplotData.labels.length);
106+
boxplotData.labels.push(month);
107+
108+
for (var index = 0; index < boxplotData.datasets.length; ++index) {
109+
//window.myBar.addData(randomBoxPlot(), index);
110+
boxplotData.datasets[index].data.push(samples.randomBoxPlot());
111+
}
112+
113+
window.myBar.update();
114+
}
115+
});
116+
117+
document.getElementById('removeDataset').addEventListener('click', function() {
118+
boxplotData.datasets.splice(0, 1);
119+
window.myBar.update();
120+
});
121+
122+
document.getElementById('removeData').addEventListener('click', function() {
123+
boxplotData.labels.splice(-1, 1); // remove the label first
124+
125+
boxplotData.datasets.forEach(function(dataset, datasetIndex) {
126+
dataset.data.pop();
127+
});
128+
129+
window.myBar.update();
130+
});
131+
</script>
132+
</body>
133+
134+
</html>

src/controllers/base.js

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export function toFixed(value) {
2525
return Number.parseFloat(value).toFixed(decimals);
2626
}
2727

28+
const configKeys = ['outlierRadius', 'itemRadius', 'itemStyle', 'itemBackgroundColor', 'itemBorderColor', 'outlierColor', 'medianColor', 'hitPadding', 'outlierHitRadius'];
29+
const configKeyIsColor = [false, false, false, true, true, true, true, false, false];
30+
2831
const array = {
2932
_elementOptions() {
3033
return {};
@@ -37,7 +40,6 @@ const array = {
3740
Chart.controllers.bar.prototype.updateElement.call(this, elem, index, reset);
3841
const resolve = Chart.helpers.options.resolve;
3942

40-
const keys = ['outlierRadius', 'itemRadius', 'itemStyle', 'itemBackgroundColor', 'itemBorderColor', 'outlierColor', 'medianColor', 'hitPadding'];
4143
// Scriptable options
4244
const context = {
4345
chart: this.chart,
@@ -46,7 +48,7 @@ const array = {
4648
datasetIndex: this.index
4749
};
4850

49-
keys.forEach((item) => {
51+
configKeys.forEach((item) => {
5052
elem._model[item] = resolve([custom[item], dataset[item], options[item]], context, index);
5153
});
5254
},
@@ -60,6 +62,24 @@ const array = {
6062
} else if (container.items) {
6163
r.items = container.items.map((d) => scale.getPixelForValue(Number(d)));
6264
}
65+
},
66+
setHoverStyle(element) {
67+
Chart.controllers.bar.prototype.setHoverStyle.call(this, element);
68+
69+
const dataset = this.chart.data.datasets[element._datasetIndex];
70+
const index = element._index;
71+
const custom = element.custom || {};
72+
const model = element._model;
73+
const getHoverColor = Chart.helpers.getHoverColor;
74+
const resolve = Chart.helpers.options.resolve;
75+
76+
77+
configKeys.forEach((item, i) => {
78+
element.$previousStyle[item] = model[item];
79+
const hoverKey = `hover${item.charAt(0).toUpperCase()}${item.slice(1)}`;
80+
const modelValue = configKeyIsColor[i] && model[item] != null ? getHoverColor(model[item]) : model[item];
81+
element._model[item] = resolve([custom[hoverKey], dataset[hoverKey], modelValue], undefined, index);
82+
});
6383
}
6484
};
6585

src/controllers/boxplot.js

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,33 @@ import {asBoxPlotStats} from '../data';
44
import * as Chart from 'chart.js';
55
import base, {verticalDefaults, horizontalDefaults, toFixed} from './base';
66

7+
8+
function boxplotTooltip(item, data, ...args) {
9+
const value = data.datasets[item.datasetIndex].data[item.index];
10+
const options = this._chart.getDatasetMeta(item.datasetIndex).controller._getValueScale().options.ticks;
11+
const b = asBoxPlotStats(value, options);
12+
13+
const hoveredOutlierIndex = this._tooltipOutlier == null ? -1 : this._tooltipOutlier;
14+
15+
const label = this._options.callbacks.boxplotLabel;
16+
return label.apply(this, [item, data, b, hoveredOutlierIndex, ...args]);
17+
}
18+
719
const defaults = {
820
tooltips: {
21+
position: 'boxplot',
922
callbacks: {
10-
label(item, data) {
23+
label: boxplotTooltip,
24+
boxplotLabel(item, data, b, hoveredOutlierIndex) {
1125
const datasetLabel = data.datasets[item.datasetIndex].label || '';
12-
const value = data.datasets[item.datasetIndex].data[item.index];
13-
const options = this._chart.getDatasetMeta(item.datasetIndex).controller._getValueScale().options.ticks;
14-
const b = asBoxPlotStats(value, options);
1526
let label = `${datasetLabel} ${typeof item.xLabel === 'string' ? item.xLabel : item.yLabel}`;
1627
if (!b) {
1728
return `${label} (NaN)`;
1829
}
30+
if (hoveredOutlierIndex >= 0) {
31+
const outlier = b.outliers[hoveredOutlierIndex];
32+
return `${label} (outlier: ${toFixed.call(this, outlier)})`;
33+
}
1934
return `${label} (min: ${toFixed.call(this, b.min)}, q1: ${toFixed.call(this, b.q1)}, median: ${toFixed.call(this, b.median)}, q3: ${toFixed.call(this, b.q3)}, max: ${toFixed.call(this, b.max)})`;
2035
}
2136
}
@@ -25,6 +40,13 @@ const defaults = {
2540
Chart.defaults.boxplot = Chart.helpers.merge({}, [Chart.defaults.bar, verticalDefaults, defaults]);
2641
Chart.defaults.horizontalBoxplot = Chart.helpers.merge({}, [Chart.defaults.horizontalBar, horizontalDefaults, defaults]);
2742

43+
if (Chart.defaults.global.datasets && Chart.defaults.global.datasets.bar) {
44+
Chart.defaults.global.datasets.boxplot = {...Chart.defaults.global.datasets.bar};
45+
}
46+
if (Chart.defaults.global.datasets && Chart.defaults.global.datasets.horizontalBar) {
47+
Chart.defaults.global.datasets.horizontalBoxplot = {...Chart.defaults.global.datasets.horizontalBar};
48+
}
49+
2850
const boxplot = {
2951
...base,
3052
dataElementType: Chart.elements.BoxAndWhiskers,
@@ -35,8 +57,8 @@ const boxplot = {
3557
/**
3658
* @private
3759
*/
38-
_updateElementGeometry(elem, index, reset) {
39-
Chart.controllers.bar.prototype._updateElementGeometry.call(this, elem, index, reset);
60+
_updateElementGeometry(elem, index, reset, ...args) {
61+
Chart.controllers.bar.prototype._updateElementGeometry.call(this, elem, index, reset, ...args);
4062
elem._model.boxplot = this._calculateBoxPlotValuesPixels(this.index, index);
4163
},
4264

src/controllers/violin.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,21 @@ import {asViolinStats} from '../data';
44
import * as Chart from 'chart.js';
55
import base, {verticalDefaults, horizontalDefaults, toFixed} from './base';
66

7+
8+
function violinTooltip(item, data, ...args) {
9+
const value = data.datasets[item.datasetIndex].data[item.index];
10+
const options = this._chart.getDatasetMeta(item.datasetIndex).controller._getValueScale().options.ticks;
11+
const v = asViolinStats(value, options);
12+
13+
const label = this._options.callbacks.violinLabel;
14+
return label.apply(this, [item, data, v, ...args]);
15+
}
16+
717
const defaults = {
818
tooltips: {
919
callbacks: {
10-
label(item, data) {
20+
label: violinTooltip,
21+
violinLabel(item, data) {
1122
const datasetLabel = data.datasets[item.datasetIndex].label || '';
1223
const value = item.value;
1324
const label = `${datasetLabel} ${typeof item.xLabel === 'string' ? item.xLabel : item.yLabel}`;
@@ -20,6 +31,13 @@ const defaults = {
2031
Chart.defaults.violin = Chart.helpers.merge({}, [Chart.defaults.bar, verticalDefaults, defaults]);
2132
Chart.defaults.horizontalViolin = Chart.helpers.merge({}, [Chart.defaults.horizontalBar, horizontalDefaults, defaults]);
2233

34+
if (Chart.defaults.global.datasets && Chart.defaults.global.datasets.bar) {
35+
Chart.defaults.global.datasets.violin = {...Chart.defaults.global.datasets.bar};
36+
}
37+
if (Chart.defaults.global.datasets && Chart.defaults.global.datasets.horizontalBar) {
38+
Chart.defaults.global.datasets.horizontalViolin = {...Chart.defaults.global.datasets.horizontalBar};
39+
}
40+
2341
const controller = {
2442
...base,
2543
dataElementType: Chart.elements.Violin,
@@ -30,8 +48,8 @@ const controller = {
3048
/**
3149
* @private
3250
*/
33-
_updateElementGeometry(elem, index, reset) {
34-
Chart.controllers.bar.prototype._updateElementGeometry.call(this, elem, index, reset);
51+
_updateElementGeometry(elem, index, reset, ...args) {
52+
Chart.controllers.bar.prototype._updateElementGeometry.call(this, elem, index, reset, ...args);
3553
const custom = elem.custom || {};
3654
const options = this._elementOptions();
3755
elem._model.violin = this._calculateViolinValuesPixels(this.index, index, custom.points !== undefined ? custom.points : options.points);

src/data.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,7 @@ export function boxplotStats(arr, options) {
133133
export function violinStats(arr, options) {
134134
// console.assert(Array.isArray(arr));
135135
if (arr.length === 0) {
136-
return {
137-
outliers: []
138-
};
136+
return {};
139137
}
140138
arr = arr.filter((v) => typeof v === 'number' && !isNaN(v));
141139
arr.sort((a, b) => a - b);

0 commit comments

Comments
 (0)