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

     1  /**
     2   * Copyright 2023 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 { CountUp } from '../node_modules/countup.js/dist/countUp.js';
     8  import '../node_modules/@polymer/polymer/lib/elements/dom-if.js';
     9  import { html, PolymerElement } from '../node_modules/@polymer/polymer/polymer-element.js';
    10  import {afterNextRender} from  '../node_modules/@polymer/polymer/lib/utils/render-status.js';
    11  
    12  class InteropSummary extends PolymerElement {
    13    static get template() {
    14      return html`
    15        <link rel="preconnect" href="https://fonts.gstatic.com">
    16        <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400&display=swap" rel="stylesheet">
    17  
    18        <style>
    19          #summaryNumberRow {
    20            display: flex;
    21            justify-content: center;
    22            gap: 30px;
    23            margin-bottom: 20px;
    24          }
    25  
    26          .summary-container {
    27            min-height: 470px;
    28          }
    29  
    30          .summary-number {
    31            font-size: 4.5em;
    32            width: 3ch;
    33            height: 3ch;
    34            padding: 10px;
    35            font-family: 'Roboto Mono', monospace;
    36            display: grid;
    37            place-content: center;
    38            aspect-ratio: 1;
    39            border-radius: 50%;
    40            margin-bottom: 10px;
    41            margin-left: auto;
    42            margin-right: auto;
    43          }
    44  
    45          .smaller-summary-number {
    46            font-size: 3.5em;
    47            width: 2.5ch;
    48            height: 2.5ch;
    49            padding: 8px;
    50          }
    51  
    52          .summary-browser-name {
    53            text-align: center;
    54            display: flex;
    55            place-content: center;
    56            justify-content: space-around;
    57            gap: 2ch;
    58          }
    59  
    60          .summary-title {
    61            margin: 10px 0;
    62            text-align: center;
    63            font-size: 1em;
    64          }
    65  
    66          .summary-browser-name > figure {
    67            margin: 0;
    68            flex: 1;
    69          }
    70  
    71          .summary-browser-name > figure > figcaption {
    72            line-height: 1.1;
    73          }
    74  
    75          .summary-browser-name[data-stable-browsers] > :not(.stable) {
    76            display: none;
    77          }
    78  
    79          .summary-browser-name:not([data-stable-browsers]) > .stable {
    80            display: none;
    81          }
    82        </style>
    83        <div class="summary-container">
    84          <div id="summaryNumberRow">
    85            <!-- Interop -->
    86            <div id="interopSummary" class="summary-flex-item" tabindex="0">
    87              <div class="summary-number score-number smaller-summary-number">--</div>
    88              <h3 class="summary-title">INTEROP</h3>
    89            </div>
    90            <!-- Investigations -->
    91            <div id="investigationSummary" class="summary-flex-item" tabindex="0">
    92              <div id="investigationNumber" class="summary-number smaller-summary-number">--</div>
    93              <h3 class="summary-title">INVESTIGATIONS</h3>
    94            </div>
    95          </div>
    96          <div id="summaryNumberRow">
    97            <template is="dom-repeat" items="{{getYearProp('browserInfo')}}" as="browserInfo">
    98              <div class="summary-flex-item" tabindex="0">
    99                <div class="summary-number score-number smaller-summary-number">--</div>
   100                <template is="dom-if" if="{{isChromeEdgeCombo(browserInfo)}}">
   101                  <!-- Chrome/Edge -->
   102                  <template is="dom-if" if="[[stable]]">
   103                    <div class="summary-browser-name">
   104                      <figure>
   105                        <img src="/static/chrome_64x64.png" width="36" alt="Chrome" />
   106                        <figcaption>Chrome</figcaption>
   107                      </figure>
   108                      <figure>
   109                        <img src="/static/edge_64x64.png" width="36" alt="Edge" />
   110                        <figcaption>Edge</figcaption>
   111                      </figure>
   112                    </div>
   113                  </template>
   114                  <template is="dom-if" if="[[!stable]]">
   115                    <div class="summary-browser-name">
   116                      <figure>
   117                        <img src="/static/chrome-dev_64x64.png" width="36" alt="Chrome Dev" />
   118                        <figcaption>Chrome<br>Dev</figcaption>
   119                      </figure>
   120                      <figure>
   121                        <img src="/static/edge-dev_64x64.png" width="36" alt="Edge Dev" />
   122                        <figcaption>Edge<br>Dev</figcaption>
   123                      </figure>
   124                    </div>
   125                  </template>
   126                </template>
   127                <template is="dom-if" if="{{!isChromeEdgeCombo(browserInfo)}}">
   128                  <div class="summary-browser-name">
   129                    <figure>
   130                      <img src="[[getBrowserIcon(browserInfo, stable)]]" width="36" alt="[[getBrowserIconName(browserInfo, stable)]]" />
   131                      <template is="dom-if" if="[[stable]]">
   132                        <figcaption>[[browserInfo.tableName]]</figcaption>
   133                      </template>
   134                      <template is="dom-if" if="[[!stable]]">
   135                        <figcaption>
   136                          <template is="dom-repeat" items="[[getBrowserNameParts(browserInfo)]]" as="namePart">
   137                            [[namePart]]<br>
   138                          </template>
   139                        </figcaption>
   140                      </template>
   141                    </figure>
   142                  </div>
   143                </template>
   144              </div>
   145            </template>
   146          </div>
   147        </div>
   148  `;
   149    }
   150  
   151    static get is() {
   152      return 'interop-summary';
   153    }
   154  
   155    static get properties() {
   156      return {
   157        year: String,
   158        dataManager: Object,
   159        scores: Object,
   160        stable: {
   161          type: Boolean,
   162          observer: '_stableChanged',
   163        }
   164      };
   165    }
   166  
   167    _stableChanged() {
   168      this.updateSummaryScores();
   169    }
   170  
   171    ready() {
   172      super.ready();
   173      // Hide the top summary numbers if there is no investigation value.
   174      if (!this.shouldDisplayInvestigationNumber()) {
   175        const investigationDiv = this.shadowRoot.querySelector('#investigationSummary');
   176        investigationDiv.style.display = 'none';
   177      }
   178  
   179      const summaryDiv = this.shadowRoot.querySelector('.summary-container');
   180      // Don't display the interop score for Interop 2021.
   181      if (this.year === '2021') {
   182        const interopDiv = this.shadowRoot.querySelector('#interopSummary');
   183        interopDiv.style.display = 'none';
   184        summaryDiv.style.minHeight = '275px';
   185      }
   186      if (this.year === '2024') {
   187        summaryDiv.style.minHeight = '350px';
   188      }
   189  
   190      // The summary score elements are given class names asynchronously,
   191      // so we have to wait until they've finished rendering to update them.
   192      afterNextRender(this, this.updateSummaryScores);
   193      afterNextRender(this, this.setSummaryNumberSizes);
   194    }
   195  
   196    shouldDisplayInvestigationNumber() {
   197      const scores = this.dataManager.getYearProp('investigationScores');
   198      return scores !== null && scores !== undefined;
   199    }
   200  
   201    // roundScore defines the rounding rules for the top-level scores.
   202    roundScore(score) {
   203      // Round down before interop 2024.
   204      if (parseInt(this.year) < 2024) {
   205        return Math.floor(score / 10);
   206      }
   207  
   208      const roundedScore = Math.round(score / 10);
   209      // A special case for 100.
   210      if (roundedScore === 100 && score < 1000) {
   211        return 99;
   212      }
   213      return roundedScore;
   214    }
   215  
   216    // Takes a summary number div and changes the value to match the score (with CountUp).
   217    updateSummaryScore(number, score) {
   218      score = this.roundScore(score);
   219      const curScore = number.innerText;
   220      new CountUp(number, score, {
   221        startVal: curScore === '--' ? 0 : curScore
   222      }).start();
   223      const colors = this.calculateColor(score);
   224      number.style.color = `color-mix(in lch, ${colors[0]} 70%, black)`;
   225      number.style.backgroundColor = colors[1];
   226    }
   227  
   228    async updateSummaryScores() {
   229      const scoreElements = this.shadowRoot.querySelectorAll('.score-number');
   230      const scores = this.stable ? this.scores.stable : this.scores.experimental;
   231      const summaryFeatureName = this.dataManager.getYearProp('summaryFeatureName');
   232      // If the elements have not rendered yet, don't update the scores.
   233      if (scoreElements.length !== scores.length) {
   234        return;
   235      }
   236      // Update interop summary number first.
   237      this.updateSummaryScore(scoreElements[0], scores[scores.length - 1][summaryFeatureName]);
   238      // Update the rest of the browser scores.
   239      for (let i = 1; i < scoreElements.length; i++) {
   240        this.updateSummaryScore(scoreElements[i], scores[i - 1][summaryFeatureName]);
   241      }
   242  
   243      // Update investigation summary separately.
   244      if (this.shouldDisplayInvestigationNumber()) {
   245        const investigationNumber = this.shadowRoot.querySelector('#investigationNumber');
   246        this.updateSummaryScore(
   247          investigationNumber, this.dataManager.getYearProp('investigationTotalScore'));
   248      }
   249    }
   250  
   251    // Sets the size of the summary number bubbles based on the number of browsers.
   252    setSummaryNumberSizes() {
   253      const numBrowsers = this.dataManager.getYearProp('numBrowsers');
   254      if (numBrowsers < 4) {
   255        const scoreElements = this.shadowRoot.querySelectorAll('.summary-number');
   256        scoreElements.forEach(scoreElement => scoreElement.classList.remove('smaller-summary-number'));
   257      }
   258    }
   259  
   260    getYearProp(prop) {
   261      return this.dataManager.getYearProp(prop);
   262    }
   263  
   264    // Checks if this section is displaying the Chrome/Edge combo together.
   265    isChromeEdgeCombo(browserInfo) {
   266      return browserInfo.tableName === 'Chrome/Edge';
   267    }
   268  
   269    getBrowserIcon(browserInfo, isStable) {
   270      const icon = (isStable) ? browserInfo.stableIcon : browserInfo.experimentalIcon;
   271      return `/static/${icon}_64x64.png`;
   272    }
   273  
   274    getBrowserIconName(browserInfo, isStable) {
   275      if (isStable) {
   276        return browserInfo.tableName;
   277      }
   278      return `${browserInfo.tableName} ${browserInfo.experimentalName}`;
   279    }
   280  
   281    // Returns the browser full names as a list of strings so we can
   282    // render them with breaks. e.g. ["Safari", "Technology", "Preview"]
   283    getBrowserNameParts(browserInfo) {
   284      return [browserInfo.tableName, ...browserInfo.experimentalName.split(' ')];
   285    }
   286  
   287    calculateColor(score) {
   288      const gradient = [
   289        // Red.
   290        { scale: 0, color: [250, 0, 0] },
   291        // Orange.
   292        { scale: 33.33, color: [250, 125, 0] },
   293        // Yellow.
   294        { scale: 66.67, color: [220, 220, 0] },
   295        // Green.
   296        { scale: 100, color: [0, 160, 0] },
   297      ];
   298  
   299      let color1, color2;
   300      for (let i = 1; i < gradient.length; i++) {
   301        if (score <= gradient[i].scale) {
   302          color1 = gradient[i - 1];
   303          color2 = gradient[i];
   304          break;
   305        }
   306      }
   307      const colorWeight = ((score - color1.scale) / (color2.scale - color1.scale));
   308      const color = [
   309        Math.round(color1.color[0] * (1 - colorWeight) + color2.color[0] * colorWeight),
   310        Math.round(color1.color[1] * (1 - colorWeight) + color2.color[1] * colorWeight),
   311        Math.round(color1.color[2] * (1 - colorWeight) + color2.color[2] * colorWeight),
   312      ];
   313  
   314      return [
   315        `rgb(${color[0]}, ${color[1]}, ${color[2]})`,
   316        `rgba(${color[0]}, ${color[1]}, ${color[2]}, 0.15)`,
   317      ];
   318    }
   319  }
   320  export { InteropSummary };