-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjquery.flot.fillbetween.js
383 lines (327 loc) · 12.2 KB
/
jquery.flot.fillbetween.js
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
/*
Flot plugin for computing bottoms for filled line and bar charts.
The case: you've got two series that you want to fill the area
between. In Flot terms, you need to use one as the fill bottom of the
other. You can specify the bottom of each data point as the third
coordinate manually, or you can use this plugin to compute it for you.
In order to name the other series, you need to give it an id, like this
var dataset = [
{ data: [ ... ], id: "foo" } , // use default bottom
{ data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom
];
$.plot($("#placeholder"), dataset, { lines: { show: true, fill: true }});
As a convenience, if the id given is a number that doesn't appear as
an id in the series, it is interpreted as the index in the array
instead (so fillBetween: 0 can also mean the first series).
Internally, the plugin modifies the datapoints in each series. For
line series, extra data points might be inserted through
interpolation. Note that at points where the bottom line is not
defined (due to a null point or start/end of line), the current line
will show a gap too. The algorithm comes from the jquery.flot.stack.js
plugin, possibly some code could be shared.
*/
(function ($) {
var options = {
series: { fillBetween: null } // or number
};
function init(plot) {
/**
* Creates a read-only point object with only getter methods.
*/
var immutablePoint = function( x, y )
{
var point = {
x: x,
y: y
};
return {
getX: function() { return point.x; },
getY: function() { return point.y; }
};
};
function createAreaPolygonFromDatapoints( datapoints, s1, s2 )
{
/**
* Creates a read-only polygon object with only getter methods.
*/
var immutableAreaPolygon = function( outlinePoints, series )
{
var areaPolygon = {
outline: outlinePoints,
series: series
};
return {
getOutline: function() {
return areaPolygon.outline;
},
getSeries: function() {
return areaPolygon.series;
}
};
};
var outline = []; // Top data points in normal order
var bottomOutline = []; // Gather bottom data points for later reversing
var prevNull = false; // Previous datapoint was null?
for ( var i = 0; i < datapoints.points.length; i += datapoints.pointsize )
{
var x = datapoints.points[ i ];
var topy = datapoints.points[ i + 1 ];
var bottomy = datapoints.points[ i + datapoints.pointsize - 1 ];
if ( x === null )
{
outline.push( immutablePoint(
bottomOutline[ bottomOutline.length - 1 ].getX(),
bottomOutline[ bottomOutline.length - 1 ].getY() ) );
prevNull = true;
}
else
{
if ( prevNull )
{
// Previous datapoint was null. Produce an additional
// segment for the top outline in order to have the polygon
// run at zero width through the gap area.
outline.push( immutablePoint( x, bottomy ) );
}
outline.push( immutablePoint( x, topy ) );
bottomOutline.push( immutablePoint( x, bottomy ) );
prevNull = false;
}
}
bottomOutline.reverse();
return immutableAreaPolygon( outline.concat( bottomOutline ), [ s1, s2 ] );
}
// All the areas represented as polygons
var areaPolygons = [];
function findBottomSeries(s, allseries) {
var i;
for (i = 0; i < allseries.length; ++i) {
if (allseries[i].id == s.fillBetween)
return allseries[i];
}
if (typeof s.fillBetween == "number") {
i = s.fillBetween;
if (i < 0 || i >= allseries.length)
return null;
return allseries[i];
}
return null;
}
function computeFillBottoms(plot, s, datapoints) {
if (s.fillBetween == null)
return;
var other = findBottomSeries(s, plot.getData());
if (!other)
return;
var ps = datapoints.pointsize,
points = datapoints.points,
otherps = other.datapoints.pointsize,
otherpoints = other.datapoints.points,
newpoints = [],
px, py, intery, qx, qy, bottom,
withlines = s.lines.show,
withbottom = ps > 2 && datapoints.format[2].y,
withsteps = withlines && s.lines.steps,
fromgap = true,
i = 0, j = 0, l;
while (true) {
if (i >= points.length)
break;
l = newpoints.length;
if (points[i] == null) {
// copy gaps
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
i += ps;
}
else if (j >= otherpoints.length) {
// for lines, we can't use the rest of the points
if (!withlines) {
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
}
i += ps;
}
else if (otherpoints[j] == null) {
// oops, got a gap
for (m = 0; m < ps; ++m)
newpoints.push(null);
fromgap = true;
j += otherps;
}
else {
// cases where we actually got two points
px = points[i];
py = points[i + 1];
qx = otherpoints[j];
qy = otherpoints[j + 1];
bottom = 0;
if (px == qx) {
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
//newpoints[l + 1] += qy;
bottom = qy;
i += ps;
j += otherps;
}
else if (px > qx) {
// we got past point below, might need to
// insert interpolated extra point
if (withlines && i > 0 && points[i - ps] != null) {
intery = py + (points[i - ps + 1] - py) * (qx - px) / (points[i - ps] - px);
newpoints.push(qx);
newpoints.push(intery)
for (m = 2; m < ps; ++m)
newpoints.push(points[i + m]);
bottom = qy;
}
j += otherps;
}
else { // px < qx
if (fromgap && withlines) {
// if we come from a gap, we just skip this point
i += ps;
continue;
}
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
// we might be able to interpolate a point below,
// this can give us a better y
if (withlines && j > 0 && otherpoints[j - otherps] != null)
bottom = qy + (otherpoints[j - otherps + 1] - qy) * (px - qx) / (otherpoints[j - otherps] - qx);
//newpoints[l + 1] += bottom;
i += ps;
}
fromgap = false;
if (l != newpoints.length && withbottom)
newpoints[l + 2] = bottom;
}
// maintain the line steps invariant
if (withsteps && l != newpoints.length && l > 0
&& newpoints[l] != null
&& newpoints[l] != newpoints[l - ps]
&& newpoints[l + 1] != newpoints[l - ps + 1]) {
for (m = 0; m < ps; ++m)
newpoints[l + ps + m] = newpoints[l + m];
newpoints[l + 1] = newpoints[l - ps + 1];
}
}
datapoints.points = newpoints;
// Create area polygons here. Depend on hoverable/clickable option.
if ( plot.getOptions().grid.hoverableFill || plot.getOptions().grid.clickableFill )
{
areaPolygons.push( createAreaPolygonFromDatapoints( datapoints, s, other ) );
}
}
/**
* Returns the filled areas over which the mouse is currently
* hovering. Empty array will be returned if the mouse isn't hovering
* over any filled area.
* Parameters:
* plot - the plot object
* mouseX - mouse location on X-axis (plot coordinates)
* mouseY - mouse location on Y-axis (plot coordinates)
* Return format:
* [
* [ seriesA, seriesB ],
* [ ..., ... ],
* ...
* ]
*/
function isHoveringOverFilledArea( mouseX, mouseY )
{
var immutableSegment = function( pointA, pointB )
{
var segment = {
a: pointA,
b: pointB
};
return {
getA: function() { return segment.a; },
getB: function() { return segment.b; }
};
};
// Check whether two segments intersect
var intersect = function( segmentA, segmentB )
{
var ccw = function( pointA, pointB, pointC )
{
return ( (pointB.getX() - pointA.getX())*(pointC.getY() - pointA.getY()) -
(pointB.getY() - pointA.getY())*(pointC.getX() - pointA.getX())) > 0;
}
var pointAA = segmentA.getA();
var pointAB = segmentA.getB();
var pointBA = segmentB.getA();
var pointBB = segmentB.getB();
return (ccw( pointAA, pointBA, pointBB ) != ccw( pointAB, pointBA, pointBB )) &&
(ccw( pointAA, pointAB, pointBA ) != ccw( pointAA, pointAB, pointBB ));
}
var hoverPoint = immutablePoint( mouseX, mouseY );
var hoverAreas = [];
for ( var i in areaPolygons )
{
var polygon = areaPolygons[ i ];
var points = polygon.getOutline();
var series = polygon.getSeries();
var farEastEdge = series[ 0 ].xaxis.datamax > series[ 1 ].xaxis.datamax ?
series[ 0 ].xaxis.datamax + 1 : series[ 1 ].xaxis.datamax + 1;
var rayCastSegment = immutableSegment( hoverPoint, immutablePoint( farEastEdge, mouseY ) );
var intersections = 0;
for ( var j = 0; j < points.length; ++j )
{
var firstPoint = points[ j ];
var secondPoint = j < (points.length - 1) ? secondPoint = points[ j + 1 ] : secondPoint = points[ 0 ];
if ( intersect( rayCastSegment, immutableSegment( firstPoint, secondPoint ) ) ) intersections++;
}
if ( intersections % 2 == 1 )
{
hoverAreas.push( series );
}
}
return hoverAreas;
}
/**
* Set up area hover and area click event triggering.
*/
var checkForFillAreaHover = function( plot, eventHolder )
{
// Require the 'hoverableFill' option
if ( plot.getOptions().grid.hoverableFill )
{
// Bind to the existing plothover event. This way the 'hoverable'
// attribute is taken into account (and we get pre-calculated
// mouse cursor positions easily).
$( plot.getPlaceholder() ).bind( 'plothover', function( event, pos, item )
{
var areas = isHoveringOverFilledArea( pos.x, pos.y );
if ( areas.length > 0 )
{
$( plot.getPlaceholder() ).trigger( 'fillareahover', [ pos, areas ] );
}
} );
}
// Require the 'clickableFill' option
if ( plot.getOptions().grid.clickableFill )
{
// Bind to the plotclick event. This way the 'clickable' attribute
// is taken into account.
$( plot.getPlaceholder() ).bind( 'plotclick', function( event, pos, item )
{
var areas = isHoveringOverFilledArea( pos.x, pos.y );
if ( areas.length > 0 )
{
$( plot.getPlaceholder() ).trigger( 'fillareaclick', [ pos, areas ] );
}
} );
}
}
plot.hooks.processDatapoints.push(computeFillBottoms);
plot.hooks.bindEvents.push( checkForFillAreaHover );
}
$.plot.plugins.push({
init: init,
options: options,
name: 'fillbetween',
version: '1.0'
});
})(jQuery);