[76] | 1 | (function(){d3.chart = {}; |
---|
| 2 | // Inspired by http://informationandvisualization.de/blog/box-plot |
---|
| 3 | d3.chart.box = function() { |
---|
| 4 | var width = 1, |
---|
| 5 | height = 1, |
---|
| 6 | duration = 0, |
---|
| 7 | domain = null, |
---|
| 8 | value = Number, |
---|
| 9 | whiskers = d3_chart_boxWhiskers, |
---|
| 10 | quartiles = d3_chart_boxQuartiles, |
---|
| 11 | tickFormat = null; |
---|
| 12 | |
---|
| 13 | // For each small multiple⊠|
---|
| 14 | function box(g) { |
---|
| 15 | g.each(function(d, i) { |
---|
| 16 | d = d.map(value).sort(d3.ascending); |
---|
| 17 | var g = d3.select(this), |
---|
| 18 | n = d.length, |
---|
| 19 | min = d[0], |
---|
| 20 | max = d[n - 1]; |
---|
| 21 | |
---|
| 22 | // Compute quartiles. Must return exactly 3 elements. |
---|
| 23 | var quartileData = d.quartiles = quartiles(d); |
---|
| 24 | |
---|
| 25 | // Compute whiskers. Must return exactly 2 elements, or null. |
---|
| 26 | var whiskerIndices = whiskers && whiskers.call(this, d, i), |
---|
| 27 | whiskerData = whiskerIndices && whiskerIndices.map(function(i) { return d[i]; }); |
---|
| 28 | |
---|
| 29 | // Compute outliers. If no whiskers are specified, all data are "outliers". |
---|
| 30 | // We compute the outliers as indices, so that we can join across transitions! |
---|
| 31 | var outlierIndices = whiskerIndices |
---|
| 32 | ? d3.range(0, whiskerIndices[0]).concat(d3.range(whiskerIndices[1] + 1, n)) |
---|
| 33 | : d3.range(n); |
---|
| 34 | |
---|
| 35 | // Compute the new x-scale. |
---|
| 36 | var x1 = d3.scale.linear() |
---|
| 37 | .domain(domain && domain.call(this, d, i) || [min, max]) |
---|
| 38 | .range([height, 0]); |
---|
| 39 | |
---|
| 40 | // Retrieve the old x-scale, if this is an update. |
---|
| 41 | var x0 = this.__chart__ || d3.scale.linear() |
---|
| 42 | .domain([0, Infinity]) |
---|
| 43 | .range(x1.range()); |
---|
| 44 | |
---|
| 45 | // Stash the new scale. |
---|
| 46 | this.__chart__ = x1; |
---|
| 47 | |
---|
| 48 | // Note: the box, median, and box tick elements are fixed in number, |
---|
| 49 | // so we only have to handle enter and update. In contrast, the outliers |
---|
| 50 | // and other elements are variable, so we need to exit them! Variable |
---|
| 51 | // elements also fade in and out. |
---|
| 52 | |
---|
| 53 | // Update center line: the vertical line spanning the whiskers. |
---|
| 54 | var center = g.selectAll("line.center") |
---|
| 55 | .data(whiskerData ? [whiskerData] : []); |
---|
| 56 | |
---|
| 57 | center.enter().insert("svg:line", "rect") |
---|
| 58 | .attr("class", "center") |
---|
| 59 | .attr("x1", width / 2) |
---|
| 60 | .attr("y1", function(d) { return x0(d[0]); }) |
---|
| 61 | .attr("x2", width / 2) |
---|
| 62 | .attr("y2", function(d) { return x0(d[1]); }) |
---|
| 63 | .style("opacity", 1e-6) |
---|
| 64 | .transition() |
---|
| 65 | .duration(duration) |
---|
| 66 | .style("opacity", 1) |
---|
| 67 | .attr("y1", function(d) { return x1(d[0]); }) |
---|
| 68 | .attr("y2", function(d) { return x1(d[1]); }); |
---|
| 69 | |
---|
| 70 | center.transition() |
---|
| 71 | .duration(duration) |
---|
| 72 | .style("opacity", 1) |
---|
| 73 | .attr("y1", function(d) { return x1(d[0]); }) |
---|
| 74 | .attr("y2", function(d) { return x1(d[1]); }); |
---|
| 75 | |
---|
| 76 | center.exit().transition() |
---|
| 77 | .duration(duration) |
---|
| 78 | .style("opacity", 1e-6) |
---|
| 79 | .attr("y1", function(d) { return x1(d[0]); }) |
---|
| 80 | .attr("y2", function(d) { return x1(d[1]); }) |
---|
| 81 | .remove(); |
---|
| 82 | |
---|
| 83 | // Update innerquartile box. |
---|
| 84 | var box = g.selectAll("rect.box") |
---|
| 85 | .data([quartileData]); |
---|
| 86 | |
---|
| 87 | box.enter().append("svg:rect") |
---|
| 88 | .attr("class", "box") |
---|
| 89 | .attr("x", 0) |
---|
| 90 | .attr("y", function(d) { return x0(d[2]); }) |
---|
| 91 | .attr("width", width) |
---|
| 92 | .attr("height", function(d) { return x0(d[0]) - x0(d[2]); }) |
---|
| 93 | .transition() |
---|
| 94 | .duration(duration) |
---|
| 95 | .attr("y", function(d) { return x1(d[2]); }) |
---|
| 96 | .attr("height", function(d) { return x1(d[0]) - x1(d[2]); }); |
---|
| 97 | |
---|
| 98 | box.transition() |
---|
| 99 | .duration(duration) |
---|
| 100 | .attr("y", function(d) { return x1(d[2]); }) |
---|
| 101 | .attr("height", function(d) { return x1(d[0]) - x1(d[2]); }); |
---|
| 102 | |
---|
| 103 | // Update median line. |
---|
| 104 | var medianLine = g.selectAll("line.median") |
---|
| 105 | .data([quartileData[1]]); |
---|
| 106 | |
---|
| 107 | medianLine.enter().append("svg:line") |
---|
| 108 | .attr("class", "median") |
---|
| 109 | .attr("x1", 0) |
---|
| 110 | .attr("y1", x0) |
---|
| 111 | .attr("x2", width) |
---|
| 112 | .attr("y2", x0) |
---|
| 113 | .transition() |
---|
| 114 | .duration(duration) |
---|
| 115 | .attr("y1", x1) |
---|
| 116 | .attr("y2", x1); |
---|
| 117 | |
---|
| 118 | medianLine.transition() |
---|
| 119 | .duration(duration) |
---|
| 120 | .attr("y1", x1) |
---|
| 121 | .attr("y2", x1); |
---|
| 122 | |
---|
| 123 | // Update whiskers. |
---|
| 124 | var whisker = g.selectAll("line.whisker") |
---|
| 125 | .data(whiskerData || []); |
---|
| 126 | |
---|
| 127 | whisker.enter().insert("svg:line", "circle, text") |
---|
| 128 | .attr("class", "whisker") |
---|
| 129 | .attr("x1", 0) |
---|
| 130 | .attr("y1", x0) |
---|
| 131 | .attr("x2", width) |
---|
| 132 | .attr("y2", x0) |
---|
| 133 | .style("opacity", 1e-6) |
---|
| 134 | .transition() |
---|
| 135 | .duration(duration) |
---|
| 136 | .attr("y1", x1) |
---|
| 137 | .attr("y2", x1) |
---|
| 138 | .style("opacity", 1); |
---|
| 139 | |
---|
| 140 | whisker.transition() |
---|
| 141 | .duration(duration) |
---|
| 142 | .attr("y1", x1) |
---|
| 143 | .attr("y2", x1) |
---|
| 144 | .style("opacity", 1); |
---|
| 145 | |
---|
| 146 | whisker.exit().transition() |
---|
| 147 | .duration(duration) |
---|
| 148 | .attr("y1", x1) |
---|
| 149 | .attr("y2", x1) |
---|
| 150 | .style("opacity", 1e-6) |
---|
| 151 | .remove(); |
---|
| 152 | |
---|
| 153 | // Update outliers. |
---|
| 154 | var outlier = g.selectAll("circle.outlier") |
---|
| 155 | .data(outlierIndices, Number); |
---|
| 156 | |
---|
| 157 | outlier.enter().insert("svg:circle", "text") |
---|
| 158 | .attr("class", "outlier") |
---|
| 159 | .attr("r", 5) |
---|
| 160 | .attr("cx", width / 2) |
---|
| 161 | .attr("cy", function(i) { return x0(d[i]); }) |
---|
| 162 | .style("opacity", 1e-6) |
---|
| 163 | .transition() |
---|
| 164 | .duration(duration) |
---|
| 165 | .attr("cy", function(i) { return x1(d[i]); }) |
---|
| 166 | .style("opacity", 1); |
---|
| 167 | |
---|
| 168 | outlier.transition() |
---|
| 169 | .duration(duration) |
---|
| 170 | .attr("cy", function(i) { return x1(d[i]); }) |
---|
| 171 | .style("opacity", 1); |
---|
| 172 | |
---|
| 173 | outlier.exit().transition() |
---|
| 174 | .duration(duration) |
---|
| 175 | .attr("cy", function(i) { return x1(d[i]); }) |
---|
| 176 | .style("opacity", 1e-6) |
---|
| 177 | .remove(); |
---|
| 178 | |
---|
| 179 | // Compute the tick format. |
---|
| 180 | var format = tickFormat || x1.tickFormat(8); |
---|
| 181 | |
---|
| 182 | // Update box ticks. |
---|
| 183 | var boxTick = g.selectAll("text.box") |
---|
| 184 | .data(quartileData); |
---|
| 185 | |
---|
| 186 | boxTick.enter().append("svg:text") |
---|
| 187 | .attr("class", "box") |
---|
| 188 | .attr("dy", ".3em") |
---|
| 189 | .attr("dx", function(d, i) { return i & 1 ? 6 : -6 }) |
---|
| 190 | .attr("x", function(d, i) { return i & 1 ? width : 0 }) |
---|
| 191 | .attr("y", x0) |
---|
| 192 | .attr("text-anchor", function(d, i) { return i & 1 ? "start" : "end"; }) |
---|
| 193 | .text(format) |
---|
| 194 | .transition() |
---|
| 195 | .duration(duration) |
---|
| 196 | .attr("y", x1); |
---|
| 197 | |
---|
| 198 | boxTick.transition() |
---|
| 199 | .duration(duration) |
---|
| 200 | .text(format) |
---|
| 201 | .attr("y", x1); |
---|
| 202 | |
---|
| 203 | // Update whisker ticks. These are handled separately from the box |
---|
| 204 | // ticks because they may or may not exist, and we want don't want |
---|
| 205 | // to join box ticks pre-transition with whisker ticks post-. |
---|
| 206 | var whiskerTick = g.selectAll("text.whisker") |
---|
| 207 | .data(whiskerData || []); |
---|
| 208 | |
---|
| 209 | whiskerTick.enter().append("svg:text") |
---|
| 210 | .attr("class", "whisker") |
---|
| 211 | .attr("dy", ".3em") |
---|
| 212 | .attr("dx", 6) |
---|
| 213 | .attr("x", width) |
---|
| 214 | .attr("y", x0) |
---|
| 215 | .text(format) |
---|
| 216 | .style("opacity", 1e-6) |
---|
| 217 | .transition() |
---|
| 218 | .duration(duration) |
---|
| 219 | .attr("y", x1) |
---|
| 220 | .style("opacity", 1); |
---|
| 221 | |
---|
| 222 | whiskerTick.transition() |
---|
| 223 | .duration(duration) |
---|
| 224 | .text(format) |
---|
| 225 | .attr("y", x1) |
---|
| 226 | .style("opacity", 1); |
---|
| 227 | |
---|
| 228 | whiskerTick.exit().transition() |
---|
| 229 | .duration(duration) |
---|
| 230 | .attr("y", x1) |
---|
| 231 | .style("opacity", 1e-6) |
---|
| 232 | .remove(); |
---|
| 233 | }); |
---|
| 234 | d3.timer.flush(); |
---|
| 235 | } |
---|
| 236 | |
---|
| 237 | box.width = function(x) { |
---|
| 238 | if (!arguments.length) return width; |
---|
| 239 | width = x; |
---|
| 240 | return box; |
---|
| 241 | }; |
---|
| 242 | |
---|
| 243 | box.height = function(x) { |
---|
| 244 | if (!arguments.length) return height; |
---|
| 245 | height = x; |
---|
| 246 | return box; |
---|
| 247 | }; |
---|
| 248 | |
---|
| 249 | box.tickFormat = function(x) { |
---|
| 250 | if (!arguments.length) return tickFormat; |
---|
| 251 | tickFormat = x; |
---|
| 252 | return box; |
---|
| 253 | }; |
---|
| 254 | |
---|
| 255 | box.duration = function(x) { |
---|
| 256 | if (!arguments.length) return duration; |
---|
| 257 | duration = x; |
---|
| 258 | return box; |
---|
| 259 | }; |
---|
| 260 | |
---|
| 261 | box.domain = function(x) { |
---|
| 262 | if (!arguments.length) return domain; |
---|
| 263 | domain = x == null ? x : d3.functor(x); |
---|
| 264 | return box; |
---|
| 265 | }; |
---|
| 266 | |
---|
| 267 | box.value = function(x) { |
---|
| 268 | if (!arguments.length) return value; |
---|
| 269 | value = x; |
---|
| 270 | return box; |
---|
| 271 | }; |
---|
| 272 | |
---|
| 273 | box.whiskers = function(x) { |
---|
| 274 | if (!arguments.length) return whiskers; |
---|
| 275 | whiskers = x; |
---|
| 276 | return box; |
---|
| 277 | }; |
---|
| 278 | |
---|
| 279 | box.quartiles = function(x) { |
---|
| 280 | if (!arguments.length) return quartiles; |
---|
| 281 | quartiles = x; |
---|
| 282 | return box; |
---|
| 283 | }; |
---|
| 284 | |
---|
| 285 | return box; |
---|
| 286 | }; |
---|
| 287 | |
---|
| 288 | function d3_chart_boxWhiskers(d) { |
---|
| 289 | return [0, d.length - 1]; |
---|
| 290 | } |
---|
| 291 | |
---|
| 292 | function d3_chart_boxQuartiles(d) { |
---|
| 293 | return [ |
---|
| 294 | d3.quantile(d, .25), |
---|
| 295 | d3.quantile(d, .5), |
---|
| 296 | d3.quantile(d, .75) |
---|
| 297 | ]; |
---|
| 298 | } |
---|
| 299 | // Chart design based on the recommendations of Stephen Few. Implementation |
---|
| 300 | // based on the work of Clint Ivy, Jamie Love, and Jason Davies. |
---|
| 301 | // http://projects.instantcognition.com/protovis/bulletchart/ |
---|
| 302 | d3.chart.bullet = function() { |
---|
| 303 | var orient = "left", // TODO top & bottom |
---|
| 304 | reverse = false, |
---|
| 305 | duration = 0, |
---|
| 306 | ranges = d3_chart_bulletRanges, |
---|
| 307 | markers = d3_chart_bulletMarkers, |
---|
| 308 | measures = d3_chart_bulletMeasures, |
---|
| 309 | width = 380, |
---|
| 310 | height = 30, |
---|
| 311 | tickFormat = null; |
---|
| 312 | |
---|
| 313 | // For each small multiple⊠|
---|
| 314 | function bullet(g) { |
---|
| 315 | g.each(function(d, i) { |
---|
| 316 | var rangez = ranges.call(this, d, i).slice().sort(d3.descending), |
---|
| 317 | markerz = markers.call(this, d, i).slice().sort(d3.descending), |
---|
| 318 | measurez = measures.call(this, d, i).slice().sort(d3.descending), |
---|
| 319 | g = d3.select(this); |
---|
| 320 | |
---|
| 321 | // Compute the new x-scale. |
---|
| 322 | var x1 = d3.scale.linear() |
---|
| 323 | .domain([0, Math.max(rangez[0], markerz[0], measurez[0])]) |
---|
| 324 | .range(reverse ? [width, 0] : [0, width]); |
---|
| 325 | |
---|
| 326 | // Retrieve the old x-scale, if this is an update. |
---|
| 327 | var x0 = this.__chart__ || d3.scale.linear() |
---|
| 328 | .domain([0, Infinity]) |
---|
| 329 | .range(x1.range()); |
---|
| 330 | |
---|
| 331 | // Stash the new scale. |
---|
| 332 | this.__chart__ = x1; |
---|
| 333 | |
---|
| 334 | // Derive width-scales from the x-scales. |
---|
| 335 | var w0 = d3_chart_bulletWidth(x0), |
---|
| 336 | w1 = d3_chart_bulletWidth(x1); |
---|
| 337 | |
---|
| 338 | // Update the range rects. |
---|
| 339 | var range = g.selectAll("rect.range") |
---|
| 340 | .data(rangez); |
---|
| 341 | |
---|
| 342 | range.enter().append("svg:rect") |
---|
| 343 | .attr("class", function(d, i) { return "range s" + i; }) |
---|
| 344 | .attr("width", w0) |
---|
| 345 | .attr("height", height) |
---|
| 346 | .attr("x", reverse ? x0 : 0) |
---|
| 347 | .transition() |
---|
| 348 | .duration(duration) |
---|
| 349 | .attr("width", w1) |
---|
| 350 | .attr("x", reverse ? x1 : 0); |
---|
| 351 | |
---|
| 352 | range.transition() |
---|
| 353 | .duration(duration) |
---|
| 354 | .attr("x", reverse ? x1 : 0) |
---|
| 355 | .attr("width", w1) |
---|
| 356 | .attr("height", height); |
---|
| 357 | |
---|
| 358 | // Update the measure rects. |
---|
| 359 | var measure = g.selectAll("rect.measure") |
---|
| 360 | .data(measurez); |
---|
| 361 | |
---|
| 362 | measure.enter().append("svg:rect") |
---|
| 363 | .attr("class", function(d, i) { return "measure s" + i; }) |
---|
| 364 | .attr("width", w0) |
---|
| 365 | .attr("height", height / 3) |
---|
| 366 | .attr("x", reverse ? x0 : 0) |
---|
| 367 | .attr("y", height / 3) |
---|
| 368 | .transition() |
---|
| 369 | .duration(duration) |
---|
| 370 | .attr("width", w1) |
---|
| 371 | .attr("x", reverse ? x1 : 0); |
---|
| 372 | |
---|
| 373 | measure.transition() |
---|
| 374 | .duration(duration) |
---|
| 375 | .attr("width", w1) |
---|
| 376 | .attr("height", height / 3) |
---|
| 377 | .attr("x", reverse ? x1 : 0) |
---|
| 378 | .attr("y", height / 3); |
---|
| 379 | |
---|
| 380 | // Update the marker lines. |
---|
| 381 | var marker = g.selectAll("line.marker") |
---|
| 382 | .data(markerz); |
---|
| 383 | |
---|
| 384 | marker.enter().append("svg:line") |
---|
| 385 | .attr("class", "marker") |
---|
| 386 | .attr("x1", x0) |
---|
| 387 | .attr("x2", x0) |
---|
| 388 | .attr("y1", height / 6) |
---|
| 389 | .attr("y2", height * 5 / 6) |
---|
| 390 | .transition() |
---|
| 391 | .duration(duration) |
---|
| 392 | .attr("x1", x1) |
---|
| 393 | .attr("x2", x1); |
---|
| 394 | |
---|
| 395 | marker.transition() |
---|
| 396 | .duration(duration) |
---|
| 397 | .attr("x1", x1) |
---|
| 398 | .attr("x2", x1) |
---|
| 399 | .attr("y1", height / 6) |
---|
| 400 | .attr("y2", height * 5 / 6); |
---|
| 401 | |
---|
| 402 | // Compute the tick format. |
---|
| 403 | var format = tickFormat || x1.tickFormat(8); |
---|
| 404 | |
---|
| 405 | // Update the tick groups. |
---|
| 406 | var tick = g.selectAll("g.tick") |
---|
| 407 | .data(x1.ticks(8), function(d) { |
---|
| 408 | return this.textContent || format(d); |
---|
| 409 | }); |
---|
| 410 | |
---|
| 411 | // Initialize the ticks with the old scale, x0. |
---|
| 412 | var tickEnter = tick.enter().append("svg:g") |
---|
| 413 | .attr("class", "tick") |
---|
| 414 | .attr("transform", d3_chart_bulletTranslate(x0)) |
---|
| 415 | .style("opacity", 1e-6); |
---|
| 416 | |
---|
| 417 | tickEnter.append("svg:line") |
---|
| 418 | .attr("y1", height) |
---|
| 419 | .attr("y2", height * 7 / 6); |
---|
| 420 | |
---|
| 421 | tickEnter.append("svg:text") |
---|
| 422 | .attr("text-anchor", "middle") |
---|
| 423 | .attr("dy", "1em") |
---|
| 424 | .attr("y", height * 7 / 6) |
---|
| 425 | .text(format); |
---|
| 426 | |
---|
| 427 | // Transition the entering ticks to the new scale, x1. |
---|
| 428 | tickEnter.transition() |
---|
| 429 | .duration(duration) |
---|
| 430 | .attr("transform", d3_chart_bulletTranslate(x1)) |
---|
| 431 | .style("opacity", 1); |
---|
| 432 | |
---|
| 433 | // Transition the updating ticks to the new scale, x1. |
---|
| 434 | var tickUpdate = tick.transition() |
---|
| 435 | .duration(duration) |
---|
| 436 | .attr("transform", d3_chart_bulletTranslate(x1)) |
---|
| 437 | .style("opacity", 1); |
---|
| 438 | |
---|
| 439 | tickUpdate.select("line") |
---|
| 440 | .attr("y1", height) |
---|
| 441 | .attr("y2", height * 7 / 6); |
---|
| 442 | |
---|
| 443 | tickUpdate.select("text") |
---|
| 444 | .attr("y", height * 7 / 6); |
---|
| 445 | |
---|
| 446 | // Transition the exiting ticks to the new scale, x1. |
---|
| 447 | tick.exit().transition() |
---|
| 448 | .duration(duration) |
---|
| 449 | .attr("transform", d3_chart_bulletTranslate(x1)) |
---|
| 450 | .style("opacity", 1e-6) |
---|
| 451 | .remove(); |
---|
| 452 | }); |
---|
| 453 | d3.timer.flush(); |
---|
| 454 | } |
---|
| 455 | |
---|
| 456 | // left, right, top, bottom |
---|
| 457 | bullet.orient = function(x) { |
---|
| 458 | if (!arguments.length) return orient; |
---|
| 459 | orient = x; |
---|
| 460 | reverse = orient == "right" || orient == "bottom"; |
---|
| 461 | return bullet; |
---|
| 462 | }; |
---|
| 463 | |
---|
| 464 | // ranges (bad, satisfactory, good) |
---|
| 465 | bullet.ranges = function(x) { |
---|
| 466 | if (!arguments.length) return ranges; |
---|
| 467 | ranges = x; |
---|
| 468 | return bullet; |
---|
| 469 | }; |
---|
| 470 | |
---|
| 471 | // markers (previous, goal) |
---|
| 472 | bullet.markers = function(x) { |
---|
| 473 | if (!arguments.length) return markers; |
---|
| 474 | markers = x; |
---|
| 475 | return bullet; |
---|
| 476 | }; |
---|
| 477 | |
---|
| 478 | // measures (actual, forecast) |
---|
| 479 | bullet.measures = function(x) { |
---|
| 480 | if (!arguments.length) return measures; |
---|
| 481 | measures = x; |
---|
| 482 | return bullet; |
---|
| 483 | }; |
---|
| 484 | |
---|
| 485 | bullet.width = function(x) { |
---|
| 486 | if (!arguments.length) return width; |
---|
| 487 | width = x; |
---|
| 488 | return bullet; |
---|
| 489 | }; |
---|
| 490 | |
---|
| 491 | bullet.height = function(x) { |
---|
| 492 | if (!arguments.length) return height; |
---|
| 493 | height = x; |
---|
| 494 | return bullet; |
---|
| 495 | }; |
---|
| 496 | |
---|
| 497 | bullet.tickFormat = function(x) { |
---|
| 498 | if (!arguments.length) return tickFormat; |
---|
| 499 | tickFormat = x; |
---|
| 500 | return bullet; |
---|
| 501 | }; |
---|
| 502 | |
---|
| 503 | bullet.duration = function(x) { |
---|
| 504 | if (!arguments.length) return duration; |
---|
| 505 | duration = x; |
---|
| 506 | return bullet; |
---|
| 507 | }; |
---|
| 508 | |
---|
| 509 | return bullet; |
---|
| 510 | }; |
---|
| 511 | |
---|
| 512 | function d3_chart_bulletRanges(d) { |
---|
| 513 | return d.ranges; |
---|
| 514 | } |
---|
| 515 | |
---|
| 516 | function d3_chart_bulletMarkers(d) { |
---|
| 517 | return d.markers; |
---|
| 518 | } |
---|
| 519 | |
---|
| 520 | function d3_chart_bulletMeasures(d) { |
---|
| 521 | return d.measures; |
---|
| 522 | } |
---|
| 523 | |
---|
| 524 | function d3_chart_bulletTranslate(x) { |
---|
| 525 | return function(d) { |
---|
| 526 | return "translate(" + x(d) + ",0)"; |
---|
| 527 | }; |
---|
| 528 | } |
---|
| 529 | |
---|
| 530 | function d3_chart_bulletWidth(x) { |
---|
| 531 | var x0 = x(0); |
---|
| 532 | return function(d) { |
---|
| 533 | return Math.abs(x(d) - x0); |
---|
| 534 | }; |
---|
| 535 | } |
---|
| 536 | // Implements a horizon layout, which is a variation of a single-series |
---|
| 537 | // area chart where the area is folded into multiple bands. Color is used to |
---|
| 538 | // encode band, allowing the size of the chart to be reduced significantly |
---|
| 539 | // without impeding readability. This layout algorithm is based on the work of |
---|
| 540 | // J. Heer, N. Kong and M. Agrawala in "Sizing the Horizon: The Effects of Chart |
---|
| 541 | // Size and Layering on the Graphical Perception of Time Series Visualizations", |
---|
| 542 | // CHI 2009. http://hci.stanford.edu/publications/2009/heer-horizon-chi09.pdf |
---|
| 543 | d3.chart.horizon = function() { |
---|
| 544 | var bands = 1, // between 1 and 5, typically |
---|
| 545 | mode = "offset", // or mirror |
---|
| 546 | interpolate = "linear", // or basis, monotone, step-before, etc. |
---|
| 547 | x = d3_chart_horizonX, |
---|
| 548 | y = d3_chart_horizonY, |
---|
| 549 | w = 960, |
---|
| 550 | h = 40, |
---|
| 551 | duration = 0; |
---|
| 552 | |
---|
| 553 | var color = d3.scale.linear() |
---|
| 554 | .domain([-1, 0, 1]) |
---|
| 555 | .range(["#d62728", "#fff", "#1f77b4"]); |
---|
| 556 | |
---|
| 557 | // For each small multiple⊠|
---|
| 558 | function horizon(g) { |
---|
| 559 | g.each(function(d, i) { |
---|
| 560 | var g = d3.select(this), |
---|
| 561 | n = 2 * bands + 1, |
---|
| 562 | xMin = Infinity, |
---|
| 563 | xMax = -Infinity, |
---|
| 564 | yMax = -Infinity, |
---|
| 565 | x0, // old x-scale |
---|
| 566 | y0, // old y-scale |
---|
| 567 | id; // unique id for paths |
---|
| 568 | |
---|
| 569 | // Compute x- and y-values along with extents. |
---|
| 570 | var data = d.map(function(d, i) { |
---|
| 571 | var xv = x.call(this, d, i), |
---|
| 572 | yv = y.call(this, d, i); |
---|
| 573 | if (xv < xMin) xMin = xv; |
---|
| 574 | if (xv > xMax) xMax = xv; |
---|
| 575 | if (-yv > yMax) yMax = -yv; |
---|
| 576 | if (yv > yMax) yMax = yv; |
---|
| 577 | return [xv, yv]; |
---|
| 578 | }); |
---|
| 579 | |
---|
| 580 | // Compute the new x- and y-scales. |
---|
| 581 | var x1 = d3.scale.linear().domain([xMin, xMax]).range([0, w]), |
---|
| 582 | y1 = d3.scale.linear().domain([0, yMax]).range([0, h * bands]); |
---|
| 583 | |
---|
| 584 | // Retrieve the old scales, if this is an update. |
---|
| 585 | if (this.__chart__) { |
---|
| 586 | x0 = this.__chart__.x; |
---|
| 587 | y0 = this.__chart__.y; |
---|
| 588 | id = this.__chart__.id; |
---|
| 589 | } else { |
---|
| 590 | x0 = d3.scale.linear().domain([0, Infinity]).range(x1.range()); |
---|
| 591 | y0 = d3.scale.linear().domain([0, Infinity]).range(y1.range()); |
---|
| 592 | id = ++d3_chart_horizonId; |
---|
| 593 | } |
---|
| 594 | |
---|
| 595 | // We'll use a defs to store the area path and the clip path. |
---|
| 596 | var defs = g.selectAll("defs") |
---|
| 597 | .data([data]); |
---|
| 598 | |
---|
| 599 | var defsEnter = defs.enter().append("svg:defs"); |
---|
| 600 | |
---|
| 601 | // The clip path is a simple rect. |
---|
| 602 | defsEnter.append("svg:clipPath") |
---|
| 603 | .attr("id", "d3_chart_horizon_clip" + id) |
---|
| 604 | .append("svg:rect") |
---|
| 605 | .attr("width", w) |
---|
| 606 | .attr("height", h); |
---|
| 607 | |
---|
| 608 | defs.select("rect").transition() |
---|
| 609 | .duration(duration) |
---|
| 610 | .attr("width", w) |
---|
| 611 | .attr("height", h); |
---|
| 612 | |
---|
| 613 | // The area path is rendered with our resuable d3.svg.area. |
---|
| 614 | defsEnter.append("svg:path") |
---|
| 615 | .attr("id", "d3_chart_horizon_path" + id) |
---|
| 616 | .attr("d", d3_chart_horizonArea |
---|
| 617 | .interpolate(interpolate) |
---|
| 618 | .x(function(d) { return x0(d[0]); }) |
---|
| 619 | .y0(h * bands) |
---|
| 620 | .y1(function(d) { return h * bands - y0(d[1]); })) |
---|
| 621 | .transition() |
---|
| 622 | .duration(duration) |
---|
| 623 | .attr("d", d3_chart_horizonArea |
---|
| 624 | .x(function(d) { return x1(d[0]); }) |
---|
| 625 | .y1(function(d) { return h * bands - y1(d[1]); })); |
---|
| 626 | |
---|
| 627 | defs.select("path").transition() |
---|
| 628 | .duration(duration) |
---|
| 629 | .attr("d", d3_chart_horizonArea); |
---|
| 630 | |
---|
| 631 | // We'll use a container to clip all horizon layers at once. |
---|
| 632 | g.selectAll("g") |
---|
| 633 | .data([null]) |
---|
| 634 | .enter().append("svg:g") |
---|
| 635 | .attr("clip-path", "url(#d3_chart_horizon_clip" + id + ")"); |
---|
| 636 | |
---|
| 637 | // Define the transform function based on the mode. |
---|
| 638 | var transform = mode == "offset" |
---|
| 639 | ? function(d) { return "translate(0," + (d + (d < 0) - bands) * h + ")"; } |
---|
| 640 | : function(d) { return (d < 0 ? "scale(1,-1)" : "") + "translate(0," + (d - bands) * h + ")"; }; |
---|
| 641 | |
---|
| 642 | // Instantiate each copy of the path with different transforms. |
---|
| 643 | var u = g.select("g").selectAll("use") |
---|
| 644 | .data(d3.range(-1, -bands - 1, -1).concat(d3.range(1, bands + 1)), Number); |
---|
| 645 | |
---|
| 646 | // TODO don't fudge the enter transition |
---|
| 647 | u.enter().append("svg:use") |
---|
| 648 | .attr("xlink:href", "#d3_chart_horizon_path" + id) |
---|
| 649 | .attr("transform", function(d) { return transform(d + (d > 0 ? 1 : -1)); }) |
---|
| 650 | .style("fill", color) |
---|
| 651 | .transition() |
---|
| 652 | .duration(duration) |
---|
| 653 | .attr("transform", transform); |
---|
| 654 | |
---|
| 655 | u.transition() |
---|
| 656 | .duration(duration) |
---|
| 657 | .attr("transform", transform) |
---|
| 658 | .style("fill", color); |
---|
| 659 | |
---|
| 660 | u.exit().transition() |
---|
| 661 | .duration(duration) |
---|
| 662 | .attr("transform", transform) |
---|
| 663 | .remove(); |
---|
| 664 | |
---|
| 665 | // Stash the new scales. |
---|
| 666 | this.__chart__ = {x: x1, y: y1, id: id}; |
---|
| 667 | }); |
---|
| 668 | d3.timer.flush(); |
---|
| 669 | } |
---|
| 670 | |
---|
| 671 | horizon.duration = function(x) { |
---|
| 672 | if (!arguments.length) return duration; |
---|
| 673 | duration = +x; |
---|
| 674 | return horizon; |
---|
| 675 | }; |
---|
| 676 | |
---|
| 677 | horizon.bands = function(x) { |
---|
| 678 | if (!arguments.length) return bands; |
---|
| 679 | bands = +x; |
---|
| 680 | color.domain([-bands, 0, bands]); |
---|
| 681 | return horizon; |
---|
| 682 | }; |
---|
| 683 | |
---|
| 684 | horizon.mode = function(x) { |
---|
| 685 | if (!arguments.length) return mode; |
---|
| 686 | mode = x + ""; |
---|
| 687 | return horizon; |
---|
| 688 | }; |
---|
| 689 | |
---|
| 690 | horizon.colors = function(x) { |
---|
| 691 | if (!arguments.length) return color.range(); |
---|
| 692 | color.range(x); |
---|
| 693 | return horizon; |
---|
| 694 | }; |
---|
| 695 | |
---|
| 696 | horizon.interpolate = function(x) { |
---|
| 697 | if (!arguments.length) return interpolate; |
---|
| 698 | interpolate = x + ""; |
---|
| 699 | return horizon; |
---|
| 700 | }; |
---|
| 701 | |
---|
| 702 | horizon.x = function(z) { |
---|
| 703 | if (!arguments.length) return x; |
---|
| 704 | x = z; |
---|
| 705 | return horizon; |
---|
| 706 | }; |
---|
| 707 | |
---|
| 708 | horizon.y = function(z) { |
---|
| 709 | if (!arguments.length) return y; |
---|
| 710 | y = z; |
---|
| 711 | return horizon; |
---|
| 712 | }; |
---|
| 713 | |
---|
| 714 | horizon.width = function(x) { |
---|
| 715 | if (!arguments.length) return w; |
---|
| 716 | w = +x; |
---|
| 717 | return horizon; |
---|
| 718 | }; |
---|
| 719 | |
---|
| 720 | horizon.height = function(x) { |
---|
| 721 | if (!arguments.length) return h; |
---|
| 722 | h = +x; |
---|
| 723 | return horizon; |
---|
| 724 | }; |
---|
| 725 | |
---|
| 726 | return horizon; |
---|
| 727 | }; |
---|
| 728 | |
---|
| 729 | var d3_chart_horizonArea = d3.svg.area(), |
---|
| 730 | d3_chart_horizonId = 0; |
---|
| 731 | |
---|
| 732 | function d3_chart_horizonX(d) { |
---|
| 733 | return d[0]; |
---|
| 734 | } |
---|
| 735 | |
---|
| 736 | function d3_chart_horizonY(d) { |
---|
| 737 | return d[1]; |
---|
| 738 | } |
---|
| 739 | // Based on http://vis.stanford.edu/protovis/ex/qqplot.html |
---|
| 740 | d3.chart.qq = function() { |
---|
| 741 | var width = 1, |
---|
| 742 | height = 1, |
---|
| 743 | duration = 0, |
---|
| 744 | domain = null, |
---|
| 745 | tickFormat = null, |
---|
| 746 | n = 100, |
---|
| 747 | x = d3_chart_qqX, |
---|
| 748 | y = d3_chart_qqY; |
---|
| 749 | |
---|
| 750 | // For each small multiple⊠|
---|
| 751 | function qq(g) { |
---|
| 752 | g.each(function(d, i) { |
---|
| 753 | var g = d3.select(this), |
---|
| 754 | qx = d3_chart_qqQuantiles(n, x.call(this, d, i)), |
---|
| 755 | qy = d3_chart_qqQuantiles(n, y.call(this, d, i)), |
---|
| 756 | xd = domain && domain.call(this, d, i) || [d3.min(qx), d3.max(qx)], // new x-domain |
---|
| 757 | yd = domain && domain.call(this, d, i) || [d3.min(qy), d3.max(qy)], // new y-domain |
---|
| 758 | x0, // old x-scale |
---|
| 759 | y0; // old y-scale |
---|
| 760 | |
---|
| 761 | // Compute the new x-scale. |
---|
| 762 | var x1 = d3.scale.linear() |
---|
| 763 | .domain(xd) |
---|
| 764 | .range([0, width]); |
---|
| 765 | |
---|
| 766 | // Compute the new y-scale. |
---|
| 767 | var y1 = d3.scale.linear() |
---|
| 768 | .domain(yd) |
---|
| 769 | .range([height, 0]); |
---|
| 770 | |
---|
| 771 | // Retrieve the old scales, if this is an update. |
---|
| 772 | if (this.__chart__) { |
---|
| 773 | x0 = this.__chart__.x; |
---|
| 774 | y0 = this.__chart__.y; |
---|
| 775 | } else { |
---|
| 776 | x0 = d3.scale.linear().domain([0, Infinity]).range(x1.range()); |
---|
| 777 | y0 = d3.scale.linear().domain([0, Infinity]).range(y1.range()); |
---|
| 778 | } |
---|
| 779 | |
---|
| 780 | // Stash the new scales. |
---|
| 781 | this.__chart__ = {x: x1, y: y1}; |
---|
| 782 | |
---|
| 783 | // Update diagonal line. |
---|
| 784 | var diagonal = g.selectAll("line.diagonal") |
---|
| 785 | .data([null]); |
---|
| 786 | |
---|
| 787 | diagonal.enter().append("svg:line") |
---|
| 788 | .attr("class", "diagonal") |
---|
| 789 | .attr("x1", x1(yd[0])) |
---|
| 790 | .attr("y1", y1(xd[0])) |
---|
| 791 | .attr("x2", x1(yd[1])) |
---|
| 792 | .attr("y2", y1(xd[1])); |
---|
| 793 | |
---|
| 794 | diagonal.transition() |
---|
| 795 | .duration(duration) |
---|
| 796 | .attr("x1", x1(yd[0])) |
---|
| 797 | .attr("y1", y1(xd[0])) |
---|
| 798 | .attr("x2", x1(yd[1])) |
---|
| 799 | .attr("y2", y1(xd[1])); |
---|
| 800 | |
---|
| 801 | // Update quantile plots. |
---|
| 802 | var circle = g.selectAll("circle") |
---|
| 803 | .data(d3.range(n).map(function(i) { |
---|
| 804 | return {x: qx[i], y: qy[i]}; |
---|
| 805 | })); |
---|
| 806 | |
---|
| 807 | circle.enter().append("svg:circle") |
---|
| 808 | .attr("class", "quantile") |
---|
| 809 | .attr("r", 4.5) |
---|
| 810 | .attr("cx", function(d) { return x0(d.x); }) |
---|
| 811 | .attr("cy", function(d) { return y0(d.y); }) |
---|
| 812 | .style("opacity", 1e-6) |
---|
| 813 | .transition() |
---|
| 814 | .duration(duration) |
---|
| 815 | .attr("cx", function(d) { return x1(d.x); }) |
---|
| 816 | .attr("cy", function(d) { return y1(d.y); }) |
---|
| 817 | .style("opacity", 1); |
---|
| 818 | |
---|
| 819 | circle.transition() |
---|
| 820 | .duration(duration) |
---|
| 821 | .attr("cx", function(d) { return x1(d.x); }) |
---|
| 822 | .attr("cy", function(d) { return y1(d.y); }) |
---|
| 823 | .style("opacity", 1); |
---|
| 824 | |
---|
| 825 | circle.exit().transition() |
---|
| 826 | .duration(duration) |
---|
| 827 | .attr("cx", function(d) { return x1(d.x); }) |
---|
| 828 | .attr("cy", function(d) { return y1(d.y); }) |
---|
| 829 | .style("opacity", 1e-6) |
---|
| 830 | .remove(); |
---|
| 831 | |
---|
| 832 | var xformat = tickFormat || x1.tickFormat(4), |
---|
| 833 | yformat = tickFormat || y1.tickFormat(4), |
---|
| 834 | tx = function(d) { return "translate(" + x1(d) + "," + height + ")"; }, |
---|
| 835 | ty = function(d) { return "translate(0," + y1(d) + ")"; }; |
---|
| 836 | |
---|
| 837 | // Update x-ticks. |
---|
| 838 | var xtick = g.selectAll("g.x.tick") |
---|
| 839 | .data(x1.ticks(4), function(d) { |
---|
| 840 | return this.textContent || xformat(d); |
---|
| 841 | }); |
---|
| 842 | |
---|
| 843 | var xtickEnter = xtick.enter().append("svg:g") |
---|
| 844 | .attr("class", "x tick") |
---|
| 845 | .attr("transform", function(d) { return "translate(" + x0(d) + "," + height + ")"; }) |
---|
| 846 | .style("opacity", 1e-6); |
---|
| 847 | |
---|
| 848 | xtickEnter.append("svg:line") |
---|
| 849 | .attr("y1", 0) |
---|
| 850 | .attr("y2", -6); |
---|
| 851 | |
---|
| 852 | xtickEnter.append("svg:text") |
---|
| 853 | .attr("text-anchor", "middle") |
---|
| 854 | .attr("dy", "1em") |
---|
| 855 | .text(xformat); |
---|
| 856 | |
---|
| 857 | // Transition the entering ticks to the new scale, x1. |
---|
| 858 | xtickEnter.transition() |
---|
| 859 | .duration(duration) |
---|
| 860 | .attr("transform", tx) |
---|
| 861 | .style("opacity", 1); |
---|
| 862 | |
---|
| 863 | // Transition the updating ticks to the new scale, x1. |
---|
| 864 | xtick.transition() |
---|
| 865 | .duration(duration) |
---|
| 866 | .attr("transform", tx) |
---|
| 867 | .style("opacity", 1); |
---|
| 868 | |
---|
| 869 | // Transition the exiting ticks to the new scale, x1. |
---|
| 870 | xtick.exit().transition() |
---|
| 871 | .duration(duration) |
---|
| 872 | .attr("transform", tx) |
---|
| 873 | .style("opacity", 1e-6) |
---|
| 874 | .remove(); |
---|
| 875 | |
---|
| 876 | // Update ticks. |
---|
| 877 | var ytick = g.selectAll("g.y.tick") |
---|
| 878 | .data(y1.ticks(4), function(d) { |
---|
| 879 | return this.textContent || yformat(d); |
---|
| 880 | }); |
---|
| 881 | |
---|
| 882 | var ytickEnter = ytick.enter().append("svg:g") |
---|
| 883 | .attr("class", "y tick") |
---|
| 884 | .attr("transform", function(d) { return "translate(0," + y0(d) + ")"; }) |
---|
| 885 | .style("opacity", 1e-6); |
---|
| 886 | |
---|
| 887 | ytickEnter.append("svg:line") |
---|
| 888 | .attr("x1", 0) |
---|
| 889 | .attr("x2", 6); |
---|
| 890 | |
---|
| 891 | ytickEnter.append("svg:text") |
---|
| 892 | .attr("text-anchor", "end") |
---|
| 893 | .attr("dx", "-.5em") |
---|
| 894 | .attr("dy", ".3em") |
---|
| 895 | .text(yformat); |
---|
| 896 | |
---|
| 897 | // Transition the entering ticks to the new scale, y1. |
---|
| 898 | ytickEnter.transition() |
---|
| 899 | .duration(duration) |
---|
| 900 | .attr("transform", ty) |
---|
| 901 | .style("opacity", 1); |
---|
| 902 | |
---|
| 903 | // Transition the updating ticks to the new scale, y1. |
---|
| 904 | ytick.transition() |
---|
| 905 | .duration(duration) |
---|
| 906 | .attr("transform", ty) |
---|
| 907 | .style("opacity", 1); |
---|
| 908 | |
---|
| 909 | // Transition the exiting ticks to the new scale, y1. |
---|
| 910 | ytick.exit().transition() |
---|
| 911 | .duration(duration) |
---|
| 912 | .attr("transform", ty) |
---|
| 913 | .style("opacity", 1e-6) |
---|
| 914 | .remove(); |
---|
| 915 | }); |
---|
| 916 | } |
---|
| 917 | |
---|
| 918 | qq.width = function(x) { |
---|
| 919 | if (!arguments.length) return width; |
---|
| 920 | width = x; |
---|
| 921 | return qq; |
---|
| 922 | }; |
---|
| 923 | |
---|
| 924 | qq.height = function(x) { |
---|
| 925 | if (!arguments.length) return height; |
---|
| 926 | height = x; |
---|
| 927 | return qq; |
---|
| 928 | }; |
---|
| 929 | |
---|
| 930 | qq.duration = function(x) { |
---|
| 931 | if (!arguments.length) return duration; |
---|
| 932 | duration = x; |
---|
| 933 | return qq; |
---|
| 934 | }; |
---|
| 935 | |
---|
| 936 | qq.domain = function(x) { |
---|
| 937 | if (!arguments.length) return domain; |
---|
| 938 | domain = x == null ? x : d3.functor(x); |
---|
| 939 | return qq; |
---|
| 940 | }; |
---|
| 941 | |
---|
| 942 | qq.count = function(z) { |
---|
| 943 | if (!arguments.length) return n; |
---|
| 944 | n = z; |
---|
| 945 | return qq; |
---|
| 946 | }; |
---|
| 947 | |
---|
| 948 | qq.x = function(z) { |
---|
| 949 | if (!arguments.length) return x; |
---|
| 950 | x = z; |
---|
| 951 | return qq; |
---|
| 952 | }; |
---|
| 953 | |
---|
| 954 | qq.y = function(z) { |
---|
| 955 | if (!arguments.length) return y; |
---|
| 956 | y = z; |
---|
| 957 | return qq; |
---|
| 958 | }; |
---|
| 959 | |
---|
| 960 | qq.tickFormat = function(x) { |
---|
| 961 | if (!arguments.length) return tickFormat; |
---|
| 962 | tickFormat = x; |
---|
| 963 | return qq; |
---|
| 964 | }; |
---|
| 965 | |
---|
| 966 | return qq; |
---|
| 967 | }; |
---|
| 968 | |
---|
| 969 | function d3_chart_qqQuantiles(n, values) { |
---|
| 970 | var m = values.length - 1; |
---|
| 971 | values = values.slice().sort(d3.ascending); |
---|
| 972 | return d3.range(n).map(function(i) { |
---|
| 973 | return values[~~(i * m / n)]; |
---|
| 974 | }); |
---|
| 975 | } |
---|
| 976 | |
---|
| 977 | function d3_chart_qqX(d) { |
---|
| 978 | return d.x; |
---|
| 979 | } |
---|
| 980 | |
---|
| 981 | function d3_chart_qqY(d) { |
---|
| 982 | return d.y; |
---|
| 983 | } |
---|
| 984 | })(); |
---|