123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468 |
- /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- "use strict";
- const Cu = Components.utils;
- const NET_STRINGS_URI = "devtools/client/locales/netmonitor.properties";
- const SVG_NS = "http://www.w3.org/2000/svg";
- const PI = Math.PI;
- const TAU = PI * 2;
- const EPSILON = 0.0000001;
- const NAMED_SLICE_MIN_ANGLE = TAU / 8;
- const NAMED_SLICE_TEXT_DISTANCE_RATIO = 1.9;
- const HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO = 20;
- const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
- const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
- const EventEmitter = require("devtools/shared/event-emitter");
- const { LocalizationHelper } = require("devtools/shared/l10n");
- this.EXPORTED_SYMBOLS = ["Chart"];
- /**
- * Localization convenience methods.
- */
- var L10N = new LocalizationHelper(NET_STRINGS_URI);
- /**
- * A factory for creating charts.
- * Example usage: let myChart = Chart.Pie(document, { ... });
- */
- var Chart = {
- Pie: createPieChart,
- Table: createTableChart,
- PieTable: createPieTableChart
- };
- /**
- * A simple pie chart proxy for the underlying view.
- * Each item in the `slices` property represents a [data, node] pair containing
- * the data used to create the slice and the nsIDOMNode displaying it.
- *
- * @param nsIDOMNode node
- * The node representing the view for this chart.
- */
- function PieChart(node) {
- this.node = node;
- this.slices = new WeakMap();
- EventEmitter.decorate(this);
- }
- /**
- * A simple table chart proxy for the underlying view.
- * Each item in the `rows` property represents a [data, node] pair containing
- * the data used to create the row and the nsIDOMNode displaying it.
- *
- * @param nsIDOMNode node
- * The node representing the view for this chart.
- */
- function TableChart(node) {
- this.node = node;
- this.rows = new WeakMap();
- EventEmitter.decorate(this);
- }
- /**
- * A simple pie+table chart proxy for the underlying view.
- *
- * @param nsIDOMNode node
- * The node representing the view for this chart.
- * @param PieChart pie
- * The pie chart proxy.
- * @param TableChart table
- * The table chart proxy.
- */
- function PieTableChart(node, pie, table) {
- this.node = node;
- this.pie = pie;
- this.table = table;
- EventEmitter.decorate(this);
- }
- /**
- * Creates the DOM for a pie+table chart.
- *
- * @param nsIDocument document
- * The document responsible with creating the DOM.
- * @param object
- * An object containing all or some of the following properties:
- * - title: a string displayed as the table chart's (description)/local
- * - diameter: the diameter of the pie chart, in pixels
- * - data: an array of items used to display each slice in the pie
- * and each row in the table;
- * @see `createPieChart` and `createTableChart` for details.
- * - strings: @see `createTableChart` for details.
- * - totals: @see `createTableChart` for details.
- * - sorted: a flag specifying if the `data` should be sorted
- * ascending by `size`.
- * @return PieTableChart
- * A pie+table chart proxy instance, which emits the following events:
- * - "mouseover", when the mouse enters a slice or a row
- * - "mouseout", when the mouse leaves a slice or a row
- * - "click", when the mouse enters a slice or a row
- */
- function createPieTableChart(document, { title, diameter, data, strings, totals, sorted, header }) {
- if (data && sorted) {
- data = data.slice().sort((a, b) => +(a.size < b.size));
- }
- let pie = Chart.Pie(document, {
- width: diameter,
- data: data
- });
- let table = Chart.Table(document, {
- title: title,
- data: data,
- strings: strings,
- totals: totals,
- header: header,
- });
- let container = document.createElement("hbox");
- container.className = "pie-table-chart-container";
- container.appendChild(pie.node);
- container.appendChild(table.node);
- let proxy = new PieTableChart(container, pie, table);
- pie.on("click", (event, item) => {
- proxy.emit(event, item);
- });
- table.on("click", (event, item) => {
- proxy.emit(event, item);
- });
- pie.on("mouseover", (event, item) => {
- proxy.emit(event, item);
- if (table.rows.has(item)) {
- table.rows.get(item).setAttribute("focused", "");
- }
- });
- pie.on("mouseout", (event, item) => {
- proxy.emit(event, item);
- if (table.rows.has(item)) {
- table.rows.get(item).removeAttribute("focused");
- }
- });
- table.on("mouseover", (event, item) => {
- proxy.emit(event, item);
- if (pie.slices.has(item)) {
- pie.slices.get(item).setAttribute("focused", "");
- }
- });
- table.on("mouseout", (event, item) => {
- proxy.emit(event, item);
- if (pie.slices.has(item)) {
- pie.slices.get(item).removeAttribute("focused");
- }
- });
- return proxy;
- }
- /**
- * Creates the DOM for a pie chart based on the specified properties.
- *
- * @param nsIDocument document
- * The document responsible with creating the DOM.
- * @param object
- * An object containing all or some of the following properties:
- * - data: an array of items used to display each slice; all the items
- * should be objects containing a `size` and a `label` property.
- * e.g: [{
- * size: 1,
- * label: "foo"
- * }, {
- * size: 2,
- * label: "bar"
- * }];
- * - width: the width of the chart, in pixels
- * - height: optional, the height of the chart, in pixels.
- * - centerX: optional, the X-axis center of the chart, in pixels.
- * - centerY: optional, the Y-axis center of the chart, in pixels.
- * - radius: optional, the radius of the chart, in pixels.
- * @return PieChart
- * A pie chart proxy instance, which emits the following events:
- * - "mouseover", when the mouse enters a slice
- * - "mouseout", when the mouse leaves a slice
- * - "click", when the mouse clicks a slice
- */
- function createPieChart(document, { data, width, height, centerX, centerY, radius }) {
- height = height || width;
- centerX = centerX || width / 2;
- centerY = centerY || height / 2;
- radius = radius || (width + height) / 4;
- let isPlaceholder = false;
- // Filter out very small sizes, as they'll just render invisible slices.
- data = data ? data.filter(e => e.size > EPSILON) : null;
- // If there's no data available, display an empty placeholder.
- if (!data) {
- data = loadingPieChartData;
- isPlaceholder = true;
- }
- if (!data.length) {
- data = emptyPieChartData;
- isPlaceholder = true;
- }
- let container = document.createElementNS(SVG_NS, "svg");
- container.setAttribute("class", "generic-chart-container pie-chart-container");
- container.setAttribute("pack", "center");
- container.setAttribute("flex", "1");
- container.setAttribute("width", width);
- container.setAttribute("height", height);
- container.setAttribute("viewBox", "0 0 " + width + " " + height);
- container.setAttribute("slices", data.length);
- container.setAttribute("placeholder", isPlaceholder);
- let proxy = new PieChart(container);
- let total = data.reduce((acc, e) => acc + e.size, 0);
- let angles = data.map(e => e.size / total * (TAU - EPSILON));
- let largest = data.reduce((a, b) => a.size > b.size ? a : b);
- let smallest = data.reduce((a, b) => a.size < b.size ? a : b);
- let textDistance = radius / NAMED_SLICE_TEXT_DISTANCE_RATIO;
- let translateDistance = radius / HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO;
- let startAngle = TAU;
- let endAngle = 0;
- let midAngle = 0;
- radius -= translateDistance;
- for (let i = data.length - 1; i >= 0; i--) {
- let sliceInfo = data[i];
- let sliceAngle = angles[i];
- if (!sliceInfo.size || sliceAngle < EPSILON) {
- continue;
- }
- endAngle = startAngle - sliceAngle;
- midAngle = (startAngle + endAngle) / 2;
- let x1 = centerX + radius * Math.sin(startAngle);
- let y1 = centerY - radius * Math.cos(startAngle);
- let x2 = centerX + radius * Math.sin(endAngle);
- let y2 = centerY - radius * Math.cos(endAngle);
- let largeArcFlag = Math.abs(startAngle - endAngle) > PI ? 1 : 0;
- let pathNode = document.createElementNS(SVG_NS, "path");
- pathNode.setAttribute("class", "pie-chart-slice chart-colored-blob");
- pathNode.setAttribute("name", sliceInfo.label);
- pathNode.setAttribute("d",
- " M " + centerX + "," + centerY +
- " L " + x2 + "," + y2 +
- " A " + radius + "," + radius +
- " 0 " + largeArcFlag +
- " 1 " + x1 + "," + y1 +
- " Z");
- if (sliceInfo == largest) {
- pathNode.setAttribute("largest", "");
- }
- if (sliceInfo == smallest) {
- pathNode.setAttribute("smallest", "");
- }
- let hoverX = translateDistance * Math.sin(midAngle);
- let hoverY = -translateDistance * Math.cos(midAngle);
- let hoverTransform = "transform: translate(" + hoverX + "px, " + hoverY + "px)";
- pathNode.setAttribute("style", data.length > 1 ? hoverTransform : "");
- proxy.slices.set(sliceInfo, pathNode);
- delegate(proxy, ["click", "mouseover", "mouseout"], pathNode, sliceInfo);
- container.appendChild(pathNode);
- if (sliceInfo.label && sliceAngle > NAMED_SLICE_MIN_ANGLE) {
- let textX = centerX + textDistance * Math.sin(midAngle);
- let textY = centerY - textDistance * Math.cos(midAngle);
- let label = document.createElementNS(SVG_NS, "text");
- label.appendChild(document.createTextNode(sliceInfo.label));
- label.setAttribute("class", "pie-chart-label");
- label.setAttribute("style", data.length > 1 ? hoverTransform : "");
- label.setAttribute("x", data.length > 1 ? textX : centerX);
- label.setAttribute("y", data.length > 1 ? textY : centerY);
- container.appendChild(label);
- }
- startAngle = endAngle;
- }
- return proxy;
- }
- /**
- * Creates the DOM for a table chart based on the specified properties.
- *
- * @param nsIDocument document
- * The document responsible with creating the DOM.
- * @param object
- * An object containing all or some of the following properties:
- * - title: a string displayed as the chart's (description)/local
- * - data: an array of items used to display each row; all the items
- * should be objects representing columns, for which the
- * properties' values will be displayed in each cell of a row.
- * e.g: [{
- * label1: 1,
- * label2: 3,
- * label3: "foo"
- * }, {
- * label1: 4,
- * label2: 6,
- * label3: "bar
- * }];
- * - strings: an object specifying for which rows in the `data` array
- * their cell values should be stringified and localized
- * based on a predicate function;
- * e.g: {
- * label1: value => l10n.getFormatStr("...", value)
- * }
- * - totals: an object specifying for which rows in the `data` array
- * the sum of their cells is to be displayed in the chart;
- * e.g: {
- * label1: total => l10n.getFormatStr("...", total), // 5
- * label2: total => l10n.getFormatStr("...", total), // 9
- * }
- * @return TableChart
- * A table chart proxy instance, which emits the following events:
- * - "mouseover", when the mouse enters a row
- * - "mouseout", when the mouse leaves a row
- * - "click", when the mouse clicks a row
- */
- function createTableChart(document, { title, data, strings, totals, header }) {
- strings = strings || {};
- totals = totals || {};
- let isPlaceholder = false;
- // If there's no data available, display an empty placeholder.
- if (!data) {
- data = loadingTableChartData;
- isPlaceholder = true;
- }
- if (!data.length) {
- data = emptyTableChartData;
- isPlaceholder = true;
- }
- let container = document.createElement("vbox");
- container.className = "generic-chart-container table-chart-container";
- container.setAttribute("pack", "center");
- container.setAttribute("flex", "1");
- container.setAttribute("rows", data.length);
- container.setAttribute("placeholder", isPlaceholder);
- let proxy = new TableChart(container);
- let titleNode = document.createElement("label");
- titleNode.className = "plain table-chart-title";
- titleNode.setAttribute("value", title);
- container.appendChild(titleNode);
- let tableNode = document.createElement("vbox");
- tableNode.className = "plain table-chart-grid";
- container.appendChild(tableNode);
- const headerNode = document.createElement("div");
- headerNode.className = "table-chart-row";
- const headerBoxNode = document.createElement("div");
- headerBoxNode.className = "table-chart-row-box";
- headerNode.appendChild(headerBoxNode);
- for (let [key, value] of Object.entries(header)) {
- let headerLabelNode = document.createElement("span");
- headerLabelNode.className = "plain table-chart-row-label";
- headerLabelNode.setAttribute("name", key);
- headerLabelNode.textContent = value;
- headerNode.appendChild(headerLabelNode);
- }
- tableNode.appendChild(headerNode);
- for (let rowInfo of data) {
- let rowNode = document.createElement("hbox");
- rowNode.className = "table-chart-row";
- rowNode.setAttribute("align", "center");
- let boxNode = document.createElement("hbox");
- boxNode.className = "table-chart-row-box chart-colored-blob";
- boxNode.setAttribute("name", rowInfo.label);
- rowNode.appendChild(boxNode);
- for (let [key, value] of Object.entries(rowInfo)) {
- let index = data.indexOf(rowInfo);
- let stringified = strings[key] ? strings[key](value, index) : value;
- let labelNode = document.createElement("label");
- labelNode.className = "plain table-chart-row-label";
- labelNode.setAttribute("name", key);
- labelNode.setAttribute("value", stringified);
- rowNode.appendChild(labelNode);
- }
- proxy.rows.set(rowInfo, rowNode);
- delegate(proxy, ["click", "mouseover", "mouseout"], rowNode, rowInfo);
- tableNode.appendChild(rowNode);
- }
- let totalsNode = document.createElement("vbox");
- totalsNode.className = "table-chart-totals";
- for (let [key, value] of Object.entries(totals)) {
- let total = data.reduce((acc, e) => acc + e[key], 0);
- let stringified = totals[key] ? totals[key](total || 0) : total;
- let labelNode = document.createElement("label");
- labelNode.className = "plain table-chart-summary-label";
- labelNode.setAttribute("name", key);
- labelNode.setAttribute("value", stringified);
- totalsNode.appendChild(labelNode);
- }
- container.appendChild(totalsNode);
- return proxy;
- }
- XPCOMUtils.defineLazyGetter(this, "loadingPieChartData", () => {
- return [{ size: 1, label: L10N.getStr("pieChart.loading") }];
- });
- XPCOMUtils.defineLazyGetter(this, "emptyPieChartData", () => {
- return [{ size: 1, label: L10N.getStr("pieChart.unavailable") }];
- });
- XPCOMUtils.defineLazyGetter(this, "loadingTableChartData", () => {
- return [{ size: "", label: L10N.getStr("tableChart.loading") }];
- });
- XPCOMUtils.defineLazyGetter(this, "emptyTableChartData", () => {
- return [{ size: "", label: L10N.getStr("tableChart.unavailable") }];
- });
- /**
- * Delegates DOM events emitted by an nsIDOMNode to an EventEmitter proxy.
- *
- * @param EventEmitter emitter
- * The event emitter proxy instance.
- * @param array events
- * An array of events, e.g. ["mouseover", "mouseout"].
- * @param nsIDOMNode node
- * The element firing the DOM events.
- * @param any args
- * The arguments passed when emitting events through the proxy.
- */
- function delegate(emitter, events, node, args) {
- for (let event of events) {
- node.addEventListener(event, emitter.emit.bind(emitter, event, args));
- }
- }
|