tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

Chart.js (17650B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 "use strict";
      5 
      6 const NET_STRINGS_URI = "devtools/client/locales/netmonitor.properties";
      7 const SVG_NS = "http://www.w3.org/2000/svg";
      8 const PI = Math.PI;
      9 const TAU = PI * 2;
     10 const EPSILON = 0.0000001;
     11 const NAMED_SLICE_MIN_ANGLE = TAU / 8;
     12 const NAMED_SLICE_TEXT_DISTANCE_RATIO = 1.9;
     13 const HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO = 20;
     14 
     15 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
     16 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
     17 const L10N = new LocalizationHelper(NET_STRINGS_URI);
     18 
     19 /**
     20 * A factory for creating charts.
     21 * Example usage: let myChart = Chart.Pie(document, { ... });
     22 */
     23 var Chart = {
     24  Pie: createPieChart,
     25  Table: createTableChart,
     26  PieTable: createPieTableChart,
     27 };
     28 
     29 /**
     30 * A simple pie chart proxy for the underlying view.
     31 * Each item in the `slices` property represents a [data, node] pair containing
     32 * the data used to create the slice and the Node displaying it.
     33 *
     34 * @param Node node
     35 *        The node representing the view for this chart.
     36 */
     37 function PieChart(node) {
     38  this.node = node;
     39  this.slices = new WeakMap();
     40  EventEmitter.decorate(this);
     41 }
     42 
     43 /**
     44 * A simple table chart proxy for the underlying view.
     45 * Each item in the `rows` property represents a [data, node] pair containing
     46 * the data used to create the row and the Node displaying it.
     47 *
     48 * @param Node node
     49 *        The node representing the view for this chart.
     50 */
     51 function TableChart(node) {
     52  this.node = node;
     53  this.rows = new WeakMap();
     54  EventEmitter.decorate(this);
     55 }
     56 
     57 /**
     58 * A simple pie+table chart proxy for the underlying view.
     59 *
     60 * @param Node node
     61 *        The node representing the view for this chart.
     62 * @param PieChart pie
     63 *        The pie chart proxy.
     64 * @param TableChart table
     65 *        The table chart proxy.
     66 */
     67 function PieTableChart(node, pie, table) {
     68  this.node = node;
     69  this.pie = pie;
     70  this.table = table;
     71  EventEmitter.decorate(this);
     72 }
     73 
     74 /**
     75 * Creates the DOM for a pie+table chart.
     76 *
     77 * @param Document document
     78 *        The document responsible with creating the DOM.
     79 * @param object
     80 *        An object containing all or some of the following properties:
     81 *          - title: a string displayed as the table chart's (description)/local
     82 *          - diameter: the diameter of the pie chart, in pixels
     83 *          - data: an array of items used to display each slice in the pie
     84 *                  and each row in the table;
     85 *                  @see `createPieChart` and `createTableChart` for details.
     86 *          - strings: @see `createTableChart` for details.
     87 *          - totals: @see `createTableChart` for details.
     88 *          - sorted: a flag specifying if the `data` should be sorted
     89 *                    ascending by `size`.
     90 * @return PieTableChart
     91 *         A pie+table chart proxy instance, which emits the following events:
     92 *           - "mouseover", when the mouse enters a slice or a row
     93 *           - "mouseout", when the mouse leaves a slice or a row
     94 *           - "click", when the mouse enters a slice or a row
     95 */
     96 function createPieTableChart(
     97  document,
     98  { title, diameter, data, strings, totals, sorted, header }
     99 ) {
    100  if (data && sorted) {
    101    data = data.slice().sort((a, b) => +(a.size < b.size));
    102  }
    103 
    104  const pie = Chart.Pie(document, {
    105    width: diameter,
    106    data,
    107  });
    108 
    109  const table = Chart.Table(document, {
    110    title,
    111    data,
    112    strings,
    113    totals,
    114    header,
    115  });
    116 
    117  const container = document.createElement("div");
    118  container.className = "pie-table-chart-container";
    119  container.appendChild(pie.node);
    120  container.appendChild(table.node);
    121 
    122  const proxy = new PieTableChart(container, pie, table);
    123 
    124  pie.on("click", item => {
    125    proxy.emit("click", item);
    126  });
    127 
    128  table.on("click", item => {
    129    proxy.emit("click", item);
    130  });
    131 
    132  pie.on("mouseover", item => {
    133    proxy.emit("mouseover", item);
    134    if (table.rows.has(item)) {
    135      table.rows.get(item).setAttribute("focused", "");
    136    }
    137  });
    138 
    139  pie.on("mouseout", item => {
    140    proxy.emit("mouseout", item);
    141    if (table.rows.has(item)) {
    142      table.rows.get(item).removeAttribute("focused");
    143    }
    144  });
    145 
    146  table.on("mouseover", item => {
    147    proxy.emit("mouseover", item);
    148    if (pie.slices.has(item)) {
    149      pie.slices.get(item).setAttribute("focused", "");
    150    }
    151  });
    152 
    153  table.on("mouseout", item => {
    154    proxy.emit("mouseout", item);
    155    if (pie.slices.has(item)) {
    156      pie.slices.get(item).removeAttribute("focused");
    157    }
    158  });
    159 
    160  return proxy;
    161 }
    162 
    163 /**
    164 * Creates the DOM for a pie chart based on the specified properties.
    165 *
    166 * @param Document document
    167 *        The document responsible with creating the DOM.
    168 * @param object
    169 *        An object containing all or some of the following properties:
    170 *          - data: an array of items used to display each slice; all the items
    171 *                  should be objects containing a `size` and a `label` property.
    172 *                  e.g: [{
    173 *                    size: 1,
    174 *                    label: "foo"
    175 *                  }, {
    176 *                    size: 2,
    177 *                    label: "bar"
    178 *                  }];
    179 *          - width: the width of the chart, in pixels
    180 *          - height: optional, the height of the chart, in pixels.
    181 *          - centerX: optional, the X-axis center of the chart, in pixels.
    182 *          - centerY: optional, the Y-axis center of the chart, in pixels.
    183 *          - radius: optional, the radius of the chart, in pixels.
    184 * @return PieChart
    185 *         A pie chart proxy instance, which emits the following events:
    186 *           - "mouseover", when the mouse enters a slice
    187 *           - "mouseout", when the mouse leaves a slice
    188 *           - "click", when the mouse clicks a slice
    189 */
    190 function createPieChart(
    191  document,
    192  { data, width, height, centerX, centerY, radius }
    193 ) {
    194  height = height || width;
    195  centerX = centerX || width / 2;
    196  centerY = centerY || height / 2;
    197  radius = radius || (width + height) / 4;
    198  let isPlaceholder = false;
    199 
    200  // If there's no data available, display an empty placeholder.
    201  if (!data) {
    202    data = loadingPieChartData();
    203    isPlaceholder = true;
    204  }
    205  if (!data.length) {
    206    data = emptyPieChartData();
    207    isPlaceholder = true;
    208  }
    209 
    210  const container = document.createElementNS(SVG_NS, "svg");
    211  container.setAttribute(
    212    "class",
    213    "generic-chart-container pie-chart-container"
    214  );
    215 
    216  container.setAttribute("width", width);
    217  container.setAttribute("height", height);
    218  container.setAttribute("viewBox", "0 0 " + width + " " + height);
    219  container.setAttribute("slices", data.length);
    220  container.setAttribute("placeholder", isPlaceholder);
    221  container.setAttribute("role", "group");
    222  container.setAttribute("aria-label", L10N.getStr("pieChart.ariaLabel"));
    223 
    224  const slicesGroup = document.createElementNS(SVG_NS, "g");
    225  slicesGroup.setAttribute("role", "list");
    226  container.append(slicesGroup);
    227 
    228  const proxy = new PieChart(container);
    229 
    230  const total = data.reduce((acc, e) => acc + e.size, 0);
    231  const angles = data.map(e => (e.size / total) * (TAU - EPSILON));
    232  const largest = data.reduce((a, b) => (a.size > b.size ? a : b));
    233  const smallest = data.reduce((a, b) => (a.size < b.size ? a : b));
    234 
    235  const textDistance = radius / NAMED_SLICE_TEXT_DISTANCE_RATIO;
    236  const translateDistance = radius / HOVERED_SLICE_TRANSLATE_DISTANCE_RATIO;
    237  let startAngle = TAU;
    238  let endAngle = 0;
    239  let midAngle = 0;
    240  radius -= translateDistance;
    241 
    242  for (let i = data.length - 1; i >= 0; i--) {
    243    const sliceInfo = data[i];
    244    const sliceAngle = angles[i];
    245 
    246    const sliceNode = document.createElementNS(SVG_NS, "g");
    247    sliceNode.setAttribute("role", "listitem");
    248    slicesGroup.append(sliceNode);
    249 
    250    const interactiveNodeId = `${sliceInfo.label}-slice`;
    251    const textNodeId = `${sliceInfo.label}-slice-label`;
    252 
    253    // The only way to make this keyboard accessible is to have a link
    254    const interactiveNode = document.createElementNS(SVG_NS, "a");
    255    interactiveNode.setAttribute("id", interactiveNodeId);
    256    interactiveNode.setAttribute("xlink:href", `#${interactiveNodeId}`);
    257    interactiveNode.setAttribute("tabindex", `0`);
    258    interactiveNode.setAttribute("role", `button`);
    259    interactiveNode.classList.add("pie-chart-slice-container");
    260    if (!isPlaceholder) {
    261      interactiveNode.setAttribute(
    262        "aria-label",
    263        L10N.getFormatStr(
    264          "pieChart.sliceAriaLabel",
    265          sliceInfo.label,
    266          new Intl.NumberFormat(undefined, {
    267            style: "unit",
    268            unit: "percent",
    269            maximumFractionDigits: 2,
    270          }).format((sliceInfo.size / total) * 100)
    271        )
    272      );
    273    }
    274 
    275    sliceNode.append(interactiveNode);
    276 
    277    endAngle = startAngle - sliceAngle;
    278    midAngle = (startAngle + endAngle) / 2;
    279 
    280    const x1 = centerX + radius * Math.sin(startAngle);
    281    const y1 = centerY - radius * Math.cos(startAngle);
    282    const x2 = centerX + radius * Math.sin(endAngle);
    283    const y2 = centerY - radius * Math.cos(endAngle);
    284    const largeArcFlag = Math.abs(startAngle - endAngle) > PI ? 1 : 0;
    285 
    286    const pathNode = document.createElementNS(SVG_NS, "path");
    287    pathNode.classList.add("pie-chart-slice");
    288    pathNode.setAttribute("data-statistic-name", sliceInfo.label);
    289    pathNode.setAttribute(
    290      "d",
    291      " M " +
    292        centerX +
    293        "," +
    294        centerY +
    295        " L " +
    296        x2 +
    297        "," +
    298        y2 +
    299        " A " +
    300        radius +
    301        "," +
    302        radius +
    303        " 0 " +
    304        largeArcFlag +
    305        " 1 " +
    306        x1 +
    307        "," +
    308        y1 +
    309        " Z"
    310    );
    311 
    312    if (sliceInfo == largest) {
    313      pathNode.setAttribute("largest", "");
    314    }
    315    if (sliceInfo == smallest) {
    316      pathNode.setAttribute("smallest", "");
    317    }
    318 
    319    const hoverX = translateDistance * Math.sin(midAngle);
    320    const hoverY = -translateDistance * Math.cos(midAngle);
    321    const hoverTranslate = "translate(" + hoverX + "px, " + hoverY + "px)";
    322    if (data.length > 1) {
    323      pathNode.style.transform = hoverTranslate;
    324    }
    325 
    326    proxy.slices.set(sliceInfo, pathNode);
    327    delegate(
    328      proxy,
    329      ["click", "mouseover", "mouseout", "focus"],
    330      interactiveNode,
    331      sliceInfo
    332    );
    333    interactiveNode.appendChild(pathNode);
    334 
    335    const textX = centerX + textDistance * Math.sin(midAngle);
    336    const textY = centerY - textDistance * Math.cos(midAngle);
    337 
    338    // Don't add the label if the slice isn't large enough so it doesn't look cramped.
    339    if (sliceAngle >= NAMED_SLICE_MIN_ANGLE) {
    340      const label = document.createElementNS(SVG_NS, "text");
    341      label.appendChild(document.createTextNode(sliceInfo.label));
    342      label.setAttribute("id", textNodeId);
    343      // A label is already set on `interactiveNode`, so hide this from the accessibility tree
    344      // to avoid duplicating text.
    345      label.setAttribute("aria-hidden", "true");
    346      label.setAttribute("class", "pie-chart-label");
    347      if (data.length > 1) {
    348        label.style.transform = hoverTranslate;
    349      }
    350      label.setAttribute("x", data.length > 1 ? textX : centerX);
    351      label.setAttribute("y", data.length > 1 ? textY : centerY);
    352      interactiveNode.append(label);
    353    }
    354 
    355    startAngle = endAngle;
    356  }
    357 
    358  return proxy;
    359 }
    360 
    361 /**
    362 * Creates the DOM for a table chart based on the specified properties.
    363 *
    364 * @param Document document
    365 *        The document responsible with creating the DOM.
    366 * @param object
    367 *        An object containing all or some of the following properties:
    368 *          - title: a string displayed as the chart's (description)/local
    369 *          - data: an array of items used to display each row; all the items
    370 *                  should be objects representing columns, for which the
    371 *                  properties' values will be displayed in each cell of a row.
    372 *                  e.g: [{
    373 *                    label1: 1,
    374 *                    label2: 3,
    375 *                    label3: "foo"
    376 *                  }, {
    377 *                    label1: 4,
    378 *                    label2: 6,
    379 *                    label3: "bar
    380 *                  }];
    381 *          - strings: an object specifying for which rows in the `data` array
    382 *                     their cell values should be stringified and localized
    383 *                     based on a predicate function;
    384 *                     e.g: {
    385 *                       label1: value => l10n.getFormatStr("...", value)
    386 *                     }
    387 *          - totals: an object specifying for which rows in the `data` array
    388 *                    the sum of their cells is to be displayed in the chart;
    389 *                    e.g: {
    390 *                      label1: total => l10n.getFormatStr("...", total),  // 5
    391 *                      label2: total => l10n.getFormatStr("...", total),  // 9
    392 *                    }
    393 *          - header: an object specifying strings to use for table column
    394 *                    headers
    395 *                    e.g. {
    396 *                      label1: l10n.getStr(...),
    397 *                      label2: l10n.getStr(...),
    398 *                    }
    399 * @return TableChart
    400 *         A table chart proxy instance, which emits the following events:
    401 *           - "mouseover", when the mouse enters a row
    402 *           - "mouseout", when the mouse leaves a row
    403 *           - "click", when the mouse clicks a row
    404 */
    405 function createTableChart(document, { title, data, strings, totals, header }) {
    406  strings = strings || {};
    407  totals = totals || {};
    408  header = header || {};
    409  let isPlaceholder = false;
    410 
    411  // If there's no data available, display an empty placeholder.
    412  if (!data) {
    413    data = loadingTableChartData();
    414    isPlaceholder = true;
    415  }
    416  if (!data.length) {
    417    data = emptyTableChartData();
    418    isPlaceholder = true;
    419  }
    420 
    421  const container = document.createElement("div");
    422  container.className = "generic-chart-container table-chart-container";
    423  container.setAttribute("placeholder", isPlaceholder);
    424 
    425  const proxy = new TableChart(container);
    426 
    427  const titleNode = document.createElement("span");
    428  titleNode.className = "table-chart-title";
    429  titleNode.textContent = title;
    430  container.appendChild(titleNode);
    431 
    432  const tableNode = document.createElement("table");
    433  tableNode.className = "table-chart-grid";
    434  container.appendChild(tableNode);
    435 
    436  const headerNode = document.createElement("thead");
    437  headerNode.className = "table-chart-row";
    438 
    439  const bodyNode = document.createElement("tbody");
    440 
    441  const headerBoxNode = document.createElement("tr");
    442  headerBoxNode.className = "table-chart-row-box";
    443  headerNode.appendChild(headerBoxNode);
    444 
    445  for (const [key, value] of Object.entries(header)) {
    446    const headerLabelNode = document.createElement("th");
    447    headerLabelNode.className = "table-chart-row-label";
    448    headerLabelNode.setAttribute("name", key);
    449    headerLabelNode.textContent = value;
    450    if (key == "count") {
    451      headerLabelNode.classList.add("offscreen");
    452    }
    453    headerBoxNode.appendChild(headerLabelNode);
    454  }
    455 
    456  tableNode.append(headerNode, bodyNode);
    457 
    458  for (const rowInfo of data) {
    459    const rowNode = document.createElement("tr");
    460    rowNode.className = "table-chart-row";
    461    rowNode.setAttribute("data-statistic-name", rowInfo.label);
    462 
    463    for (const [key, value] of Object.entries(rowInfo)) {
    464      // Don't render the "cached" column. We only have it in here so it can be displayed
    465      // in the `totals` section.
    466      if (key == "cached") {
    467        continue;
    468      }
    469      const index = data.indexOf(rowInfo);
    470      const stringified = strings[key] ? strings[key](value, index) : value;
    471      const labelNode = document.createElement("td");
    472      labelNode.className = "table-chart-row-label";
    473      labelNode.setAttribute("name", key);
    474      labelNode.textContent = stringified;
    475      rowNode.appendChild(labelNode);
    476    }
    477 
    478    proxy.rows.set(rowInfo, rowNode);
    479    delegate(proxy, ["click", "mouseover", "mouseout"], rowNode, rowInfo);
    480    bodyNode.appendChild(rowNode);
    481  }
    482 
    483  const totalsNode = document.createElement("div");
    484  totalsNode.className = "table-chart-totals";
    485 
    486  for (const [key, value] of Object.entries(totals)) {
    487    const total = data.reduce((acc, e) => acc + e[key], 0);
    488    const stringified = value ? value(total || 0) : total;
    489    const labelNode = document.createElement("span");
    490    labelNode.className = "table-chart-summary-label";
    491    labelNode.setAttribute("name", key);
    492    labelNode.textContent = stringified;
    493    totalsNode.appendChild(labelNode);
    494  }
    495 
    496  container.appendChild(totalsNode);
    497 
    498  return proxy;
    499 }
    500 
    501 function loadingPieChartData() {
    502  return [{ size: 1, label: L10N.getStr("pieChart.loading") }];
    503 }
    504 
    505 function emptyPieChartData() {
    506  return [{ size: 1, label: L10N.getStr("pieChart.unavailable") }];
    507 }
    508 
    509 function loadingTableChartData() {
    510  return [{ size: "", label: L10N.getStr("tableChart.loading") }];
    511 }
    512 
    513 function emptyTableChartData() {
    514  return [{ size: "", label: L10N.getStr("tableChart.unavailable") }];
    515 }
    516 
    517 /**
    518 * Delegates DOM events emitted by a Node to an EventEmitter proxy.
    519 *
    520 * @param EventEmitter emitter
    521 *        The event emitter proxy instance.
    522 * @param array events
    523 *        An array of events, e.g. ["mouseover", "mouseout"].
    524 * @param Node node
    525 *        The element firing the DOM events.
    526 * @param any args
    527 *        The arguments passed when emitting events through the proxy.
    528 */
    529 function delegate(emitter, events, node, args) {
    530  for (const event of events) {
    531    node.addEventListener(event, emitter.emit.bind(emitter, event, args));
    532  }
    533 }
    534 
    535 exports.Chart = Chart;