1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
| // Plugin code
(function (Highcharts) {
Highcharts.wrap(Highcharts.Tooltip.prototype, 'hide', function (proceed) {
var tooltip = this.chart.options.tooltip;
// Run the original proceed method
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
if (!this.isHidden && tooltip.events && tooltip.events.hide) {
tooltip.events.hide();
}
});
Highcharts.wrap(Highcharts.Tooltip.prototype, 'refresh', function (proceed) {
var tooltip = this.chart.options.tooltip;
// Run the original proceed method
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
if (tooltip.events && tooltip.events.show) {
tooltip.events.show(this.chart.hoverPoints);
}
});
}(Highcharts));
$(function () {
function setVisibility(setObject, setValue) {
setObject.element.setAttribute('visibility', setValue);
setObject.div.style.visibility = setValue;
}
function setClone(doWhat) {
chart.container.firstChild[doWhat+'Child'](cloneToolTip.element);
chart.container[doWhat+'Child'](cloneToolTip.div);
}
var cloneToolTip = null;
var clickedPoint = null;
var chart = new Highcharts.Chart({
chart: {
renderTo: 'container'
},
xAxis: {
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
},
series: [{
allowPointSelect: true,
data: [29.9, 71.5, 106.4, 129.2, 144.0, 176.0, 135.6, 148.5, 216.4, 194.1, 95.6, 54.4]
}, {
allowPointSelect: true,
data: [29.9, 71.5, 106.4, 129.2, 144.0, 176.0, 135.6, 148.5, 216.4, 194.1, 95.6, 54.4].reverse()
}, {
allowPointSelect: true,
data: [194.1, 95.6, 54.4, 29.9, 71.5, 106.4, 129.2, 144.0, 176.0, 135.6, 148.5, 216.4]
}],
plotOptions: {
series: {
cursor: 'pointer',
point: {
events: {
// this function makes the ToolTip "stick" when its point is clicked by cloning it
click: function() {
var doClone = true;
// if there is currently a cloned ToolTip, remove it from the DOM before adding a new one
if (cloneToolTip) {
setClone('remove');
// if the clicked point is the one that was already cloned, don't clone it again, only remove
if (clickedPoint == this) {
doClone = false;
}
}
// if a _different_ point from the cloned one was clicked (or there is no clone currently),
// clone the newly clicked point's ToolTip
if (doClone) {
// clone
cloneToolTip = {
element : chart.tooltip.label.element.cloneNode(true),
div : chart.tooltip.label.div.cloneNode(true)
};
// make clone visible, original might not be
setVisibility(cloneToolTip, 'visible');
// only ToolTip from a clicked point can receive events
cloneToolTip.div.style.pointerEvents = 'auto';
// append clones to the appropriate element
setClone('append');
// hide the original ToolTip
setVisibility(chart.tooltip.label, 'hidden');
// remember which point was clicked to detect closing it
clickedPoint = this;
} else {
// a clicked point was clicked again to "unstick", set clones to null
cloneToolTip = null;
// show the original ToolTip again
setVisibility(chart.tooltip.label, 'visible');
// set last clicked point to null
clickedPoint = null;
}
}
}
}
}
},
tooltip: {
useHTML: true,
events: {
// even though we hide the ToolTip in the click-event above. if the mouse moves away from the chart long
// enough to completely remove the ToolTip then it will be redrawn from scratch, nullifying the
// visibility-attributes so we have to hide it again in this custom event that fires right after
// the ToolTip has been drawn. Simply using [return false] in the formatter doesn't work because we need
// a ToolTip to clone if a different point is clicked.
show: function(points) {
if (cloneToolTip) {
setVisibility(chart.tooltip.label, 'hidden');
}
}
},
formatter: function() {
// build the ToolTip, this is made to resemble the default ToolTip with added data- attributes
// to know in the dblclick event-handler which point from which series to update
return `
<span>${this.point.category}<br>
<span style="color:${this.series.color}">●</span> ${this.series.name}:
<b data-series="${this.series.index}" data-x="${this.point.index}" data-y="${this.y}" title="Double-click to edit value">${this.y}</b>
</span>`;
}
}
});
$(window).resize(function() {
if (cloneToolTip) {
// remove cloned ToolTip after screen resize
setClone('remove');
cloneToolTip = null;
// edited point is no longer clicked
clickedPoint = null;
// show original ToolTip again
setVisibility(chart.tooltip.label, 'visible');
// go through selected points and unselect()
chart.getSelectedPoints().map(p => p.select(false));
}
});
// watch for any <b> elements (dynamically) added to #container that have a data-x attribute
// and attach dblclick event-handler
$('#container').on('dblclick', 'b[data-x]', function() {
var el = $(this);
// get information about which point from which series to update
var x = parseInt(el.attr('data-x'));
var series = parseInt(el.attr('data-series'));
// ask new value
var newVal = parseFloat(prompt('New value', el.attr('data-y')));
// return if new value wasn't given or not a valid float
if (newVal == undefined || isNaN(newVal)) return;
// Some $.ajax() call to actually persist the new value server-side
// and possibly do more validation-checks here
// update point by removing it without redraw and adding it _with_ redraw
chart.series[series].removePoint(x, false);
chart.series[series].addPoint([x, newVal], true);
// remove cloned ToolTip after point is edited
setClone('remove');
cloneToolTip = null;
// edited point is no longer clicked
clickedPoint = null;
// show original ToolTip again
setVisibility(chart.tooltip.label, 'visible');
});
});
|