1 /* Flot plugin for computing bottoms for filled line and bar charts. |
|
2 |
|
3 Copyright (c) 2007-2014 IOLA and Ole Laursen. |
|
4 Licensed under the MIT license. |
|
5 |
|
6 The case: you've got two series that you want to fill the area between. In Flot |
|
7 terms, you need to use one as the fill bottom of the other. You can specify the |
|
8 bottom of each data point as the third coordinate manually, or you can use this |
|
9 plugin to compute it for you. |
|
10 |
|
11 In order to name the other series, you need to give it an id, like this: |
|
12 |
|
13 var dataset = [ |
|
14 { data: [ ... ], id: "foo" } , // use default bottom |
|
15 { data: [ ... ], fillBetween: "foo" }, // use first dataset as bottom |
|
16 ]; |
|
17 |
|
18 $.plot($("#placeholder"), dataset, { lines: { show: true, fill: true }}); |
|
19 |
|
20 As a convenience, if the id given is a number that doesn't appear as an id in |
|
21 the series, it is interpreted as the index in the array instead (so fillBetween: |
|
22 0 can also mean the first series). |
|
23 |
|
24 Internally, the plugin modifies the datapoints in each series. For line series, |
|
25 extra data points might be inserted through interpolation. Note that at points |
|
26 where the bottom line is not defined (due to a null point or start/end of line), |
|
27 the current line will show a gap too. The algorithm comes from the |
|
28 jquery.flot.stack.js plugin, possibly some code could be shared. |
|
29 |
|
30 */ |
|
31 |
|
32 (function ($) { |
|
33 var options = { |
|
34 series: { |
|
35 fillBetween: null // or number |
|
36 } |
|
37 }; |
|
38 |
|
39 function init(plot) { |
|
40 function findBottomSeries(s, allseries) { |
|
41 var i; |
|
42 |
|
43 for (i = 0; i < allseries.length; ++i) { |
|
44 if (allseries[ i ].id === s.fillBetween) { |
|
45 return allseries[ i ]; |
|
46 } |
|
47 } |
|
48 |
|
49 if (typeof s.fillBetween === "number") { |
|
50 if (s.fillBetween < 0 || s.fillBetween >= allseries.length) { |
|
51 return null; |
|
52 } |
|
53 return allseries[ s.fillBetween ]; |
|
54 } |
|
55 |
|
56 return null; |
|
57 } |
|
58 |
|
59 function computeFormat(plot, s, data, datapoints) { |
|
60 if (s.fillBetween == null) { |
|
61 return; |
|
62 } |
|
63 |
|
64 format = datapoints.format; |
|
65 var plotHasId = function(id) { |
|
66 var plotData = plot.getData(); |
|
67 for (i = 0; i < plotData.length; i++) { |
|
68 if (plotData[i].id === id) { |
|
69 return true; |
|
70 } |
|
71 } |
|
72 |
|
73 return false; |
|
74 } |
|
75 |
|
76 if (!format) { |
|
77 format = []; |
|
78 |
|
79 format.push({ |
|
80 x: true, |
|
81 number: true, |
|
82 computeRange: s.xaxis.options.autoScale !== 'none', |
|
83 required: true |
|
84 }); |
|
85 format.push({ |
|
86 y: true, |
|
87 number: true, |
|
88 computeRange: s.yaxis.options.autoScale !== 'none', |
|
89 required: true |
|
90 }); |
|
91 |
|
92 if (s.fillBetween !== undefined && s.fillBetween !== '' && plotHasId(s.fillBetween) && s.fillBetween !== s.id) { |
|
93 format.push({ |
|
94 x: false, |
|
95 y: true, |
|
96 number: true, |
|
97 required: false, |
|
98 computeRange: s.yaxis.options.autoScale !== 'none', |
|
99 defaultValue: 0 |
|
100 }); |
|
101 } |
|
102 |
|
103 datapoints.format = format; |
|
104 } |
|
105 } |
|
106 |
|
107 function computeFillBottoms(plot, s, datapoints) { |
|
108 if (s.fillBetween == null) { |
|
109 return; |
|
110 } |
|
111 |
|
112 var other = findBottomSeries(s, plot.getData()); |
|
113 |
|
114 if (!other) { |
|
115 return; |
|
116 } |
|
117 |
|
118 var ps = datapoints.pointsize, |
|
119 points = datapoints.points, |
|
120 otherps = other.datapoints.pointsize, |
|
121 otherpoints = other.datapoints.points, |
|
122 newpoints = [], |
|
123 px, py, intery, qx, qy, bottom, |
|
124 withlines = s.lines.show, |
|
125 withbottom = ps > 2 && datapoints.format[2].y, |
|
126 withsteps = withlines && s.lines.steps, |
|
127 fromgap = true, |
|
128 i = 0, |
|
129 j = 0, |
|
130 l, m; |
|
131 |
|
132 while (true) { |
|
133 if (i >= points.length) { |
|
134 break; |
|
135 } |
|
136 |
|
137 l = newpoints.length; |
|
138 |
|
139 if (points[ i ] == null) { |
|
140 // copy gaps |
|
141 for (m = 0; m < ps; ++m) { |
|
142 newpoints.push(points[ i + m ]); |
|
143 } |
|
144 |
|
145 i += ps; |
|
146 } else if (j >= otherpoints.length) { |
|
147 // for lines, we can't use the rest of the points |
|
148 if (!withlines) { |
|
149 for (m = 0; m < ps; ++m) { |
|
150 newpoints.push(points[ i + m ]); |
|
151 } |
|
152 } |
|
153 |
|
154 i += ps; |
|
155 } else if (otherpoints[ j ] == null) { |
|
156 // oops, got a gap |
|
157 for (m = 0; m < ps; ++m) { |
|
158 newpoints.push(null); |
|
159 } |
|
160 |
|
161 fromgap = true; |
|
162 j += otherps; |
|
163 } else { |
|
164 // cases where we actually got two points |
|
165 px = points[ i ]; |
|
166 py = points[ i + 1 ]; |
|
167 qx = otherpoints[ j ]; |
|
168 qy = otherpoints[ j + 1 ]; |
|
169 bottom = 0; |
|
170 |
|
171 if (px === qx) { |
|
172 for (m = 0; m < ps; ++m) { |
|
173 newpoints.push(points[ i + m ]); |
|
174 } |
|
175 |
|
176 //newpoints[ l + 1 ] += qy; |
|
177 bottom = qy; |
|
178 |
|
179 i += ps; |
|
180 j += otherps; |
|
181 } else if (px > qx) { |
|
182 // we got past point below, might need to |
|
183 // insert interpolated extra point |
|
184 |
|
185 if (withlines && i > 0 && points[ i - ps ] != null) { |
|
186 intery = py + (points[ i - ps + 1 ] - py) * (qx - px) / (points[ i - ps ] - px); |
|
187 newpoints.push(qx); |
|
188 newpoints.push(intery); |
|
189 for (m = 2; m < ps; ++m) { |
|
190 newpoints.push(points[ i + m ]); |
|
191 } |
|
192 bottom = qy; |
|
193 } |
|
194 |
|
195 j += otherps; |
|
196 } else { |
|
197 // px < qx |
|
198 // if we come from a gap, we just skip this point |
|
199 |
|
200 if (fromgap && withlines) { |
|
201 i += ps; |
|
202 continue; |
|
203 } |
|
204 |
|
205 for (m = 0; m < ps; ++m) { |
|
206 newpoints.push(points[ i + m ]); |
|
207 } |
|
208 |
|
209 // we might be able to interpolate a point below, |
|
210 // this can give us a better y |
|
211 |
|
212 if (withlines && j > 0 && otherpoints[ j - otherps ] != null) { |
|
213 bottom = qy + (otherpoints[ j - otherps + 1 ] - qy) * (px - qx) / (otherpoints[ j - otherps ] - qx); |
|
214 } |
|
215 |
|
216 //newpoints[l + 1] += bottom; |
|
217 |
|
218 i += ps; |
|
219 } |
|
220 |
|
221 fromgap = false; |
|
222 |
|
223 if (l !== newpoints.length && withbottom) { |
|
224 newpoints[ l + 2 ] = bottom; |
|
225 } |
|
226 } |
|
227 |
|
228 // maintain the line steps invariant |
|
229 |
|
230 if (withsteps && l !== newpoints.length && l > 0 && |
|
231 newpoints[ l ] !== null && |
|
232 newpoints[ l ] !== newpoints[ l - ps ] && |
|
233 newpoints[ l + 1 ] !== newpoints[ l - ps + 1 ]) { |
|
234 for (m = 0; m < ps; ++m) { |
|
235 newpoints[ l + ps + m ] = newpoints[ l + m ]; |
|
236 } |
|
237 newpoints[ l + 1 ] = newpoints[ l - ps + 1 ]; |
|
238 } |
|
239 } |
|
240 |
|
241 datapoints.points = newpoints; |
|
242 } |
|
243 |
|
244 plot.hooks.processRawData.push(computeFormat); |
|
245 plot.hooks.processDatapoints.push(computeFillBottoms); |
|
246 } |
|
247 |
|
248 $.plot.plugins.push({ |
|
249 init: init, |
|
250 options: options, |
|
251 name: "fillbetween", |
|
252 version: "1.0" |
|
253 }); |
|
254 })(jQuery); |
|