Skip to content

Commit 383b723

Browse files
committed
simpler tooltips
1 parent e343651 commit 383b723

File tree

7 files changed

+144
-172
lines changed

7 files changed

+144
-172
lines changed

samples/datalimits.html

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,11 @@
5656
display: true,
5757
text: 'Chart.js Box Plot Chart',
5858
},
59-
datasets: {
60-
minStats: 'min',
61-
maxStats: 'max',
59+
boxplot: {
60+
datasets: {
61+
minStats: 'min',
62+
maxStats: 'max',
63+
},
6264
},
6365
};
6466

@@ -71,17 +73,17 @@
7173
});
7274

7375
document.getElementById('limitMinMax').onclick = function () {
74-
options.scales.yAxes[0].ticks.minStats = 'min';
75-
options.scales.yAxes[0].ticks.maxStats = 'max';
76+
options.boxplot.datasets.minStats = 'min';
77+
options.boxplot.datasets.maxStats = 'max';
7678
window.myBar = new Chart(ctx, {
7779
type: 'boxplot',
7880
data: boxplotData,
7981
options: options,
8082
});
8183
};
8284
document.getElementById('limitWhiskers').onclick = function () {
83-
options.scales.yAxes[0].ticks.minStats = 'whiskerMin';
84-
options.scales.yAxes[0].ticks.maxStats = 'whiskerMax';
85+
options.boxplot.datasets.minStats = 'whiskerMin';
86+
options.boxplot.datasets.maxStats = 'whiskerMax';
8587
window.myBar = new Chart(ctx, {
8688
type: 'boxplot',
8789
data: boxplotData,

samples/default.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
borderWidth: 1,
4040
data: samples.boxplots({ count: 7, random: random }),
4141
outlierColor: '#999999',
42-
// lowerColor: '#461e7d',
4342
},
4443
{
4544
label: 'Dataset 2',

src/controllers/base.js

Lines changed: 41 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,7 @@
1-
import {} from 'chart.js';
2-
import { interpolateNumberArray } from '../animation';
1+
import { interpolateNumberArray } from '../animation';
2+
import { outlierPositioner, patchInHoveredOutlier } from '../tooltip';
33

4-
export function toFixed(value) {
5-
const decimals = this._chart.config.options.tooltipDecimals; // inject number of decimals from config
6-
if (decimals == null || typeof decimals !== 'number' || decimals < 0) {
7-
return value;
8-
}
9-
return value.toFixed(decimals);
10-
}
11-
12-
export const baseDefaults = {
13-
datasets: {
14-
animation: {
15-
numberArray: {
16-
fn: interpolateNumberArray,
17-
properties: ['outliers', 'items'],
18-
},
19-
},
20-
},
21-
};
22-
23-
const configKeys = [
4+
export const configKeys = [
245
'outlierRadius',
256
'itemRadius',
267
'itemStyle',
@@ -32,38 +13,41 @@ const configKeys = [
3213
'outlierHitRadius',
3314
'lowerColor',
3415
];
35-
// const configKeyIsColor = [false, false, false, true, true, true, true, false, false, true];
36-
37-
const array = {
38-
updateElement(elem, index, reset) {
39-
const dataset = this.getDataset();
40-
const custom = elem.custom || {};
41-
const options = this._elementOptions();
42-
43-
Chart.controllers.bar.prototype.updateElement.call(this, elem, index, reset);
44-
const resolve = Chart.helpers.options.resolve;
45-
46-
// Scriptable options
47-
const context = {
48-
chart: this.chart,
49-
dataIndex: index,
50-
dataset,
51-
datasetIndex: this.index,
52-
};
53-
54-
configKeys.forEach((item) => {
55-
elem._model[item] = resolve([custom[item], dataset[item], options[item]], context, index);
56-
});
57-
},
58-
_calculateCommonModel(r, data, container, scale) {
59-
if (container.outliers) {
60-
r.outliers = container.outliers.map((d) => scale.getPixelForValue(Number(d)));
61-
}
62-
63-
if (Array.isArray(data)) {
64-
r.items = data.map((d) => scale.getPixelForValue(Number(d)));
65-
} else if (container.items) {
66-
r.items = container.items.map((d) => scale.getPixelForValue(Number(d)));
67-
}
68-
},
69-
};
16+
export const colorStyleKeys = ['borderColor', 'backgroundColor'].concat(configKeys.filter((c) => c.endsWith('Color')));
17+
18+
export function baseDefaults() {
19+
return {
20+
datasets: {
21+
animation: {
22+
numberArray: {
23+
fn: interpolateNumberArray,
24+
properties: ['outliers', 'items'],
25+
},
26+
colors: {
27+
type: 'color',
28+
properties: colorStyleKeys,
29+
},
30+
show: {
31+
colors: {
32+
type: 'color',
33+
properties: colorStyleKeys,
34+
from: 'transparent',
35+
},
36+
},
37+
hide: {
38+
colors: {
39+
type: 'color',
40+
properties: colorStyleKeys,
41+
to: 'transparent',
42+
},
43+
},
44+
},
45+
},
46+
tooltips: {
47+
position: outlierPositioner.register().id,
48+
callbacks: {
49+
beforeLabel: patchInHoveredOutlier,
50+
},
51+
},
52+
};
53+
}

src/controllers/boxplot.js

Lines changed: 57 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,8 @@
1-
import { asBoxPlotStats } from '../data';
1+
import { asBoxPlotStats, defaultStatsOptions } from '../data';
22
import { controllers, helpers, defaults } from 'chart.js';
3-
import { baseDefaults, toFixed } from './base';
4-
import { boxplotPositioner } from '../tooltip';
3+
import { baseDefaults, configKeys } from './base';
54
import { BoxAndWiskers, boxOptionsKeys } from '../elements';
65

7-
function boxplotTooltip(item, data, ...args) {
8-
const value = data.datasets[item.datasetIndex].data[item.index];
9-
const options = this._chart.getDatasetMeta(item.datasetIndex).controller._getValueScale().options.ticks;
10-
const b = asBoxPlotStats(value, options);
11-
12-
const hoveredOutlierIndex = this._tooltipOutlier == null ? -1 : this._tooltipOutlier;
13-
14-
const label = this._options.callbacks.boxplotLabel;
15-
return label.apply(this, [item, data, b, hoveredOutlierIndex].concat(args));
16-
}
17-
186
// Chart.defaults.horizontalBoxplot = Chart.helpers.merge({}, [
197
// Chart.defaults.horizontalBar,
208
// horizontalDefaults,
@@ -23,64 +11,77 @@ function boxplotTooltip(item, data, ...args) {
2311

2412
export class BoxPlot extends controllers.bar {
2513
getMinMax(scale, canStack) {
26-
const r = super.getMinMax(scale, canStack);
27-
// TODO adapt scale.axis to the target stats
28-
return r;
14+
const bak = scale.axis;
15+
const config = this._config;
16+
scale.axis = config.minStats;
17+
const min = super.getMinMax(scale, canStack).min;
18+
scale.axis = config.maxStats;
19+
const max = super.getMinMax(scale, canStack).max;
20+
scale.axis = bak;
21+
return { min, max };
2922
}
30-
31-
parseObjectData(meta, data, start, count) {
32-
const r = super.parseObjectData(meta, data, start, count);
23+
parseArrayData(meta, data, start, count) {
3324
const vScale = meta.vScale;
3425
const iScale = meta.iScale;
3526
const labels = iScale.getLabels();
27+
const r = [];
3628
for (let i = 0; i < count; i++) {
3729
const index = i + start;
38-
const parsed = r[i];
30+
const parsed = {};
3931
parsed[iScale.axis] = iScale.parse(labels[index], index);
40-
const stats = asBoxPlotStats(data[index]); // TODO options
32+
const stats = asBoxPlotStats(data[index], this._config);
4133
if (stats) {
4234
Object.assign(parsed, stats);
4335
parsed[vScale.axis] = stats.median;
4436
}
37+
r.push(parsed);
4538
}
4639
return r;
4740
}
4841

42+
parseObjectData(meta, data, start, count) {
43+
return this.parseArrayData(meta, data, start, count);
44+
}
45+
4946
getLabelAndValue(index) {
5047
const r = super.getLabelAndValue(index);
5148
const vScale = this._cachedMeta.vScale;
5249
const parsed = this.getParsed(index);
5350
if (!vScale || !parsed) {
5451
return r;
5552
}
56-
const v = (v) => vScale.getLabelForValue(v);
57-
r.value = Object.assign(
58-
{
59-
toString() {
60-
// custom to string function for the 'value'
61-
return `(min: ${v(this.min)}, 25% quantile: ${v(this.q1)}, median: ${v(this.median)}, 75% quantile: ${v(
62-
this.q3
63-
)}, max: ${v(this.max)})`;
64-
},
53+
r.value = {
54+
raw: parsed,
55+
hoveredOutlierIndex: -1,
56+
toString() {
57+
if (this.hoveredOutlierIndex >= 0) {
58+
return `(outlier: ${this.outliers[this.hoveredOutlierIndex]})`;
59+
}
60+
// custom to string function for the 'value'
61+
return `(min: ${this.min}, 25% quantile: ${this.q1}, median: ${this.median}, 75% quantile: ${this.q3}, max: ${this.max})`;
6562
},
66-
parsed
67-
);
63+
};
64+
this._transformBoxplot(r.value, parsed, (v) => vScale.getLabelForValue(v));
6865
return r;
6966
}
7067

68+
_transformBoxplot(target, source, mapper) {
69+
for (const key of ['min', 'max', 'median', 'q3', 'q1', 'whiskerMin', 'whiskerMax']) {
70+
target[key] = mapper(source[key]);
71+
}
72+
for (const key of ['outliers', 'items']) {
73+
if (Array.isArray(source[key])) {
74+
target[key] = source[key].map(mapper);
75+
}
76+
}
77+
}
78+
7179
updateElement(rectangle, index, properties, mode) {
7280
const reset = mode === 'reset';
7381
const scale = this._cachedMeta.vScale;
7482
const parsed = this.getParsed(index);
7583
const base = scale.getBasePixel();
76-
for (const key of ['median', 'q3', 'q1', 'whiskerMin', 'whiskerMax']) {
77-
properties[key] = reset ? base : scale.getPixelForValue(parsed[key]);
78-
}
79-
for (const key of ['outliers', 'items']) {
80-
if (Array.isArray(parsed[key])) {
81-
properties[key] = parsed[key].map((v) => (reset ? base : scale.getPixelForValue(v)));
82-
}
83-
}
84+
this._transformBoxplot(properties, parsed, (v) => (reset ? base : scale.getPixelForValue(v)));
8485
super.updateElement(rectangle, index, properties, mode);
8586
}
8687
}
@@ -94,48 +95,24 @@ BoxPlot.register = () => {
9495
BoxPlot.id,
9596
helpers.merge({}, [
9697
defaults.bar,
97-
baseDefaults,
98+
baseDefaults(),
9899
{
99-
datasets: {
100-
animation: {
101-
numbers: {
102-
type: 'number',
103-
properties: defaults.bar.datasets.animation.numbers.properties.concat([
104-
'q1',
105-
'q3',
106-
'min',
107-
'max',
108-
'median',
109-
'whiskerMin',
110-
'whiskerMax',
111-
]),
100+
datasets: Object.assign(
101+
{
102+
minStats: 'min',
103+
maxStats: 'max',
104+
animation: {
105+
numbers: {
106+
type: 'number',
107+
properties: defaults.bar.datasets.animation.numbers.properties.concat(
108+
['q1', 'q3', 'min', 'max', 'median', 'whiskerMin', 'whiskerMax'],
109+
configKeys.filter((c) => !c.endsWith('Color'))
110+
),
111+
},
112112
},
113113
},
114-
},
115-
tooltips: {
116-
// position: boxplotPositioner.register().id,
117-
// callbacks: {
118-
// label: boxplotTooltip,
119-
// boxplotLabel(item, data, b, hoveredOutlierIndex) {
120-
// const datasetLabel = data.datasets[item.datasetIndex].label || '';
121-
// let label = `${datasetLabel} ${typeof item.xLabel === 'string' ? item.xLabel : item.yLabel}`;
122-
// if (!b) {
123-
// return `${label} (NaN)`;
124-
// }
125-
// if (hoveredOutlierIndex >= 0) {
126-
// const outlier = b.outliers[hoveredOutlierIndex];
127-
// return `${label} (outlier: ${toFixed.call(this, outlier)})`;
128-
// }
129-
// return `${label} (min: ${toFixed.call(this, b.min)}, q1: ${toFixed.call(
130-
// this,
131-
// b.q1
132-
// )}, median: ${toFixed.call(this, b.median)}, q3: ${toFixed.call(this, b.q3)}, max: ${toFixed.call(
133-
// this,
134-
// b.max
135-
// )})`;
136-
// },
137-
// },
138-
},
114+
defaultStatsOptions
115+
),
139116
},
140117
])
141118
);

src/data.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ export function whiskers(boxplot, arr, coef = 1.5) {
131131
};
132132
}
133133

134-
const defaultStatsOptions = {
134+
export const defaultStatsOptions = {
135135
coef: 1.5,
136136
quantiles: 7,
137137
};

src/elements/base.js

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -196,31 +196,32 @@ export class ArrayElementBase extends Element {
196196
}
197197

198198
_getOutliers(useFinalPosition) {
199-
const outliers = this.getProps(['outliers'], useFinalPosition);
200-
return outliers || [];
199+
const props = this.getProps(['outliers'], useFinalPosition);
200+
return props.outliers || [];
201201
}
202202

203-
tooltipPosition(eventPosition, tooltip, useFinalPosition) {
203+
tooltipPosition(eventPosition, tooltip) {
204204
if (!eventPosition) {
205205
// fallback
206-
return this.getCenterPoint(useFinalPosition);
206+
return this.getCenterPoint();
207207
}
208208
delete tooltip._tooltipOutlier;
209209

210-
const props = this.getProps(['x', 'y'], useFinalPosition);
210+
const props = this.getProps(['x', 'y']);
211211
const index = this._outlierIndexInRange(eventPosition.x, eventPosition.y);
212212
if (index < 0) {
213-
return this.getCenterPoint(useFinalPosition);
213+
return this.getCenterPoint();
214214
}
215+
// hack in the data of the hovered outlier
215216
tooltip._tooltipOutlier = index;
216217
if (this.isVertical()) {
217218
return {
218219
x: props.x,
219-
y: this._getOutliers(useFinalPosition)[index],
220+
y: this._getOutliers()[index],
220221
};
221222
}
222223
return {
223-
x: this._getOutliers(useFinalPosition)[index],
224+
x: this._getOutliers()[index],
224225
y: props.y,
225226
};
226227
}

0 commit comments

Comments
 (0)