chrome-devtools-frontend
Version:
Chrome DevTools UI
238 lines (214 loc) • 8.47 kB
text/typescript
// Copyright (c) 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/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`
<style>${pieChartStyles}</style>
<div class="root" role="group" =${this.onKeyDown} aria-label=${this.chartName}
jslog=${VisualLogging.pieChart().track({keydown: 'ArrowUp|ArrowDown'})}>
<div class="chart-root" style="width: ${this.size}px; height: ${this.size}px;">
${svg`
<svg>
<g transform="scale(${this.size / 2}) translate(1, 1) scale(0.99, 0.99)">
<circle r="1" stroke="hsl(0, 0%, 80%)" fill="transparent" stroke-width=${1 / this.size}></circle>
<circle r=${this.innerR} stroke="hsl(0, 0%, 80%)" fill="transparent" stroke-width=${1 / this.size}></circle>
${this.slices.map((slice, index) => {
const selected = this.sliceSelected === index;
const tabIndex = selected && !this.showLegend ? '0' : '-1';
return svg`<path class="slice ${selected ? 'selected' : ''}"
jslog=${VisualLogging.pieChartSlice().track({click: true})}
=${this.onSliceClicked(index)} tabIndex=${tabIndex}
fill=${slice.color} d=${this.getPathStringForSlice(slice)}
aria-label=${slice.title} id=${selected ? 'selectedSlice' : ''}></path>`;
})}
<!-- This is so that the selected slice is re-drawn on top, to avoid re-ordering slices
just to render them properly. -->
<use href="#selectedSlice" />
</g>
</svg>
`}
<div class="pie-chart-foreground">
<div class="pie-chart-total ${this.totalSelected ? 'selected' : ''}" =${this.selectTotal}
jslog=${VisualLogging.pieChartTotal('select-total').track({click: true})}
tabIndex=${this.totalSelected && !this.showLegend ? '1' : '-1'}>
${this.total ? this.formatter(this.total) : ''}
</div>
</div>
</div>
${this.showLegend ? html`
<div class="pie-chart-legend" jslog=${VisualLogging.section('legend')}>
${this.slices.map((slice, index) => {
const selected = this.sliceSelected === index;
return html`
<div class="pie-chart-legend-row ${selected ? 'selected' : ''}"
jslog=${VisualLogging.pieChartSlice().track({click: true})}
=${this.onSliceClicked(index)} tabIndex=${selected ? '0' : '-1'}>
<div class="pie-chart-size">${this.formatter(slice.value)}</div>
<div class="pie-chart-swatch" style="background-color: ${slice.color};"></div>
<div class="pie-chart-name">${slice.title}</div>
</div>`;
})}
<div class="pie-chart-legend-row ${this.totalSelected ? 'selected' : ''}"
jslog=${VisualLogging.pieChartTotal('select-total').track({click: true})}
=${this.selectTotal} tabIndex=${this.totalSelected ? '0' : '-1'}>
<div class="pie-chart-size">${this.formatter(this.total)}</div>
<div class="pie-chart-swatch"></div>
<div class="pie-chart-name">${i18nString(UIStrings.total)}</div>
</div>
</div>
` : ''}
</div>
`;
// 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<HTMLDivElement>('.pie-chart-legend > :last-child');
if (!totalLegendRow) {
return;
}
totalLegendRow.focus();
}
private selectAndFocusSlice(index: number): void {
this.selectSlice(index);
const sliceLegendRow = this.shadow.querySelector<HTMLDivElement>(`.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;
}
}