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 };