// Copyright 2020 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. /* eslint-disable @devtools/no-lit-render-outside-of-view */ import * as i18n from '../../../../core/i18n/i18n.js'; import {html, render, svg} from '../../../lit/lit.js'; import * as VisualLogging from '../../../visual_logging/visual_logging.js'; import pieChartStyles from './pieChart.css.js'; const UIStrings = { /** * @description Text for sum */ total: 'Total', } as const; const str_ = i18n.i18n.registerUIStrings('ui/legacy/components/perf_ui/PieChart.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export interface Slice { value: number; color: string; title: string; } export interface PieChartData { chartName: string; size: number; formatter: (value: number) => string; showLegend: boolean; total: number; slices: Slice[]; } /** * If the slices are not available when constructing the pie chart, set .data * immediately, with total=0 and slices=[], so that the chart is rendered with * the correct initial size. This avoids a layout shift when the slices are * later populated. **/ export class PieChart extends HTMLElement { private readonly shadow = this.attachShadow({mode: 'open'}); private chartName = ''; private size = 0; private formatter = (val: number): string => String(val); private showLegend = false; private total = 0; private slices: readonly Slice[] = []; private totalSelected = true; private sliceSelected = -1; private readonly innerR = 0.618; private lastAngle = -Math.PI / 2; set data(data: PieChartData) { this.chartName = data.chartName; this.size = data.size; this.formatter = data.formatter; this.showLegend = data.showLegend; this.total = data.total; this.slices = data.slices; this.render(); } private render(): void { this.lastAngle = -Math.PI / 2; // clang-format off const output = html`
${svg` ${this.slices.map((slice, index) => { const selected = this.sliceSelected === index; const tabIndex = selected && !this.showLegend ? '0' : '-1'; return svg``; })} `}
${this.total ? this.formatter(this.total) : ''}
${this.showLegend ? html`
${this.slices.map((slice, index) => { const selected = this.sliceSelected === index; return html`
${this.formatter(slice.value)}
${slice.title}
`; })}
${this.formatter(this.total)}
${i18nString(UIStrings.total)}
` : ''}
`; // clang-format on render(output, this.shadow, {host: this}); } private onSliceClicked(index: number): () => void { return (): void => { this.selectSlice(index); }; } private selectSlice(index: number): void { this.totalSelected = false; this.sliceSelected = index; this.render(); } private selectTotal(): void { this.totalSelected = true; this.sliceSelected = -1; this.render(); } private selectAndFocusTotal(): void { this.selectTotal(); // In order for the :focus-visible styles to work, we need to focus the // newly selected item. This is so that the outline is only shown for focus // caused by keyboard events and not all focus e.g. showing a focus ring // when we click on something is not necessary. The same goes for focusing // slices below. const totalLegendRow = this.shadow.querySelector('.pie-chart-legend > :last-child'); if (!totalLegendRow) { return; } totalLegendRow.focus(); } private selectAndFocusSlice(index: number): void { this.selectSlice(index); const sliceLegendRow = this.shadow.querySelector(`.pie-chart-legend > :nth-child(${index + 1})`); if (!sliceLegendRow) { return; } sliceLegendRow.focus(); } private focusNextElement(): void { if (this.totalSelected) { this.selectAndFocusSlice(0); } else if (this.sliceSelected === this.slices.length - 1) { this.selectAndFocusTotal(); } else { this.selectAndFocusSlice(this.sliceSelected + 1); } } private focusPreviousElement(): void { if (this.totalSelected) { this.selectAndFocusSlice(this.slices.length - 1); } else if (this.sliceSelected === 0) { this.selectAndFocusTotal(); } else { this.selectAndFocusSlice(this.sliceSelected - 1); } } private onKeyDown(event: KeyboardEvent): void { let handled = false; if (event.key === 'ArrowDown') { this.focusNextElement(); handled = true; } else if (event.key === 'ArrowUp') { this.focusPreviousElement(); handled = true; } if (handled) { event.stopImmediatePropagation(); event.preventDefault(); } } private getPathStringForSlice(slice: Slice): string|undefined { const value = slice.value; let sliceAngle = value / this.total * 2 * Math.PI; if (!isFinite(sliceAngle)) { return; } sliceAngle = Math.min(sliceAngle, 2 * Math.PI * 0.9999); const x1 = Math.cos(this.lastAngle); const y1 = Math.sin(this.lastAngle); this.lastAngle += sliceAngle; const x2 = Math.cos(this.lastAngle); const y2 = Math.sin(this.lastAngle); const r2 = this.innerR; const x3 = x2 * r2; const y3 = y2 * r2; const x4 = x1 * r2; const y4 = y1 * r2; const largeArc = sliceAngle > Math.PI ? 1 : 0; const pathString = `M${x1},${y1} A1,1,0,${largeArc},1,${x2},${y2} L${x3},${y3} A${r2},${r2},0,${largeArc},0,${x4},${y4} Z`; return pathString; } } customElements.define('devtools-perf-piechart', PieChart); declare global { interface HTMLElementTagNameMap { 'devtools-perf-piechart': PieChart; } }