github.com/web-platform-tests/wpt.fyi@v0.0.0-20240530210107-70cf978996f1/webapp/components/compat-2021.js (about)

     1  /**
     2   * Copyright 2021 The WPT Dashboard Project. All rights reserved.
     3   * Use of this source code is governed by a BSD-style license that can be
     4   * found in the LICENSE file.
     5   */
     6  
     7  import {load} from '../node_modules/@google-web-components/google-chart/google-chart-loader.js';
     8  import '../node_modules/@polymer/paper-button/paper-button.js';
     9  import '../node_modules/@polymer/paper-dialog/paper-dialog.js';
    10  import '../node_modules/@polymer/paper-input/paper-input.js';
    11  import '../node_modules/@polymer/polymer/lib/elements/dom-if.js';
    12  import { html, PolymerElement } from '../node_modules/@polymer/polymer/polymer-element.js';
    13  
    14  const GITHUB_URL_PREFIX = 'https://raw.githubusercontent.com/web-platform-tests/results-analysis';
    15  const DATA_BRANCH = 'gh-pages';
    16  // Support a 'use_webkitgtk' query parameter to substitute WebKitGTK in for
    17  // Safari, to deal with the ongoing lack of new STP versions on wpt.fyi.
    18  const DATA_FILES_PATH = (new URL(document.location)).searchParams.has('use_webkitgtk')
    19    ? 'data/compat2021/webkitgtk'
    20    : 'data/compat2021';
    21  
    22  const SUMMARY_FEATURE_NAME = 'summary';
    23  const FEATURES = [
    24    'aspect-ratio',
    25    'css-flexbox',
    26    'css-grid',
    27    'css-transforms',
    28    'position-sticky',
    29  ];
    30  
    31  // Compat2021DataManager encapsulates the loading of the CSV data that backs
    32  // both the summary scores and graphs shown on the Compat 2021 dashboard. It
    33  // fetches the CSV data, processes it into sets of datatables, and then caches
    34  // those tables for later use by the dashboard.
    35  class Compat2021DataManager {
    36    constructor() {
    37      this._dataLoaded = load().then(() => {
    38        return Promise.all([this._loadCsv('stable'), this._loadCsv('experimental')]);
    39      });
    40    }
    41  
    42    // Fetches the datatable for the given feature and stable/experimental state.
    43    // This will wait as needed for the underlying CSV data to be loaded and
    44    // processed before returning the datatable.
    45    async getDataTable(feature, stable) {
    46      await this._dataLoaded;
    47      return stable ?
    48        this.stableDatatables.get(feature) :
    49        this.experimentalDatatables.get(feature);
    50    }
    51  
    52    // Fetches a list of browser versions for stable or experimental. This is a
    53    // helper method for building tooltip actions; the returned list has one
    54    // entry per row in the corresponding datatables.
    55    async getBrowserVersions(stable) {
    56      await this._dataLoaded;
    57      return stable ?
    58        this.stableBrowserVersions :
    59        this.experimentalBrowserVersions;
    60    }
    61  
    62    // Loads the unified CSV file for either stable or experimental, and
    63    // processes it into the set of datatables provided by this class. Will
    64    // ultimately set either this.stableDatatables or this.experimentalDatatables
    65    // with a map of {feature name --> datatable}.
    66    async _loadCsv(label) {
    67      const url = `${GITHUB_URL_PREFIX}/${DATA_BRANCH}/${DATA_FILES_PATH}/unified-scores-${label}.csv`;
    68      const csvLines = await fetchCsvContents(url);
    69  
    70      const features = [SUMMARY_FEATURE_NAME, ...FEATURES];
    71      const dataTables = new Map(features.map(feature => {
    72        const dataTable = new window.google.visualization.DataTable();
    73        dataTable.addColumn('date', 'Date');
    74        dataTable.addColumn('number', 'Chrome/Edge');
    75        dataTable.addColumn({type: 'string', role: 'tooltip'});
    76        dataTable.addColumn('number', 'Firefox');
    77        dataTable.addColumn({type: 'string', role: 'tooltip'});
    78        dataTable.addColumn('number', 'Safari');
    79        dataTable.addColumn({type: 'string', role: 'tooltip'});
    80        return [feature, dataTable];
    81      }));
    82  
    83      // We list Chrome/Edge on the legend, but when creating the tooltip we
    84      // include the version information and so should be clear about which browser
    85      // exactly gave the results.
    86      const tooltipBrowserNames = [
    87        'Chrome',
    88        'Firefox',
    89        'Safari',
    90      ];
    91  
    92      // We store a lookup table of browser versions to help with the 'show
    93      // revision changelog' tooltip action.
    94      const browserVersions = [[], [], []];
    95  
    96      csvLines.forEach(line => {
    97        // We control the CSV data source, so are quite lazy with parsing it.
    98        //
    99        // The format is:
   100        //   date, [browser-version, browser-feature-a, browser-feature-b, ...]+
   101        const csvValues = line.split(',');
   102  
   103        // JavaScript Date objects use 0-indexed months whilst the CSV is
   104        // 1-indexed, so adjust for that.
   105        const dateParts = csvValues[0].split('-').map(x => parseInt(x));
   106        const date = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
   107  
   108        // Initialize a new row for each feature, with the date column set.
   109        const newRows = new Map(features.map(feature => {
   110          return [feature, [date]];
   111        }));
   112  
   113        // Now handle each of the browsers. For each there is a version column,
   114        // then the scores for each of the five features.
   115        for (let i = 1; i < csvValues.length; i += 6) {
   116          const browserIdx = Math.floor(i / 6);
   117          const browserName = tooltipBrowserNames[browserIdx];
   118          const version = csvValues[i];
   119          browserVersions[browserIdx].push(version);
   120  
   121          let summaryScore = 0;
   122          FEATURES.forEach((feature, j) => {
   123            const score = parseFloat(csvValues[i + 1 + j]);
   124            const tooltip = this.createTooltip(browserName, version, score.toFixed(3));
   125            newRows.get(feature).push(score);
   126            newRows.get(feature).push(tooltip);
   127  
   128            // The summary scores are calculated as a x/100 score, where each
   129            // feature is allowed to contribute up to 20 points. We use floor
   130            // rather than round to avoid claiming the full 20 points until we
   131            // are at 100%
   132            summaryScore += Math.floor(score * 20);
   133          });
   134  
   135          const summaryTooltip = this.createTooltip(browserName, version, summaryScore);
   136          newRows.get(SUMMARY_FEATURE_NAME).push(summaryScore);
   137          newRows.get(SUMMARY_FEATURE_NAME).push(summaryTooltip);
   138        }
   139  
   140        // Push the new rows onto the corresponding datatable.
   141        newRows.forEach((row, feature) => {
   142          dataTables.get(feature).addRow(row);
   143        });
   144      });
   145  
   146      // The datatables are now complete, so assign them to the appropriate
   147      // member variable.
   148      if (label === 'stable') {
   149        this.stableDatatables = dataTables;
   150        this.stableBrowserVersions = browserVersions;
   151      } else {
   152        this.experimentalDatatables = dataTables;
   153        this.experimentalBrowserVersions = browserVersions;
   154      }
   155    }
   156  
   157    createTooltip(browser, version, score) {
   158      return `${browser} ${version}: ${score}`;
   159    }
   160  }
   161  
   162  // Compat2021 is a custom element that holds the overall compat-2021 dashboard.
   163  // The dashboard breaks down into top-level summary scores, a small description,
   164  // graphs per feature, and a table of currently tracked tests.
   165  class Compat2021 extends PolymerElement {
   166    static get template() {
   167      return html`
   168        <style>
   169          :host {
   170            display: block;
   171            max-width: 700px;
   172            /* Override wpt.fyi's automatically injected common.css */
   173            margin: 0 auto !important;
   174            font-family: system-ui, sans-serif;
   175            line-height: 1.5;
   176          }
   177  
   178          h1 {
   179            text-align: center;
   180          }
   181  
   182          .channel-area {
   183            display: inline-flex;
   184            height: 35px;
   185            margin-top: 0;
   186            margin-bottom: 10px;
   187          }
   188  
   189          .channel-label {
   190            font-size: 18px;
   191            display: flex;
   192            justify-content: center;
   193            flex-direction: column;
   194          }
   195  
   196          .unselected {
   197            background-color: white;
   198          }
   199          .selected {
   200            background-color: var(--paper-blue-100);
   201          }
   202  
   203          .focus-area {
   204            font-size: 18px;
   205          }
   206  
   207          #featureSelect {
   208            padding: 0.5rem;
   209          }
   210  
   211          #testListText {
   212            padding-top: 1em;
   213          }
   214        </style>
   215        <h1>Compat 2021 Dashboard</h1>
   216        <compat-2021-summary stable="[[stable]]"></compat-2021-summary>
   217        <p>
   218          These scores represent how well browser engines are doing on the 2021
   219          Compat Focus Areas, as measured by wpt.fyi test results. Each feature
   220          contributes up to 20 points to the score, based on passing-test
   221          percentage, giving a maximum possible score of 100 for each browser.
   222        </p>
   223        <p>
   224          The set of tests used is derived from the full wpt.fyi test suite for
   225          each feature, filtered by believed importance to web developers.
   226          The results shown here are from
   227          <template is="dom-if" if="[[stable]]">
   228            released stable builds.
   229          </template>
   230          <template is="dom-if" if="[[!stable]]">
   231            developer preview builds with experimental features enabled.
   232          </template>
   233        </p>
   234  
   235        <fieldset>
   236          <legend>Configuration:</legend>
   237  
   238          <div class="channel-area">
   239            <span class="channel-label">Browser Type:</span>
   240            <paper-button class\$="[[experimentalButtonClass(stable)]]" raised on-click="clickExperimental">Experimental</paper-button>
   241            <paper-button class\$="[[stableButtonClass(stable)]]" raised on-click="clickStable">Stable</paper-button>
   242          </div>
   243  
   244          <!-- TODO: replace with paper-dropdown-menu -->
   245          <div class="focus-area">
   246            <label for="featureSelect">Focus area:</label>
   247            <select id="featureSelect">
   248              <option value="summary">Summary</option>
   249              <option value="aspect-ratio">aspect-ratio</option>
   250              <option value="css-flexbox">css-flexbox</option>
   251              <option value="css-grid">css-grid</option>
   252              <option value="css-transforms">css-transforms</option>
   253              <option value="position-sticky">position-sticky</option>
   254            </select>
   255          </div>
   256        </fieldset>
   257  
   258        <compat-2021-feature-chart data-manager="[[dataManager]]"
   259                                   stable="[[stable]]"
   260                                   feature="{{feature}}">
   261        </compat-2021-feature-chart>
   262  
   263        <!-- We use a 'hidden' style rather than dom-if to avoid layout shift when
   264             the feature is changed to/from summary. -->
   265        <div id="testListText" style$="visibility: [[getTestListTextVisibility(feature)]]">
   266          The score for this component is determined by pass rate on
   267          <a href="[[getTestListHref(feature)]]" target="_blank">this set of tests</a>.
   268          The test suite is never complete, and improvements are always welcome.
   269          Please contribute changes to
   270          <a href="https://github.com/web-platform-tests/wpt" target="_blank">WPT</a>
   271          and then
   272          <a href="https://github.com/web-platform-tests/wpt.fyi/issues/new?title=[compat2021]%20Add%20new%20tests%20to%20dashboard&body=" target="_blank">file an issue</a>
   273          to add them to the Compat 2021 effort!
   274        </div>
   275  
   276        <!-- TODO: Test results table -->
   277  `;
   278    }
   279  
   280    static get is() {
   281      return 'compat-2021';
   282    }
   283  
   284    static get properties() {
   285      return {
   286        embedded: Boolean,
   287        useWebkitGTK: Boolean,
   288        stable: Boolean,
   289        feature: String,
   290        dataManager: Object,
   291      };
   292    }
   293  
   294    static get observers() {
   295      return [
   296        'updateUrlParams(embedded, useWebKitGTK, stable, feature)',
   297      ];
   298    }
   299  
   300    ready() {
   301      super.ready();
   302  
   303      this.dataManager = new Compat2021DataManager();
   304  
   305      const params = (new URL(document.location)).searchParams;
   306      this.embedded = params.get('embedded') !== null;
   307      this.useWebKitGTK = params.get('use_webkitgtk') !== null;
   308      // The default view of the page is the summary scores graph for
   309      // experimental releases of browsers.
   310      this.stable = params.get('stable') !== null;
   311      this.feature = params.get('feature') || SUMMARY_FEATURE_NAME;
   312  
   313      this.$.featureSelect.value = this.feature;
   314      this.$.featureSelect.addEventListener('change', () => {
   315        this.feature = this.$.featureSelect.value;
   316      });
   317    }
   318  
   319    updateUrlParams(embedded, useWebKitGTK, stable, feature) {
   320      // Our observer may be called before the feature is set, so debounce that.
   321      if (feature === undefined) {
   322        return;
   323      }
   324  
   325      const params = [];
   326      if (feature) {
   327        params.push(`feature=${feature}`);
   328      }
   329      if (stable) {
   330        params.push('stable');
   331      }
   332      if (embedded) {
   333        params.push('embedded');
   334      }
   335      if (useWebKitGTK) {
   336        params.push('use_webkitgtk');
   337      }
   338  
   339      let url = location.pathname;
   340      if (params.length) {
   341        url += `?${params.join('&')}`;
   342      }
   343      history.pushState('', '', url);
   344    }
   345  
   346    experimentalButtonClass(stable) {
   347      return stable ? 'unselected' : 'selected';
   348    }
   349  
   350    stableButtonClass(stable) {
   351      return stable ? 'selected' : 'unselected';
   352    }
   353  
   354    clickExperimental() {
   355      if (!this.stable) {
   356        return;
   357      }
   358      this.stable = false;
   359    }
   360  
   361    clickStable() {
   362      if (this.stable) {
   363        return;
   364      }
   365      this.stable = true;
   366    }
   367  
   368    getTestListTextVisibility(feature) {
   369      return FEATURES.includes(feature) ? 'visible' : 'hidden';
   370    }
   371  
   372    getTestListHref(feature) {
   373      return `${GITHUB_URL_PREFIX}/main/compat-2021/${feature}-tests.txt`;
   374    }
   375  }
   376  window.customElements.define(Compat2021.is, Compat2021);
   377  
   378  const STABLE_TITLES = [
   379    'Chrome/Edge Stable',
   380    'Firefox Stable',
   381    'Safari Stable',
   382  ];
   383  
   384  const EXPERIMENTAL_TITLES = [
   385    'Chrome/Edge Dev',
   386    'Firefox Nightly',
   387    'Safari Preview',
   388  ];
   389  
   390  class Compat2021Summary extends PolymerElement {
   391    static get template() {
   392      return html`
   393        <link rel="preconnect" href="https://fonts.gstatic.com">
   394        <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400&display=swap" rel="stylesheet">
   395  
   396        <style>
   397          #summaryContainer {
   398            padding-top: 1em;
   399            display: flex;
   400            justify-content: center;
   401            gap: 30px;
   402          }
   403  
   404          .summary-flex-item {
   405            position: relative;
   406            width: 125px;
   407            cursor: help;
   408          }
   409  
   410          .summary-number {
   411            font-size: 5em;
   412            font-family: 'Roboto Mono', monospace;
   413            text-align: center;
   414          }
   415  
   416          .summary-browser-name {
   417            text-align: center;
   418          }
   419  
   420          .summary-flex-item:hover .summary-tooltip,
   421          .summary-flex-item:focus .summary-tooltip {
   422            display: block;
   423          }
   424  
   425          .summary-tooltip {
   426            display: none;
   427            position: absolute;
   428            /* TODO: find a better solution for drawing on-top of other numbers */
   429            z-index: 1;
   430            width: 150px;
   431            border: 1px lightgrey solid;
   432            background: white;
   433            border-radius: 3px;
   434            padding: 5px;
   435            top: 105%;
   436            left: -20%;
   437            padding: 0.5rem 0.75rem;
   438            line-height: 1.4;
   439            box-shadow: 0 0 20px 0px #c3c3c3;
   440          }
   441  
   442          .summary-tooltip > div {
   443            display: flex;
   444            justify-content: space-between;
   445          }
   446        </style>
   447  
   448        <div id="summaryContainer">
   449          <!-- Chrome/Edge -->
   450          <div class="summary-flex-item" tabindex="0">
   451            <span class="summary-tooltip"></span>
   452            <div class="summary-number">--</div>
   453            <div class="summary-browser-name"></div>
   454          </div>
   455          <!-- Firefox -->
   456          <div class="summary-flex-item" tabindex="0">
   457            <span class="summary-tooltip"></span>
   458            <div class="summary-number">--</div>
   459            <div class="summary-browser-name"></div>
   460          </div>
   461          <!-- Safari -->
   462          <div class="summary-flex-item" tabindex="0">
   463            <span class="summary-tooltip"></span>
   464            <div class="summary-number">--</div>
   465            <div class="summary-browser-name"></div>
   466          </div>
   467        </div>
   468  `;
   469    }
   470  
   471    static get is() {
   472      return 'compat-2021-summary';
   473    }
   474  
   475    static get properties() {
   476      return {
   477        stable: {
   478          type: Boolean,
   479          observer: '_stableChanged',
   480        }
   481      };
   482    }
   483  
   484    _stableChanged() {
   485      this.updateSummaryTitles();
   486      this.updateSummaryScores();
   487    }
   488  
   489    updateSummaryTitles() {
   490      let titleDivs = this.$.summaryContainer.querySelectorAll('.summary-browser-name');
   491      let titles = this.stable ? STABLE_TITLES : EXPERIMENTAL_TITLES;
   492      for (let i = 0; i < titleDivs.length; i++) {
   493        titleDivs[i].innerText = titles[i];
   494      }
   495    }
   496  
   497    async updateSummaryScores() {
   498      let scores = await this.calculateSummaryScores(this.stable);
   499      let numbers = this.$.summaryContainer.querySelectorAll('.summary-number');
   500      let tooltips = this.$.summaryContainer.querySelectorAll('.summary-tooltip');
   501      for (let i = 0; i < scores.length; i++) {
   502        numbers[i].innerText = scores[i].total;
   503        numbers[i].style.color = this.calculateColor(scores[i].total);
   504  
   505        // TODO: Replace tooltips with paper-tooltip.
   506        this.updateSummaryTooltip(tooltips[i], scores[i].breakdown);
   507      }
   508    }
   509  
   510    updateSummaryTooltip(tooltipDiv, scoreBreakdown) {
   511      tooltipDiv.innerHTML = '';
   512  
   513      scoreBreakdown.forEach((val, key) => {
   514        const keySpan = document.createElement('span');
   515        keySpan.innerText = `${key}: `;
   516        const valueSpan = document.createElement('span');
   517        valueSpan.innerText = val;
   518        valueSpan.style.color = this.calculateColor(val * 5);  // Scale to 0-100
   519  
   520        const textDiv = document.createElement('div');
   521        textDiv.appendChild(keySpan);
   522        textDiv.appendChild(valueSpan);
   523  
   524        tooltipDiv.appendChild(textDiv);
   525      });
   526    }
   527  
   528    async calculateSummaryScores(stable) {
   529      const label = stable ? 'stable' : 'experimental';
   530      const url = `${GITHUB_URL_PREFIX}/${DATA_BRANCH}/${DATA_FILES_PATH}/summary-${label}.csv`;
   531      const csvLines = await fetchCsvContents(url);
   532  
   533      if (csvLines.length !== 5) {
   534        throw new Error(`${url} did not contain 5 results`);
   535      }
   536  
   537      let scores = [
   538        { total: 0, breakdown: new Map() },
   539        { total: 0, breakdown: new Map() },
   540        { total: 0, breakdown: new Map() },
   541      ];
   542  
   543      for (const line of csvLines) {
   544        let parts = line.split(',');
   545        if (parts.length !== 4) {
   546          throw new Error(`${url} had an invalid line`);
   547        }
   548  
   549        const feature = parts.shift();
   550        for (let i = 0; i < parts.length; i++) {
   551          // Use floor rather than round to avoid claiming the full 20 points until
   552          // definitely there.
   553          let contribution = Math.floor(parseFloat(parts[i]) * 20);
   554          scores[i].total += contribution;
   555          scores[i].breakdown.set(feature, contribution);
   556        }
   557      }
   558  
   559      return scores;
   560    }
   561  
   562    // TODO: Reuse the code from wpt-colors.js
   563    calculateColor(score) {
   564      // RGB values from https://material.io/design/color/
   565      if (score >= 95) {
   566        return '#388E3C';  // Green 700
   567      }
   568      if (score >= 75) {
   569        return '#689F38';  // Light Green 700
   570      }
   571      if (score >= 50) {
   572        return '#FBC02D';  // Yellow 700
   573      }
   574      if (score >= 25) {
   575        return '#F57C00';  // Orange 700
   576      }
   577      return '#D32F2F'; // Red 700
   578    }
   579  }
   580  window.customElements.define(Compat2021Summary.is, Compat2021Summary);
   581  
   582  // Compat2021FeatureChart is a wrapper around a Google Charts chart. We cannot
   583  // use the polymer google-chart element as it does not support setting tooltip
   584  // actions, which we rely on to let users load a changelog between subsequent
   585  // versions of the same browser.
   586  class Compat2021FeatureChart extends PolymerElement {
   587    static get template() {
   588      return html`
   589        <style>
   590          .chart {
   591            /* Reserve vertical space to avoid layout shift. Should be kept in sync
   592               with the JavaScript defined height. */
   593            height: 350px;
   594            margin: 0 auto;
   595            display: flex;
   596            justify-content: center;
   597          }
   598  
   599          paper-dialog {
   600            max-width: 600px;
   601          }
   602        </style>
   603        <div id="failuresChart" class="chart"></div>
   604  
   605        <paper-dialog with-backdrop id="firefoxNightlyDialog">
   606          <h2>Firefox Nightly Changelogs</h2>
   607          <div>
   608            Nightly builds of Firefox are all given the same sub-version,
   609            <code>0a1</code>, so we cannot automatically determine the changelog.
   610            To find the changelog of a specific Nightly release, locate the
   611            corresponding revision on the
   612            <a href="https://hg.mozilla.org/mozilla-central/firefoxreleases"
   613               target="_blank">release page</a>, enter them below, and click "Go".
   614            <paper-input id="firefoxNightlyDialogFrom" label="From revision"></paper-input>
   615            <paper-input id="firefoxNightlyDialogTo" label="To revision"></paper-input>
   616          </div>
   617  
   618          <div class="buttons">
   619            <paper-button dialog-dismiss>Cancel</paper-button>
   620            <paper-button dialog-confirm on-click="clickFirefoxNightlyDialogGoButton">Go</paper-button>
   621          </div>
   622        </paper-dialog>
   623  
   624        <paper-dialog with-backdrop id="safariDialog">
   625          <h2>Safari Changelogs</h2>
   626          <template is="dom-if" if="[[stable]]">
   627            <div>
   628              Stable releases of Safari do not publish changelogs, but some insight
   629              may be gained from the
   630              <a href="https://developer.apple.com/documentation/safari-release-notes"
   631                 target="_blank">Release Notes</a>.
   632            </div>
   633          </template>
   634          <template is="dom-if" if="[[!stable]]">
   635            <div>
   636              For Safari Technology Preview releases, release notes can be found on
   637              the <a href="https://webkit.org/blog/" target="_blank">WebKit Blog</a>.
   638              Each post usually contains a revision changelog link - look for the
   639              text "This release covers WebKit revisions ...".
   640            </div>
   641          </template>
   642  
   643          <div class="buttons">
   644            <paper-button dialog-dismiss>Dismiss</paper-button>
   645          </div>
   646        </paper-dialog>
   647  `;
   648    }
   649  
   650    static get properties() {
   651      return {
   652        dataManager: Object,
   653        stable: Boolean,
   654        feature: String,
   655      };
   656    }
   657  
   658    static get observers() {
   659      return [
   660        'updateChart(feature, stable)',
   661      ];
   662    }
   663  
   664    static get is() {
   665      return 'compat-2021-feature-chart';
   666    }
   667  
   668    ready() {
   669      super.ready();
   670  
   671      // Google Charts is not responsive, even if one sets a percentage-width, so
   672      // we add a resize observer to redraw the chart if the size changes.
   673      window.addEventListener('resize', () => {
   674        this.updateChart(this.feature, this.stable);
   675      });
   676    }
   677  
   678    async updateChart(feature, stable) {
   679      // Our observer may be called before the feature is set, so debounce that.
   680      if (!feature) {
   681        return;
   682      }
   683  
   684      // Fetching the datatable first ensures that Google Charts has been loaded.
   685      const dataTable = await this.dataManager.getDataTable(feature, stable);
   686  
   687      const div = this.$.failuresChart;
   688      const chart = new window.google.visualization.LineChart(div);
   689  
   690      // We define a tooltip action that can quickly show users the changelog
   691      // between two subsequent versions of a browser. The goal is to help users
   692      // understand why an improvement or regression may have happened - though
   693      // this only exposes browser changes and not test suite changes.
   694      const browserVersions = await this.dataManager.getBrowserVersions(stable);
   695      chart.setAction({
   696        id: 'revisionChangelog',
   697        text: 'Show browser changelog',
   698        action: () => {
   699          let selection = chart.getSelection();
   700          let row = selection[0].row;
   701          let column = selection[0].column;
   702  
   703          // Map from the selected column to the browser index. In the datatable
   704          // Chrome is 1, Firefox is 3, Safari is 5 => these must map to [0, 1, 2].
   705          let browserIdx = Math.floor(column / 2);
   706  
   707          let version = browserVersions[browserIdx][row];
   708          let lastVersion = version;
   709          while (row > 0 && lastVersion === version) {
   710            row -= 1;
   711            lastVersion = browserVersions[browserIdx][row];
   712          }
   713          // TODO: If row == -1 here then we've failed.
   714  
   715          if (browserIdx === 0) {
   716            window.open(this.getChromeChangelogUrl(lastVersion, version));
   717            return;
   718          }
   719  
   720          if (browserIdx === 1) {
   721            if (stable) {
   722              window.open(this.getFirefoxStableChangelogUrl(lastVersion, version));
   723            } else {
   724              this.$.firefoxNightlyDialog.open();
   725            }
   726            return;
   727          }
   728  
   729          this.$.safariDialog.open();
   730        },
   731      });
   732  
   733      chart.draw(dataTable, this.getChartOptions(div, feature));
   734    }
   735  
   736    getChromeChangelogUrl(fromVersion, toVersion) {
   737      // Strip off the 'dev' suffix if there.
   738      fromVersion = fromVersion.split(' ')[0];
   739      toVersion = toVersion.split(' ')[0];
   740      return `https://chromium.googlesource.com/chromium/src/+log/${fromVersion}..${toVersion}?pretty=fuller&n=10000`;
   741    }
   742  
   743    getFirefoxStableChangelogUrl(fromVersion, toVersion) {
   744      // The version numbers are reported as XX.Y.Z, but pushlog wants
   745      // 'FIREFOX_XX_Y_Z_RELEASE'.
   746      const fromParts = fromVersion.split('.');
   747      const fromRelease = `FIREFOX_${fromParts.join('_')}_RELEASE`;
   748      const toParts = toVersion.split('.');
   749      const toRelease = `FIREFOX_${toParts.join('_')}_RELEASE`;
   750      return `https://hg.mozilla.org/mozilla-unified/pushloghtml?fromchange=${fromRelease}&tochange=${toRelease}`;
   751    }
   752  
   753    clickFirefoxNightlyDialogGoButton() {
   754      const fromSha = this.$.firefoxNightlyDialogFrom.value;
   755      const toSha = this.$.firefoxNightlyDialogTo.value;
   756      const url = `https://hg.mozilla.org/mozilla-unified/pushloghtml?fromchange=${fromSha}&tochange=${toSha}`;
   757      window.open(url);
   758    }
   759  
   760    getChartOptions(containerDiv, feature) {
   761      const options = {
   762        height: 350,
   763        fontSize: 14,
   764        tooltip: {
   765          trigger: 'both',
   766        },
   767        hAxis: {
   768          title: 'Date',
   769          format: 'MMM-YYYY',
   770        },
   771        explorer: {
   772          actions: ['dragToZoom', 'rightClickToReset'],
   773          axis: 'horizontal',
   774          keepInBounds: true,
   775          maxZoomIn: 4.0,
   776        },
   777        colors: ['#4285f4', '#ea4335', '#fbbc04'],
   778      };
   779  
   780      if (feature === SUMMARY_FEATURE_NAME) {
   781        options.vAxis = {
   782          title: 'Compat 2021 Score',
   783          viewWindow: {
   784            min: 50,
   785            max: 100,
   786          }
   787        };
   788      } else {
   789        options.vAxis = {
   790          title: 'Percentage of tests passing',
   791          format: 'percent',
   792          viewWindow: {
   793            // We set a global minimum value for the y-axis to keep the graphs
   794            // consistent when you switch features. Currently the lowest value
   795            // is aspect-ratio, with a ~25% pass-rate on Safari STP, Safari
   796            // Stable, and Firefox Stable.
   797            min: 0.2,
   798            max: 1,
   799          }
   800        };
   801      }
   802  
   803      // We draw the chart in two ways, depending on the viewport width. In
   804      // 'full' mode the legend is on the right and we limit the chart size to
   805      // 700px wide. In 'mobile' mode the legend is on the top and we use all the
   806      // space we can get for the chart.
   807      if (containerDiv.clientWidth >= 700) {
   808        options.width = 700;
   809        options.chartArea = {
   810          height: '80%'
   811        };
   812      } else {
   813        options.width = '100%';
   814        options.legend = {
   815          position: 'top',
   816          alignment: 'center',
   817        };
   818        options.chartArea = {
   819          left: 75,
   820          width: '80%',
   821        };
   822      }
   823  
   824      return options;
   825    }
   826  }
   827  window.customElements.define(Compat2021FeatureChart.is, Compat2021FeatureChart);
   828  
   829  async function fetchCsvContents(url) {
   830    const csvResp = await fetch(url);
   831    if (!csvResp.ok) {
   832      throw new Error(`Fetching chart csv data failed: ${csvResp.status}`);
   833    }
   834    const csvText = await csvResp.text();
   835    const csvLines = csvText.split('\n').filter(l => l);
   836    csvLines.shift();  // We don't need the CSV header.
   837    return csvLines;
   838  }