8057caf72a
- Avoid redrawing property-link tables if the contents don't change - Don't update timeline legends if the selection doesn't change - Use shorter class names for the flamechart for faster parsing - Round positions in flamechart to avoid long strings that would be created from raw double positions - Don't redraw the tooltip if the content is the same Change-Id: I925f1708400286c7c9f8db62f75c3b5fe8a16b12 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3521945 Reviewed-by: Patrick Thier <pthier@chromium.org> Commit-Queue: Camillo Bruni <cbruni@chromium.org> Cr-Commit-Position: refs/heads/main@{#79540}
631 lines
19 KiB
JavaScript
631 lines
19 KiB
JavaScript
// Copyright 2020 the V8 project authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import {delay} from '../../helper.mjs';
|
|
import {kChunkHeight, kChunkVisualWidth, kChunkWidth} from '../../log/map.mjs';
|
|
import {SelectionEvent, SelectTimeEvent, SynchronizeSelectionEvent, ToolTipEvent,} from '../events.mjs';
|
|
import {CSSColor, DOM, formatDurationMicros, SVG, V8CustomElement} from '../helper.mjs';
|
|
|
|
export const kTimelineHeight = 200;
|
|
|
|
export class TimelineTrackBase extends V8CustomElement {
|
|
_timeline;
|
|
_nofChunks = 500;
|
|
_chunks = [];
|
|
_selectedEntry;
|
|
_focusedEntry;
|
|
_timeToPixel;
|
|
_timeStartPixelOffset;
|
|
_legend;
|
|
_lastContentWidth = 0;
|
|
|
|
_cachedTimelineBoundingClientRect;
|
|
_cachedTimelineScrollLeft;
|
|
|
|
constructor(templateText) {
|
|
super(templateText);
|
|
this._selectionHandler = new SelectionHandler(this);
|
|
this._legend = new Legend(this.$('#legendTable'));
|
|
|
|
this.timelineChunks = this.$('#timelineChunks');
|
|
this.timelineSamples = this.$('#timelineSamples');
|
|
this.timelineNode = this.$('#timeline');
|
|
this.toolTipTargetNode = this.$('#toolTipTarget');
|
|
this.hitPanelNode = this.$('#hitPanel');
|
|
this.timelineAnnotationsNode = this.$('#timelineAnnotations');
|
|
this.timelineMarkersNode = this.$('#timelineMarkers');
|
|
this._scalableContentNode = this.$('#scalableContent');
|
|
this.isLocked = false;
|
|
}
|
|
|
|
_initEventListeners() {
|
|
this._legend.onFilter = (type) => this._handleFilterTimeline();
|
|
this.timelineNode.addEventListener(
|
|
'scroll', e => this._handleTimelineScroll(e));
|
|
this.hitPanelNode.onclick = this._handleClick.bind(this);
|
|
this.hitPanelNode.ondblclick = this._handleDoubleClick.bind(this);
|
|
this.hitPanelNode.onmousemove = this._handleMouseMove.bind(this);
|
|
window.addEventListener('resize', () => this._resetCachedDimensions());
|
|
}
|
|
|
|
static get observedAttributes() {
|
|
return ['title'];
|
|
}
|
|
|
|
attributeChangedCallback(name, oldValue, newValue) {
|
|
if (name == 'title') {
|
|
this.$('#title').innerHTML = newValue;
|
|
}
|
|
}
|
|
|
|
_handleFilterTimeline(type) {
|
|
this._updateChunks();
|
|
}
|
|
|
|
set data(timeline) {
|
|
console.assert(timeline);
|
|
if (!this._timeline) this._initEventListeners();
|
|
this._timeline = timeline;
|
|
this._legend.timeline = timeline;
|
|
this.$('.content').style.display = timeline.isEmpty() ? 'none' : 'relative';
|
|
this._updateChunks();
|
|
}
|
|
|
|
set timeSelection(selection) {
|
|
this._selectionHandler.timeSelection = selection;
|
|
this.updateSelection();
|
|
}
|
|
|
|
updateSelection() {
|
|
this._selectionHandler.update();
|
|
this._legend.update();
|
|
}
|
|
|
|
get _timelineBoundingClientRect() {
|
|
if (this._cachedTimelineBoundingClientRect === undefined) {
|
|
this._cachedTimelineBoundingClientRect =
|
|
this.timelineNode.getBoundingClientRect();
|
|
}
|
|
return this._cachedTimelineBoundingClientRect;
|
|
}
|
|
|
|
get _timelineScrollLeft() {
|
|
if (this._cachedTimelineScrollLeft === undefined) {
|
|
this._cachedTimelineScrollLeft = this.timelineNode.scrollLeft;
|
|
}
|
|
return this._cachedTimelineScrollLeft;
|
|
}
|
|
|
|
_resetCachedDimensions() {
|
|
this._cachedTimelineBoundingClientRect = undefined;
|
|
this._cachedTimelineScrollLeft = undefined;
|
|
}
|
|
|
|
// Maps the clicked x position to the x position on timeline
|
|
positionOnTimeline(pagePosX) {
|
|
let rect = this._timelineBoundingClientRect;
|
|
let posClickedX = pagePosX - rect.left + this._timelineScrollLeft;
|
|
return posClickedX;
|
|
}
|
|
|
|
positionToTime(pagePosX) {
|
|
return this.relativePositionToTime(this.positionOnTimeline(pagePosX));
|
|
}
|
|
|
|
relativePositionToTime(timelineRelativeX) {
|
|
const timelineAbsoluteX = timelineRelativeX + this._timeStartPixelOffset;
|
|
return (timelineAbsoluteX / this._timeToPixel) | 0;
|
|
}
|
|
|
|
timeToPosition(time) {
|
|
let relativePosX = time * this._timeToPixel;
|
|
relativePosX -= this._timeStartPixelOffset;
|
|
return relativePosX;
|
|
}
|
|
|
|
set nofChunks(count) {
|
|
this._nofChunks = count | 0;
|
|
this._updateChunks();
|
|
}
|
|
|
|
get nofChunks() {
|
|
return this._nofChunks;
|
|
}
|
|
|
|
_updateChunks() {
|
|
this._chunks = undefined;
|
|
this._updateDimensions();
|
|
this.requestUpdate();
|
|
}
|
|
|
|
get chunks() {
|
|
if (this._chunks?.length != this.nofChunks) {
|
|
this._chunks =
|
|
this._timeline.chunks(this.nofChunks, this._legend.filterPredicate);
|
|
console.assert(this._chunks.length == this._nofChunks);
|
|
}
|
|
return this._chunks;
|
|
}
|
|
|
|
set selectedEntry(value) {
|
|
this._selectedEntry = value;
|
|
this.drawAnnotations(value);
|
|
}
|
|
|
|
get selectedEntry() {
|
|
return this._selectedEntry;
|
|
}
|
|
|
|
set scrollLeft(offset) {
|
|
this.timelineNode.scrollLeft = offset;
|
|
this._cachedTimelineScrollLeft = offset;
|
|
}
|
|
|
|
handleEntryTypeDoubleClick(e) {
|
|
this.dispatchEvent(new SelectionEvent(e.target.parentNode.entries));
|
|
}
|
|
|
|
timelineIndicatorMove(offset) {
|
|
this.timelineNode.scrollLeft += offset;
|
|
this._cachedTimelineScrollLeft = undefined;
|
|
}
|
|
|
|
_handleTimelineScroll(e) {
|
|
let scrollLeft = e.currentTarget.scrollLeft;
|
|
this._cachedTimelineScrollLeft = scrollLeft;
|
|
this.dispatchEvent(new CustomEvent(
|
|
'scrolltrack', {bubbles: true, composed: true, detail: scrollLeft}));
|
|
}
|
|
|
|
_updateDimensions() {
|
|
// No data in this timeline, no need to resize
|
|
if (!this._timeline) return;
|
|
|
|
const centerOffset = this._timelineBoundingClientRect.width / 2;
|
|
const time =
|
|
this.relativePositionToTime(this._timelineScrollLeft + centerOffset);
|
|
const start = this._timeline.startTime;
|
|
const width = this._nofChunks * kChunkWidth;
|
|
this._lastContentWidth = parseInt(this.timelineMarkersNode.style.width);
|
|
this._timeToPixel = width / this._timeline.duration();
|
|
this._timeStartPixelOffset = start * this._timeToPixel;
|
|
this.timelineChunks.style.width = `${width}px`;
|
|
this.timelineMarkersNode.style.width = `${width}px`;
|
|
this.timelineAnnotationsNode.style.width = `${width}px`;
|
|
this.hitPanelNode.style.width = `${width}px`;
|
|
this._drawMarkers();
|
|
this._selectionHandler.update();
|
|
this._scaleContent(width);
|
|
this._cachedTimelineScrollLeft = this.timelineNode.scrollLeft =
|
|
this.timeToPosition(time) - centerOffset;
|
|
}
|
|
|
|
_scaleContent(currentWidth) {
|
|
if (!this._lastContentWidth) return;
|
|
const ratio = currentWidth / this._lastContentWidth;
|
|
this._scalableContentNode.style.transform = `scale(${ratio}, 1)`;
|
|
}
|
|
|
|
_adjustHeight(height) {
|
|
const dataHeight = Math.max(height, 200);
|
|
const viewHeight = Math.min(dataHeight, 400);
|
|
this.style.setProperty('--data-height', dataHeight + 'px');
|
|
this.style.setProperty('--view-height', viewHeight + 'px');
|
|
this.timelineNode.style.overflowY =
|
|
(height > kTimelineHeight) ? 'scroll' : 'hidden';
|
|
}
|
|
|
|
_update() {
|
|
this._legend.update();
|
|
this._drawContent().then(() => this._drawAnnotations(this.selectedEntry));
|
|
this._resetCachedDimensions();
|
|
}
|
|
|
|
async _drawContent() {
|
|
if (this._timeline.isEmpty()) return;
|
|
await delay(5);
|
|
const chunks = this.chunks;
|
|
const max = chunks.max(each => each.size());
|
|
let buffer = '';
|
|
for (let i = 0; i < chunks.length; i++) {
|
|
const chunk = chunks[i];
|
|
const height = (chunk.size() / max * kChunkHeight);
|
|
chunk.height = height;
|
|
if (chunk.isEmpty()) continue;
|
|
buffer += '<g>';
|
|
buffer += this._drawChunk(i, chunk);
|
|
buffer += '</g>'
|
|
}
|
|
this._scalableContentNode.innerHTML = buffer;
|
|
this._scalableContentNode.style.transform = 'scale(1, 1)';
|
|
}
|
|
|
|
_drawChunk(chunkIndex, chunk) {
|
|
const groups = chunk.getBreakdown(event => event.type);
|
|
let buffer = '';
|
|
const kHeight = chunk.height;
|
|
let lastHeight = kTimelineHeight;
|
|
for (let i = 0; i < groups.length; i++) {
|
|
const group = groups[i];
|
|
if (group.length == 0) break;
|
|
const height = (group.length / chunk.size() * kHeight) | 0;
|
|
lastHeight -= height;
|
|
const color = this._legend.colorForType(group.key);
|
|
buffer += `<rect x=${chunkIndex * kChunkWidth} y=${lastHeight} height=${
|
|
height} width=${kChunkVisualWidth} fill=${color} />`
|
|
}
|
|
return buffer;
|
|
}
|
|
|
|
_drawMarkers() {
|
|
// Put a time marker roughly every 20 chunks.
|
|
const expected = this._timeline.duration() / this._nofChunks * 20;
|
|
let interval = (10 ** Math.floor(Math.log10(expected)));
|
|
let correction = Math.log10(expected / interval);
|
|
correction = (correction < 0.33) ? 1 : (correction < 0.75) ? 2.5 : 5;
|
|
interval *= correction;
|
|
|
|
const start = this._timeline.startTime;
|
|
let time = start;
|
|
let buffer = '';
|
|
while (time < this._timeline.endTime) {
|
|
const delta = time - start;
|
|
const text = `${(delta / 1000) | 0} ms`;
|
|
const x = (delta * this._timeToPixel) | 0;
|
|
buffer += `<text x=${x - 2} y=0 class=markerText >${text}</text>`
|
|
buffer +=
|
|
`<line x1=${x} x2=${x} y1=12 y2=2000 dy=100% class=markerLine />`
|
|
time += interval;
|
|
}
|
|
this.timelineMarkersNode.innerHTML = buffer;
|
|
}
|
|
|
|
_drawAnnotations(logEntry, time) {
|
|
if (!this._focusedEntry) return;
|
|
this._drawEntryMark(this._focusedEntry);
|
|
}
|
|
|
|
_drawEntryMark(entry) {
|
|
const [x, y] = this._positionForEntry(entry);
|
|
const color = this._legend.colorForType(entry.type);
|
|
const mark =
|
|
`<circle cx=${x} cy=${y} r=3 stroke=${color} class=annotationPoint />`;
|
|
this.timelineAnnotationsNode.innerHTML = mark;
|
|
}
|
|
|
|
_handleUnlockedMouseEvent(event) {
|
|
this._focusedEntry = this._getEntryForEvent(event);
|
|
if (!this._focusedEntry) return false;
|
|
this._updateToolTip(event);
|
|
const time = this.positionToTime(event.pageX);
|
|
this._drawAnnotations(this._focusedEntry, time);
|
|
}
|
|
|
|
_updateToolTip(event) {
|
|
if (!this._focusedEntry) return false;
|
|
this.dispatchEvent(
|
|
new ToolTipEvent(this._focusedEntry, this.toolTipTargetNode));
|
|
event.stopImmediatePropagation();
|
|
}
|
|
|
|
_handleClick(event) {
|
|
if (event.button !== 0) return;
|
|
if (event.target === this.timelineChunks) return;
|
|
this.isLocked = !this.isLocked;
|
|
// Do this unconditionally since we want the tooltip to be update to the
|
|
// latest locked state.
|
|
this._handleUnlockedMouseEvent(event);
|
|
return false;
|
|
}
|
|
|
|
_handleDoubleClick(event) {
|
|
if (event.button !== 0) return;
|
|
this._selectionHandler.clearSelection();
|
|
const time = this.positionToTime(event.pageX);
|
|
const chunk = this._getChunkForEvent(event)
|
|
if (!chunk) return;
|
|
event.stopImmediatePropagation();
|
|
this.dispatchEvent(new SelectTimeEvent(chunk.start, chunk.end));
|
|
return false;
|
|
}
|
|
|
|
_handleMouseMove(event) {
|
|
if (event.button !== 0) return;
|
|
if (this._selectionHandler.isSelecting) return false;
|
|
if (this.isLocked && this._focusedEntry) {
|
|
this._updateToolTip(event);
|
|
return false;
|
|
}
|
|
this._handleUnlockedMouseEvent(event);
|
|
}
|
|
|
|
_getChunkForEvent(event) {
|
|
const time = this.positionToTime(event.pageX);
|
|
return this._chunkForTime(time);
|
|
}
|
|
|
|
_chunkForTime(time) {
|
|
const chunkIndex = ((time - this._timeline.startTime) /
|
|
this._timeline.duration() * this._nofChunks) |
|
|
0;
|
|
return this.chunks[chunkIndex];
|
|
}
|
|
|
|
_positionForEntry(entry) {
|
|
const chunk = this._chunkForTime(entry.time);
|
|
if (chunk === undefined) return [-1, -1];
|
|
const xFrom = (chunk.index * kChunkWidth + kChunkVisualWidth / 2) | 0;
|
|
const yFrom = kTimelineHeight - chunk.yOffset(entry) | 0;
|
|
return [xFrom, yFrom];
|
|
}
|
|
|
|
_getEntryForEvent(event) {
|
|
const chunk = this._getChunkForEvent(event);
|
|
if (chunk?.isEmpty() ?? true) return false;
|
|
const relativeIndex = Math.round(
|
|
(kTimelineHeight - event.layerY) / chunk.height * (chunk.size() - 1));
|
|
if (relativeIndex > chunk.size()) return false;
|
|
const logEntry = chunk.at(relativeIndex);
|
|
const style = this.toolTipTargetNode.style;
|
|
style.left = `${chunk.index * kChunkWidth}px`;
|
|
style.top = `${kTimelineHeight - chunk.height}px`;
|
|
style.height = `${chunk.height}px`;
|
|
style.width = `${kChunkVisualWidth}px`;
|
|
return logEntry;
|
|
}
|
|
};
|
|
|
|
class SelectionHandler {
|
|
// TODO turn into static field once Safari supports it.
|
|
static get SELECTION_OFFSET() {
|
|
return 10
|
|
};
|
|
|
|
_timeSelection = {start: -1, end: Infinity};
|
|
_selectionOriginTime = -1;
|
|
|
|
constructor(timeline) {
|
|
this._timeline = timeline;
|
|
this._timelineNode.addEventListener(
|
|
'mousedown', e => this._handleTimeSelectionMouseDown(e));
|
|
this._timelineNode.addEventListener(
|
|
'mouseup', e => this._handleTimeSelectionMouseUp(e));
|
|
this._timelineNode.addEventListener(
|
|
'mousemove', e => this._handleTimeSelectionMouseMove(e));
|
|
}
|
|
|
|
update() {
|
|
if (!this.hasSelection) {
|
|
this._selectionNode.style.display = 'none';
|
|
return;
|
|
}
|
|
this._selectionNode.style.display = 'inherit';
|
|
const startPosition = this.timeToPosition(this._timeSelection.start);
|
|
const endPosition = this.timeToPosition(this._timeSelection.end);
|
|
this._leftHandleNode.style.left = startPosition + 'px';
|
|
this._rightHandleNode.style.left = endPosition + 'px';
|
|
const delta = endPosition - startPosition;
|
|
const selectionNode = this._selectionBackgroundNode;
|
|
selectionNode.style.left = startPosition + 'px';
|
|
selectionNode.style.width = delta + 'px';
|
|
}
|
|
|
|
set timeSelection(selection) {
|
|
this._timeSelection.start = selection.start;
|
|
this._timeSelection.end = selection.end;
|
|
}
|
|
|
|
clearSelection() {
|
|
this._timeline.dispatchEvent(new SelectTimeEvent());
|
|
}
|
|
|
|
timeToPosition(posX) {
|
|
return this._timeline.timeToPosition(posX);
|
|
}
|
|
|
|
positionToTime(posX) {
|
|
return this._timeline.positionToTime(posX);
|
|
}
|
|
|
|
get isSelecting() {
|
|
return this._selectionOriginTime >= 0;
|
|
}
|
|
|
|
get hasSelection() {
|
|
return this._timeSelection.start >= 0 &&
|
|
this._timeSelection.end != Infinity;
|
|
}
|
|
|
|
get _timelineNode() {
|
|
return this._timeline.$('#timeline');
|
|
}
|
|
|
|
get _selectionNode() {
|
|
return this._timeline.$('#selection');
|
|
}
|
|
|
|
get _selectionBackgroundNode() {
|
|
return this._timeline.$('#selectionBackground');
|
|
}
|
|
|
|
get _leftHandleNode() {
|
|
return this._timeline.$('#leftHandle');
|
|
}
|
|
|
|
get _rightHandleNode() {
|
|
return this._timeline.$('#rightHandle');
|
|
}
|
|
|
|
get _leftHandlePosX() {
|
|
return this._leftHandleNode.getBoundingClientRect().x;
|
|
}
|
|
|
|
get _rightHandlePosX() {
|
|
return this._rightHandleNode.getBoundingClientRect().x;
|
|
}
|
|
|
|
_isOnLeftHandle(posX) {
|
|
return Math.abs(this._leftHandlePosX - posX) <=
|
|
SelectionHandler.SELECTION_OFFSET;
|
|
}
|
|
|
|
_isOnRightHandle(posX) {
|
|
return Math.abs(this._rightHandlePosX - posX) <=
|
|
SelectionHandler.SELECTION_OFFSET;
|
|
}
|
|
|
|
_handleTimeSelectionMouseDown(event) {
|
|
if (event.button !== 0) return;
|
|
let xPosition = event.clientX
|
|
// Update origin time in case we click on a handle.
|
|
if (this._isOnLeftHandle(xPosition)) {
|
|
xPosition = this._rightHandlePosX;
|
|
}
|
|
else if (this._isOnRightHandle(xPosition)) {
|
|
xPosition = this._leftHandlePosX;
|
|
}
|
|
this._selectionOriginTime = this.positionToTime(xPosition);
|
|
}
|
|
|
|
_handleTimeSelectionMouseMove(event) {
|
|
if (event.button !== 0) return;
|
|
if (!this.isSelecting) return;
|
|
const currentTime = this.positionToTime(event.clientX);
|
|
this._timeline.dispatchEvent(new SynchronizeSelectionEvent(
|
|
Math.min(this._selectionOriginTime, currentTime),
|
|
Math.max(this._selectionOriginTime, currentTime)));
|
|
}
|
|
|
|
_handleTimeSelectionMouseUp(event) {
|
|
if (event.button !== 0) return;
|
|
this._selectionOriginTime = -1;
|
|
if (this._timeSelection.start === -1) return;
|
|
const delta = this._timeSelection.end - this._timeSelection.start;
|
|
if (delta <= 1 || isNaN(delta)) return;
|
|
this._timeline.dispatchEvent(new SelectTimeEvent(
|
|
this._timeSelection.start, this._timeSelection.end));
|
|
}
|
|
}
|
|
|
|
class Legend {
|
|
_timeline;
|
|
_lastSelection;
|
|
_typesFilters = new Map();
|
|
_typeClickHandler = this._handleTypeClick.bind(this);
|
|
_filterPredicate = this.filter.bind(this);
|
|
onFilter = () => {};
|
|
|
|
constructor(table) {
|
|
this._table = table;
|
|
this._enableDuration = false;
|
|
}
|
|
|
|
set timeline(timeline) {
|
|
this._timeline = timeline;
|
|
const groups = timeline.getBreakdown();
|
|
this._typesFilters = new Map(groups.map(each => [each.key, true]));
|
|
this._colors =
|
|
new Map(groups.map(each => [each.key, CSSColor.at(each.id)]));
|
|
}
|
|
|
|
get selection() {
|
|
return this._timeline.selectionOrSelf;
|
|
}
|
|
|
|
get filterPredicate() {
|
|
for (let visible of this._typesFilters.values()) {
|
|
if (!visible) return this._filterPredicate;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
colorForType(type) {
|
|
let color = this._colors.get(type);
|
|
if (color === undefined) {
|
|
color = CSSColor.at(this._colors.size);
|
|
this._colors.set(type, color);
|
|
}
|
|
return color;
|
|
}
|
|
|
|
filter(logEntry) {
|
|
return this._typesFilters.get(logEntry.type);
|
|
}
|
|
|
|
update() {
|
|
if (this._lastSelection === this.selection) return;
|
|
this._lastSelection = this.selection;
|
|
const tbody = DOM.tbody();
|
|
const missingTypes = new Set(this._typesFilters.keys());
|
|
this._checkDurationField();
|
|
this.selection.getBreakdown(undefined, this._enableDuration)
|
|
.forEach(group => {
|
|
tbody.appendChild(this._addTypeRow(group));
|
|
missingTypes.delete(group.key);
|
|
});
|
|
missingTypes.forEach(
|
|
key => tbody.appendChild(this._addRow('', key, 0, '0%')));
|
|
if (this._timeline.selection) {
|
|
tbody.appendChild(
|
|
this._addRow('', 'Selection', this.selection.length, '100%'));
|
|
}
|
|
tbody.appendChild(this._addRow('', 'All', this._timeline.length, ''));
|
|
this._table.tBodies[0].replaceWith(tbody);
|
|
}
|
|
|
|
_checkDurationField() {
|
|
if (this._enableDuration) return;
|
|
const example = this.selection.at(0);
|
|
if (!example || !('duration' in example)) return;
|
|
this._enableDuration = true;
|
|
this._table.tHead.rows[0].appendChild(DOM.td('Duration'));
|
|
}
|
|
|
|
_addRow(colorNode, type, count, countPercent, duration, durationPercent) {
|
|
const row = DOM.tr();
|
|
row.appendChild(DOM.td(colorNode));
|
|
const typeCell = row.appendChild(DOM.td(type));
|
|
typeCell.setAttribute('title', type);
|
|
row.appendChild(DOM.td(count.toString()));
|
|
row.appendChild(DOM.td(countPercent));
|
|
if (this._enableDuration) {
|
|
row.appendChild(DOM.td(formatDurationMicros(duration ?? 0)));
|
|
row.appendChild(DOM.td(durationPercent ?? '0%'));
|
|
}
|
|
return row
|
|
}
|
|
|
|
_addTypeRow(group) {
|
|
const color = this.colorForType(group.key);
|
|
const colorDiv = DOM.div('colorbox');
|
|
if (this._typesFilters.get(group.key)) {
|
|
colorDiv.style.backgroundColor = color;
|
|
} else {
|
|
colorDiv.style.borderColor = color;
|
|
colorDiv.style.backgroundColor = CSSColor.backgroundImage;
|
|
}
|
|
let duration = 0;
|
|
if (this._enableDuration) {
|
|
const entries = group.entries;
|
|
for (let i = 0; i < entries.length; i++) {
|
|
duration += entries[i].duration;
|
|
}
|
|
}
|
|
let countPercent =
|
|
`${(group.length / this.selection.length * 100).toFixed(1)}%`;
|
|
const row = this._addRow(
|
|
colorDiv, group.key, group.length, countPercent, duration, '');
|
|
row.className = 'clickable';
|
|
row.onclick = this._typeClickHandler;
|
|
row.data = group.key;
|
|
return row;
|
|
}
|
|
|
|
_handleTypeClick(e) {
|
|
const type = e.currentTarget.data;
|
|
this._typesFilters.set(type, !this._typesFilters.get(type));
|
|
this.onFilter(type);
|
|
}
|
|
}
|