browser/devtools/shared/widgets/Chart.jsm

Wed, 31 Dec 2014 06:55:46 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:55:46 +0100
changeset 1
ca08bd8f51b2
permissions
-rw-r--r--

Added tag TORBROWSER_REPLICA for changeset 6474c204b198

michael@0 1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6 "use strict";
michael@0 7
michael@0 8 const Ci = Components.interfaces;
michael@0 9 const Cu = Components.utils;
michael@0 10
michael@0 11 const NET_STRINGS_URI = "chrome://browser/locale/devtools/netmonitor.properties";
michael@0 12 const SVG_NS = "http://www.w3.org/2000/svg";
michael@0 13 const PI = Math.PI;
michael@0 14 const TAU = PI * 2;
michael@0 15 const EPSILON = 0.0000001;
michael@0 16 const NAMED_SLICE_MIN_ANGLE = TAU / 8;
michael@0 17 const NAMED_SLICE_TEXT_DISTANCE_RATIO = 1.9;
michael@0 18 const HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO = 20;
michael@0 19
michael@0 20 Cu.import("resource://gre/modules/Services.jsm");
michael@0 21 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 22 Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
michael@0 23 Cu.import("resource://gre/modules/devtools/event-emitter.js");
michael@0 24
michael@0 25 this.EXPORTED_SYMBOLS = ["Chart"];
michael@0 26
michael@0 27 /**
michael@0 28 * Localization convenience methods.
michael@0 29 */
michael@0 30 let L10N = new ViewHelpers.L10N(NET_STRINGS_URI);
michael@0 31
michael@0 32 /**
michael@0 33 * A factory for creating charts.
michael@0 34 * Example usage: let myChart = Chart.Pie(document, { ... });
michael@0 35 */
michael@0 36 let Chart = {
michael@0 37 Pie: createPieChart,
michael@0 38 Table: createTableChart,
michael@0 39 PieTable: createPieTableChart
michael@0 40 };
michael@0 41
michael@0 42 /**
michael@0 43 * A simple pie chart proxy for the underlying view.
michael@0 44 * Each item in the `slices` property represents a [data, node] pair containing
michael@0 45 * the data used to create the slice and the nsIDOMNode displaying it.
michael@0 46 *
michael@0 47 * @param nsIDOMNode node
michael@0 48 * The node representing the view for this chart.
michael@0 49 */
michael@0 50 function PieChart(node) {
michael@0 51 this.node = node;
michael@0 52 this.slices = new WeakMap();
michael@0 53 EventEmitter.decorate(this);
michael@0 54 }
michael@0 55
michael@0 56 /**
michael@0 57 * A simple table chart proxy for the underlying view.
michael@0 58 * Each item in the `rows` property represents a [data, node] pair containing
michael@0 59 * the data used to create the row and the nsIDOMNode displaying it.
michael@0 60 *
michael@0 61 * @param nsIDOMNode node
michael@0 62 * The node representing the view for this chart.
michael@0 63 */
michael@0 64 function TableChart(node) {
michael@0 65 this.node = node;
michael@0 66 this.rows = new WeakMap();
michael@0 67 EventEmitter.decorate(this);
michael@0 68 }
michael@0 69
michael@0 70 /**
michael@0 71 * A simple pie+table chart proxy for the underlying view.
michael@0 72 *
michael@0 73 * @param nsIDOMNode node
michael@0 74 * The node representing the view for this chart.
michael@0 75 * @param PieChart pie
michael@0 76 * The pie chart proxy.
michael@0 77 * @param TableChart table
michael@0 78 * The table chart proxy.
michael@0 79 */
michael@0 80 function PieTableChart(node, pie, table) {
michael@0 81 this.node = node;
michael@0 82 this.pie = pie;
michael@0 83 this.table = table;
michael@0 84 EventEmitter.decorate(this);
michael@0 85 }
michael@0 86
michael@0 87 /**
michael@0 88 * Creates the DOM for a pie+table chart.
michael@0 89 *
michael@0 90 * @param nsIDocument document
michael@0 91 * The document responsible with creating the DOM.
michael@0 92 * @param object
michael@0 93 * An object containing all or some of the following properties:
michael@0 94 * - title: a string displayed as the table chart's (description)/local
michael@0 95 * - diameter: the diameter of the pie chart, in pixels
michael@0 96 * - data: an array of items used to display each slice in the pie
michael@0 97 * and each row in the table;
michael@0 98 * @see `createPieChart` and `createTableChart` for details.
michael@0 99 * - strings: @see `createTableChart` for details.
michael@0 100 * - totals: @see `createTableChart` for details.
michael@0 101 * - sorted: a flag specifying if the `data` should be sorted
michael@0 102 * ascending by `size`.
michael@0 103 * @return PieTableChart
michael@0 104 * A pie+table chart proxy instance, which emits the following events:
michael@0 105 * - "mouseenter", when the mouse enters a slice or a row
michael@0 106 * - "mouseleave", when the mouse leaves a slice or a row
michael@0 107 * - "click", when the mouse enters a slice or a row
michael@0 108 */
michael@0 109 function createPieTableChart(document, { title, diameter, data, strings, totals, sorted }) {
michael@0 110 if (data && sorted) {
michael@0 111 data = data.slice().sort((a, b) => +(a.size < b.size));
michael@0 112 }
michael@0 113
michael@0 114 let pie = Chart.Pie(document, {
michael@0 115 width: diameter,
michael@0 116 data: data
michael@0 117 });
michael@0 118
michael@0 119 let table = Chart.Table(document, {
michael@0 120 title: title,
michael@0 121 data: data,
michael@0 122 strings: strings,
michael@0 123 totals: totals
michael@0 124 });
michael@0 125
michael@0 126 let container = document.createElement("hbox");
michael@0 127 container.className = "pie-table-chart-container";
michael@0 128 container.appendChild(pie.node);
michael@0 129 container.appendChild(table.node);
michael@0 130
michael@0 131 let proxy = new PieTableChart(container, pie, table);
michael@0 132
michael@0 133 pie.on("click", (event, item) => {
michael@0 134 proxy.emit(event, item)
michael@0 135 });
michael@0 136
michael@0 137 table.on("click", (event, item) => {
michael@0 138 proxy.emit(event, item)
michael@0 139 });
michael@0 140
michael@0 141 pie.on("mouseenter", (event, item) => {
michael@0 142 proxy.emit(event, item);
michael@0 143 if (table.rows.has(item)) {
michael@0 144 table.rows.get(item).setAttribute("focused", "");
michael@0 145 }
michael@0 146 });
michael@0 147
michael@0 148 pie.on("mouseleave", (event, item) => {
michael@0 149 proxy.emit(event, item);
michael@0 150 if (table.rows.has(item)) {
michael@0 151 table.rows.get(item).removeAttribute("focused");
michael@0 152 }
michael@0 153 });
michael@0 154
michael@0 155 table.on("mouseenter", (event, item) => {
michael@0 156 proxy.emit(event, item);
michael@0 157 if (pie.slices.has(item)) {
michael@0 158 pie.slices.get(item).setAttribute("focused", "");
michael@0 159 }
michael@0 160 });
michael@0 161
michael@0 162 table.on("mouseleave", (event, item) => {
michael@0 163 proxy.emit(event, item);
michael@0 164 if (pie.slices.has(item)) {
michael@0 165 pie.slices.get(item).removeAttribute("focused");
michael@0 166 }
michael@0 167 });
michael@0 168
michael@0 169 return proxy;
michael@0 170 }
michael@0 171
michael@0 172 /**
michael@0 173 * Creates the DOM for a pie chart based on the specified properties.
michael@0 174 *
michael@0 175 * @param nsIDocument document
michael@0 176 * The document responsible with creating the DOM.
michael@0 177 * @param object
michael@0 178 * An object containing all or some of the following properties:
michael@0 179 * - data: an array of items used to display each slice; all the items
michael@0 180 * should be objects containing a `size` and a `label` property.
michael@0 181 * e.g: [{
michael@0 182 * size: 1,
michael@0 183 * label: "foo"
michael@0 184 * }, {
michael@0 185 * size: 2,
michael@0 186 * label: "bar"
michael@0 187 * }];
michael@0 188 * - width: the width of the chart, in pixels
michael@0 189 * - height: optional, the height of the chart, in pixels.
michael@0 190 * - centerX: optional, the X-axis center of the chart, in pixels.
michael@0 191 * - centerY: optional, the Y-axis center of the chart, in pixels.
michael@0 192 * - radius: optional, the radius of the chart, in pixels.
michael@0 193 * @return PieChart
michael@0 194 * A pie chart proxy instance, which emits the following events:
michael@0 195 * - "mouseenter", when the mouse enters a slice
michael@0 196 * - "mouseleave", when the mouse leaves a slice
michael@0 197 * - "click", when the mouse clicks a slice
michael@0 198 */
michael@0 199 function createPieChart(document, { data, width, height, centerX, centerY, radius }) {
michael@0 200 height = height || width;
michael@0 201 centerX = centerX || width / 2;
michael@0 202 centerY = centerY || height / 2;
michael@0 203 radius = radius || (width + height) / 4;
michael@0 204 let isPlaceholder = false;
michael@0 205
michael@0 206 // Filter out very small sizes, as they'll just render invisible slices.
michael@0 207 data = data ? data.filter(e => e.size > EPSILON) : null;
michael@0 208
michael@0 209 // If there's no data available, display an empty placeholder.
michael@0 210 if (!data) {
michael@0 211 data = loadingPieChartData;
michael@0 212 isPlaceholder = true;
michael@0 213 }
michael@0 214 if (!data.length) {
michael@0 215 data = emptyPieChartData;
michael@0 216 isPlaceholder = true;
michael@0 217 }
michael@0 218
michael@0 219 let container = document.createElementNS(SVG_NS, "svg");
michael@0 220 container.setAttribute("class", "generic-chart-container pie-chart-container");
michael@0 221 container.setAttribute("pack", "center");
michael@0 222 container.setAttribute("flex", "1");
michael@0 223 container.setAttribute("width", width);
michael@0 224 container.setAttribute("height", height);
michael@0 225 container.setAttribute("viewBox", "0 0 " + width + " " + height);
michael@0 226 container.setAttribute("slices", data.length);
michael@0 227 container.setAttribute("placeholder", isPlaceholder);
michael@0 228
michael@0 229 let proxy = new PieChart(container);
michael@0 230
michael@0 231 let total = data.reduce((acc, e) => acc + e.size, 0);
michael@0 232 let angles = data.map(e => e.size / total * (TAU - EPSILON));
michael@0 233 let largest = data.reduce((a, b) => a.size > b.size ? a : b);
michael@0 234 let smallest = data.reduce((a, b) => a.size < b.size ? a : b);
michael@0 235
michael@0 236 let textDistance = radius / NAMED_SLICE_TEXT_DISTANCE_RATIO;
michael@0 237 let translateDistance = radius / HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO;
michael@0 238 let startAngle = TAU;
michael@0 239 let endAngle = 0;
michael@0 240 let midAngle = 0;
michael@0 241 radius -= translateDistance;
michael@0 242
michael@0 243 for (let i = data.length - 1; i >= 0; i--) {
michael@0 244 let sliceInfo = data[i];
michael@0 245 let sliceAngle = angles[i];
michael@0 246 if (!sliceInfo.size || sliceAngle < EPSILON) {
michael@0 247 continue;
michael@0 248 }
michael@0 249
michael@0 250 endAngle = startAngle - sliceAngle;
michael@0 251 midAngle = (startAngle + endAngle) / 2;
michael@0 252
michael@0 253 let x1 = centerX + radius * Math.sin(startAngle);
michael@0 254 let y1 = centerY - radius * Math.cos(startAngle);
michael@0 255 let x2 = centerX + radius * Math.sin(endAngle);
michael@0 256 let y2 = centerY - radius * Math.cos(endAngle);
michael@0 257 let largeArcFlag = Math.abs(startAngle - endAngle) > PI ? 1 : 0;
michael@0 258
michael@0 259 let pathNode = document.createElementNS(SVG_NS, "path");
michael@0 260 pathNode.setAttribute("class", "pie-chart-slice chart-colored-blob");
michael@0 261 pathNode.setAttribute("name", sliceInfo.label);
michael@0 262 pathNode.setAttribute("d",
michael@0 263 " M " + centerX + "," + centerY +
michael@0 264 " L " + x2 + "," + y2 +
michael@0 265 " A " + radius + "," + radius +
michael@0 266 " 0 " + largeArcFlag +
michael@0 267 " 1 " + x1 + "," + y1 +
michael@0 268 " Z");
michael@0 269
michael@0 270 if (sliceInfo == largest) {
michael@0 271 pathNode.setAttribute("largest", "");
michael@0 272 }
michael@0 273 if (sliceInfo == smallest) {
michael@0 274 pathNode.setAttribute("smallest", "");
michael@0 275 }
michael@0 276
michael@0 277 let hoverX = translateDistance * Math.sin(midAngle);
michael@0 278 let hoverY = -translateDistance * Math.cos(midAngle);
michael@0 279 let hoverTransform = "transform: translate(" + hoverX + "px, " + hoverY + "px)";
michael@0 280 pathNode.setAttribute("style", data.length > 1 ? hoverTransform : "");
michael@0 281
michael@0 282 proxy.slices.set(sliceInfo, pathNode);
michael@0 283 delegate(proxy, ["click", "mouseenter", "mouseleave"], pathNode, sliceInfo);
michael@0 284 container.appendChild(pathNode);
michael@0 285
michael@0 286 if (sliceInfo.label && sliceAngle > NAMED_SLICE_MIN_ANGLE) {
michael@0 287 let textX = centerX + textDistance * Math.sin(midAngle);
michael@0 288 let textY = centerY - textDistance * Math.cos(midAngle);
michael@0 289 let label = document.createElementNS(SVG_NS, "text");
michael@0 290 label.appendChild(document.createTextNode(sliceInfo.label));
michael@0 291 label.setAttribute("class", "pie-chart-label");
michael@0 292 label.setAttribute("style", data.length > 1 ? hoverTransform : "");
michael@0 293 label.setAttribute("x", data.length > 1 ? textX : centerX);
michael@0 294 label.setAttribute("y", data.length > 1 ? textY : centerY);
michael@0 295 container.appendChild(label);
michael@0 296 }
michael@0 297
michael@0 298 startAngle = endAngle;
michael@0 299 }
michael@0 300
michael@0 301 return proxy;
michael@0 302 }
michael@0 303
michael@0 304 /**
michael@0 305 * Creates the DOM for a table chart based on the specified properties.
michael@0 306 *
michael@0 307 * @param nsIDocument document
michael@0 308 * The document responsible with creating the DOM.
michael@0 309 * @param object
michael@0 310 * An object containing all or some of the following properties:
michael@0 311 * - title: a string displayed as the chart's (description)/local
michael@0 312 * - data: an array of items used to display each row; all the items
michael@0 313 * should be objects representing columns, for which the
michael@0 314 * properties' values will be displayed in each cell of a row.
michael@0 315 * e.g: [{
michael@0 316 * label1: 1,
michael@0 317 * label2: 3,
michael@0 318 * label3: "foo"
michael@0 319 * }, {
michael@0 320 * label1: 4,
michael@0 321 * label2: 6,
michael@0 322 * label3: "bar
michael@0 323 * }];
michael@0 324 * - strings: an object specifying for which rows in the `data` array
michael@0 325 * their cell values should be stringified and localized
michael@0 326 * based on a predicate function;
michael@0 327 * e.g: {
michael@0 328 * label1: value => l10n.getFormatStr("...", value)
michael@0 329 * }
michael@0 330 * - totals: an object specifying for which rows in the `data` array
michael@0 331 * the sum of their cells is to be displayed in the chart;
michael@0 332 * e.g: {
michael@0 333 * label1: total => l10n.getFormatStr("...", total), // 5
michael@0 334 * label2: total => l10n.getFormatStr("...", total), // 9
michael@0 335 * }
michael@0 336 * @return TableChart
michael@0 337 * A table chart proxy instance, which emits the following events:
michael@0 338 * - "mouseenter", when the mouse enters a row
michael@0 339 * - "mouseleave", when the mouse leaves a row
michael@0 340 * - "click", when the mouse clicks a row
michael@0 341 */
michael@0 342 function createTableChart(document, { title, data, strings, totals }) {
michael@0 343 strings = strings || {};
michael@0 344 totals = totals || {};
michael@0 345 let isPlaceholder = false;
michael@0 346
michael@0 347 // If there's no data available, display an empty placeholder.
michael@0 348 if (!data) {
michael@0 349 data = loadingTableChartData;
michael@0 350 isPlaceholder = true;
michael@0 351 }
michael@0 352 if (!data.length) {
michael@0 353 data = emptyTableChartData;
michael@0 354 isPlaceholder = true;
michael@0 355 }
michael@0 356
michael@0 357 let container = document.createElement("vbox");
michael@0 358 container.className = "generic-chart-container table-chart-container";
michael@0 359 container.setAttribute("pack", "center");
michael@0 360 container.setAttribute("flex", "1");
michael@0 361 container.setAttribute("rows", data.length);
michael@0 362 container.setAttribute("placeholder", isPlaceholder);
michael@0 363
michael@0 364 let proxy = new TableChart(container);
michael@0 365
michael@0 366 let titleNode = document.createElement("label");
michael@0 367 titleNode.className = "plain table-chart-title";
michael@0 368 titleNode.setAttribute("value", title);
michael@0 369 container.appendChild(titleNode);
michael@0 370
michael@0 371 let tableNode = document.createElement("vbox");
michael@0 372 tableNode.className = "plain table-chart-grid";
michael@0 373 container.appendChild(tableNode);
michael@0 374
michael@0 375 for (let rowInfo of data) {
michael@0 376 let rowNode = document.createElement("hbox");
michael@0 377 rowNode.className = "table-chart-row";
michael@0 378 rowNode.setAttribute("align", "center");
michael@0 379
michael@0 380 let boxNode = document.createElement("hbox");
michael@0 381 boxNode.className = "table-chart-row-box chart-colored-blob";
michael@0 382 boxNode.setAttribute("name", rowInfo.label);
michael@0 383 rowNode.appendChild(boxNode);
michael@0 384
michael@0 385 for (let [key, value] in Iterator(rowInfo)) {
michael@0 386 let index = data.indexOf(rowInfo);
michael@0 387 let stringified = strings[key] ? strings[key](value, index) : value;
michael@0 388 let labelNode = document.createElement("label");
michael@0 389 labelNode.className = "plain table-chart-row-label";
michael@0 390 labelNode.setAttribute("name", key);
michael@0 391 labelNode.setAttribute("value", stringified);
michael@0 392 rowNode.appendChild(labelNode);
michael@0 393 }
michael@0 394
michael@0 395 proxy.rows.set(rowInfo, rowNode);
michael@0 396 delegate(proxy, ["click", "mouseenter", "mouseleave"], rowNode, rowInfo);
michael@0 397 tableNode.appendChild(rowNode);
michael@0 398 }
michael@0 399
michael@0 400 let totalsNode = document.createElement("vbox");
michael@0 401 totalsNode.className = "table-chart-totals";
michael@0 402
michael@0 403 for (let [key, value] in Iterator(totals)) {
michael@0 404 let total = data.reduce((acc, e) => acc + e[key], 0);
michael@0 405 let stringified = totals[key] ? totals[key](total || 0) : total;
michael@0 406 let labelNode = document.createElement("label");
michael@0 407 labelNode.className = "plain table-chart-summary-label";
michael@0 408 labelNode.setAttribute("name", key);
michael@0 409 labelNode.setAttribute("value", stringified);
michael@0 410 totalsNode.appendChild(labelNode);
michael@0 411 }
michael@0 412
michael@0 413 container.appendChild(totalsNode);
michael@0 414
michael@0 415 return proxy;
michael@0 416 }
michael@0 417
michael@0 418 XPCOMUtils.defineLazyGetter(this, "loadingPieChartData", () => {
michael@0 419 return [{ size: 1, label: L10N.getStr("pieChart.loading") }];
michael@0 420 });
michael@0 421
michael@0 422 XPCOMUtils.defineLazyGetter(this, "emptyPieChartData", () => {
michael@0 423 return [{ size: 1, label: L10N.getStr("pieChart.unavailable") }];
michael@0 424 });
michael@0 425
michael@0 426 XPCOMUtils.defineLazyGetter(this, "loadingTableChartData", () => {
michael@0 427 return [{ size: "", label: L10N.getStr("tableChart.loading") }];
michael@0 428 });
michael@0 429
michael@0 430 XPCOMUtils.defineLazyGetter(this, "emptyTableChartData", () => {
michael@0 431 return [{ size: "", label: L10N.getStr("tableChart.unavailable") }];
michael@0 432 });
michael@0 433
michael@0 434 /**
michael@0 435 * Delegates DOM events emitted by an nsIDOMNode to an EventEmitter proxy.
michael@0 436 *
michael@0 437 * @param EventEmitter emitter
michael@0 438 * The event emitter proxy instance.
michael@0 439 * @param array events
michael@0 440 * An array of events, e.g. ["mouseenter", "mouseleave"].
michael@0 441 * @param nsIDOMNode node
michael@0 442 * The element firing the DOM events.
michael@0 443 * @param any args
michael@0 444 * The arguments passed when emitting events through the proxy.
michael@0 445 */
michael@0 446 function delegate(emitter, events, node, args) {
michael@0 447 for (let event of events) {
michael@0 448 node.addEventListener(event, emitter.emit.bind(emitter, event, args));
michael@0 449 }
michael@0 450 }

mercurial