browser/devtools/shared/widgets/Chart.jsm

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

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

mercurial