1 /* Flot plugin for stacking data sets rather than overlaying them. |
|
2 |
|
3 Copyright (c) 2007-2014 IOLA and Ole Laursen. |
|
4 Licensed under the MIT license. |
|
5 |
|
6 The plugin assumes the data is sorted on x (or y if stacking horizontally). |
|
7 For line charts, it is assumed that if a line has an undefined gap (from a |
|
8 null point), then the line above it should have the same gap - insert zeros |
|
9 instead of "null" if you want another behaviour. This also holds for the start |
|
10 and end of the chart. Note that stacking a mix of positive and negative values |
|
11 in most instances doesn't make sense (so it looks weird). |
|
12 |
|
13 Two or more series are stacked when their "stack" attribute is set to the same |
|
14 key (which can be any number or string or just "true"). To specify the default |
|
15 stack, you can set the stack option like this: |
|
16 |
|
17 series: { |
|
18 stack: null/false, true, or a key (number/string) |
|
19 } |
|
20 |
|
21 You can also specify it for a single series, like this: |
|
22 |
|
23 $.plot( $("#placeholder"), [{ |
|
24 data: [ ... ], |
|
25 stack: true |
|
26 }]) |
|
27 |
|
28 The stacking order is determined by the order of the data series in the array |
|
29 (later series end up on top of the previous). |
|
30 |
|
31 Internally, the plugin modifies the datapoints in each series, adding an |
|
32 offset to the y value. For line series, extra data points are inserted through |
|
33 interpolation. If there's a second y value, it's also adjusted (e.g for bar |
|
34 charts or filled areas). |
|
35 |
|
36 */ |
|
37 |
|
38 (function ($) { |
|
39 var options = { |
|
40 series: { stack: null } // or number/string |
|
41 }; |
|
42 |
|
43 function init(plot) { |
|
44 function findMatchingSeries(s, allseries) { |
|
45 var res = null; |
|
46 for (var i = 0; i < allseries.length; ++i) { |
|
47 if (s === allseries[i]) break; |
|
48 |
|
49 if (allseries[i].stack === s.stack) { |
|
50 res = allseries[i]; |
|
51 } |
|
52 } |
|
53 |
|
54 return res; |
|
55 } |
|
56 |
|
57 function addBottomPoints (s, datapoints) { |
|
58 var formattedPoints = []; |
|
59 for (var i = 0; i < datapoints.points.length; i += 2) { |
|
60 formattedPoints.push(datapoints.points[i]); |
|
61 formattedPoints.push(datapoints.points[i + 1]); |
|
62 formattedPoints.push(0); |
|
63 } |
|
64 |
|
65 datapoints.format.push({ |
|
66 x: false, |
|
67 y: true, |
|
68 number: true, |
|
69 required: false, |
|
70 computeRange: s.yaxis.options.autoScale !== 'none', |
|
71 defaultValue: 0 |
|
72 }); |
|
73 datapoints.points = formattedPoints; |
|
74 datapoints.pointsize = 3; |
|
75 } |
|
76 |
|
77 function stackData(plot, s, datapoints) { |
|
78 if (s.stack == null || s.stack === false) return; |
|
79 |
|
80 var needsBottom = s.bars.show || (s.lines.show && s.lines.fill); |
|
81 var hasBottom = datapoints.pointsize > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y); |
|
82 // Series data is missing bottom points - need to format |
|
83 if (needsBottom && !hasBottom) { |
|
84 addBottomPoints(s, datapoints); |
|
85 } |
|
86 |
|
87 var other = findMatchingSeries(s, plot.getData()); |
|
88 if (!other) return; |
|
89 |
|
90 var ps = datapoints.pointsize, |
|
91 points = datapoints.points, |
|
92 otherps = other.datapoints.pointsize, |
|
93 otherpoints = other.datapoints.points, |
|
94 newpoints = [], |
|
95 px, py, intery, qx, qy, bottom, |
|
96 withlines = s.lines.show, |
|
97 horizontal = s.bars.horizontal, |
|
98 withsteps = withlines && s.lines.steps, |
|
99 fromgap = true, |
|
100 keyOffset = horizontal ? 1 : 0, |
|
101 accumulateOffset = horizontal ? 0 : 1, |
|
102 i = 0, j = 0, l, m; |
|
103 |
|
104 while (true) { |
|
105 if (i >= points.length) break; |
|
106 |
|
107 l = newpoints.length; |
|
108 |
|
109 if (points[i] == null) { |
|
110 // copy gaps |
|
111 for (m = 0; m < ps; ++m) { |
|
112 newpoints.push(points[i + m]); |
|
113 } |
|
114 |
|
115 i += ps; |
|
116 } else if (j >= otherpoints.length) { |
|
117 // for lines, we can't use the rest of the points |
|
118 if (!withlines) { |
|
119 for (m = 0; m < ps; ++m) { |
|
120 newpoints.push(points[i + m]); |
|
121 } |
|
122 } |
|
123 |
|
124 i += ps; |
|
125 } else if (otherpoints[j] == null) { |
|
126 // oops, got a gap |
|
127 for (m = 0; m < ps; ++m) { |
|
128 newpoints.push(null); |
|
129 } |
|
130 |
|
131 fromgap = true; |
|
132 j += otherps; |
|
133 } else { |
|
134 // cases where we actually got two points |
|
135 px = points[i + keyOffset]; |
|
136 py = points[i + accumulateOffset]; |
|
137 qx = otherpoints[j + keyOffset]; |
|
138 qy = otherpoints[j + accumulateOffset]; |
|
139 bottom = 0; |
|
140 |
|
141 if (px === qx) { |
|
142 for (m = 0; m < ps; ++m) { |
|
143 newpoints.push(points[i + m]); |
|
144 } |
|
145 |
|
146 newpoints[l + accumulateOffset] += qy; |
|
147 bottom = qy; |
|
148 |
|
149 i += ps; |
|
150 j += otherps; |
|
151 } else if (px > qx) { |
|
152 // we got past point below, might need to |
|
153 // insert interpolated extra point |
|
154 if (withlines && i > 0 && points[i - ps] != null) { |
|
155 intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px); |
|
156 newpoints.push(qx); |
|
157 newpoints.push(intery + qy); |
|
158 for (m = 2; m < ps; ++m) { |
|
159 newpoints.push(points[i + m]); |
|
160 } |
|
161 |
|
162 bottom = qy; |
|
163 } |
|
164 |
|
165 j += otherps; |
|
166 } else { // px < qx |
|
167 if (fromgap && withlines) { |
|
168 // if we come from a gap, we just skip this point |
|
169 i += ps; |
|
170 continue; |
|
171 } |
|
172 |
|
173 for (m = 0; m < ps; ++m) { |
|
174 newpoints.push(points[i + m]); |
|
175 } |
|
176 |
|
177 // we might be able to interpolate a point below, |
|
178 // this can give us a better y |
|
179 if (withlines && j > 0 && otherpoints[j - otherps] != null) { |
|
180 bottom = qy + (otherpoints[j - otherps + accumulateOffset] - qy) * (px - qx) / (otherpoints[j - otherps + keyOffset] - qx); |
|
181 } |
|
182 |
|
183 newpoints[l + accumulateOffset] += bottom; |
|
184 |
|
185 i += ps; |
|
186 } |
|
187 |
|
188 fromgap = false; |
|
189 |
|
190 if (l !== newpoints.length && needsBottom) { |
|
191 newpoints[l + 2] += bottom; |
|
192 } |
|
193 } |
|
194 |
|
195 // maintain the line steps invariant |
|
196 if (withsteps && l !== newpoints.length && l > 0 && |
|
197 newpoints[l] !== null && |
|
198 newpoints[l] !== newpoints[l - ps] && |
|
199 newpoints[l + 1] !== newpoints[l - ps + 1]) { |
|
200 for (m = 0; m < ps; ++m) { |
|
201 newpoints[l + ps + m] = newpoints[l + m]; |
|
202 } |
|
203 |
|
204 newpoints[l + 1] = newpoints[l - ps + 1]; |
|
205 } |
|
206 } |
|
207 |
|
208 datapoints.points = newpoints; |
|
209 } |
|
210 |
|
211 plot.hooks.processDatapoints.push(stackData); |
|
212 } |
|
213 |
|
214 $.plot.plugins.push({ |
|
215 init: init, |
|
216 options: options, |
|
217 name: 'stack', |
|
218 version: '1.2' |
|
219 }); |
|
220 })(jQuery); |
|