[tools] System-analyzer timeline improvements
- Timeline.selection is now a Timeline as well - Allow remove the current timeline-track selection by double-clicking outside-the selection - Update the timeline-track stats based on the current selection - Simplify DOM element creation methods - Add separate SelectionHandler class for timeline-track Bug: v8:10644 Change-Id: I4f15d6ab4f5ec6b7330e22769472ca3074b00edd Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2565130 Commit-Queue: Camillo Bruni <cbruni@chromium.org> Reviewed-by: Sathya Gunasekaran <gsathya@chromium.org> Cr-Commit-Position: refs/heads/master@{#71497}
This commit is contained in:
parent
21cec63d11
commit
94f0536635
@ -44,7 +44,9 @@ import { LogEntry} from "../../../tools/system-analyzer/log/log.mjs";
|
||||
let startTime = time;
|
||||
let endTime = time + 2;
|
||||
timeline.selectTimeRange(startTime, endTime);
|
||||
assertArrayEquals(timeline.selection, [entry1, entry2]);
|
||||
assertArrayEquals(timeline.selection.startTime, startTime);
|
||||
assertArrayEquals(timeline.selection.endTime, endTime);
|
||||
assertArrayEquals(timeline.selection.values, [entry1, entry2]);
|
||||
let entryIdx = timeline.find(time + 1);
|
||||
let entry = timeline.at(entryIdx);
|
||||
assertEquals(entry.time, time + 1);
|
||||
|
@ -34,8 +34,7 @@ export class Group {
|
||||
static groupBy(entries, property) {
|
||||
let accumulator = Object.create(null);
|
||||
let length = entries.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
let entry = entries[i];
|
||||
for (let entry of entries) {
|
||||
let key = entry[property];
|
||||
if (accumulator[key] == undefined) {
|
||||
accumulator[key] = new Group(property, key, entry);
|
||||
|
@ -144,6 +144,7 @@ button:hover {
|
||||
}
|
||||
|
||||
.colorbox {
|
||||
display: inline-block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border: 1px var(--background-color) solid;
|
||||
|
@ -64,11 +64,12 @@ class App {
|
||||
}
|
||||
|
||||
handleShowEntries(e) {
|
||||
if (e.entries[0] instanceof MapLogEntry) {
|
||||
const entry = e.entries[0];
|
||||
if (entry instanceof MapLogEntry) {
|
||||
this.showMapEntries(e.entries);
|
||||
} else if (e.entries[0] instanceof IcLogEntry) {
|
||||
} else if (entry instanceof IcLogEntry) {
|
||||
this.showIcEntries(e.entries);
|
||||
} else if (e.entries[0] instanceof SourcePosition) {
|
||||
} else if (entry instanceof SourcePosition) {
|
||||
this.showSourcePositionEntries(e.entries);
|
||||
} else {
|
||||
throw new Error('Unknown selection type!');
|
||||
@ -117,11 +118,12 @@ class App {
|
||||
}
|
||||
|
||||
handleShowEntryDetail(e) {
|
||||
if (e.entry instanceof MapLogEntry) {
|
||||
const entry = e.entry;
|
||||
if (entry instanceof MapLogEntry) {
|
||||
this.selectMapLogEntry(e.entry);
|
||||
} else if (e.entry instanceof IcLogEntry) {
|
||||
} else if (entry instanceof IcLogEntry) {
|
||||
this.selectICLogEntry(e.entry);
|
||||
} else if (e.entry instanceof SourcePosition) {
|
||||
} else if (entry instanceof SourcePosition) {
|
||||
this.selectSourcePosition(e.entry);
|
||||
} else {
|
||||
throw new Error('Unknown selection type!');
|
||||
@ -137,7 +139,7 @@ class App {
|
||||
|
||||
selectICLogEntry(entry) {
|
||||
this._state.ic = entry;
|
||||
this._view.icPanel.selectedLogEntries = [entry];
|
||||
this._view.icPanel.selectedEntry = [entry];
|
||||
}
|
||||
|
||||
selectSourcePosition(sourcePositions) {
|
||||
|
@ -9,13 +9,13 @@ class Timeline {
|
||||
_values;
|
||||
// Current selection, subset of #values:
|
||||
_selection;
|
||||
_uniqueTypes;
|
||||
_breakdown;
|
||||
|
||||
constructor(model) {
|
||||
constructor(model, values = [], startTime = 0, endTime = 0) {
|
||||
this._model = model;
|
||||
this._values = [];
|
||||
this.startTime = 0;
|
||||
this.endTime = 0;
|
||||
this._values = values;
|
||||
this.startTime = startTime;
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
get model() {
|
||||
@ -35,16 +35,19 @@ class Timeline {
|
||||
}
|
||||
|
||||
selectTimeRange(startTime, endTime) {
|
||||
this._selection = this.range(startTime, endTime);
|
||||
const items = this.range(startTime, endTime);
|
||||
this._selection = new Timeline(this._model, items, startTime, endTime);
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this._selection = undefined;
|
||||
}
|
||||
|
||||
getChunks(windowSizeMs) {
|
||||
// TODO(zcankara) Fill this one
|
||||
return this.chunkSizes(windowSizeMs);
|
||||
}
|
||||
|
||||
get values() {
|
||||
// TODO(zcankara) Not to break something delete later
|
||||
return this._values;
|
||||
}
|
||||
|
||||
@ -94,6 +97,10 @@ class Timeline {
|
||||
return this._values.length;
|
||||
}
|
||||
|
||||
slice(startIndex, endIndex) {
|
||||
return this._values.slice(startIndex, endIndex);
|
||||
}
|
||||
|
||||
first() {
|
||||
return this._values[0];
|
||||
}
|
||||
@ -102,11 +109,17 @@ class Timeline {
|
||||
return this._values[this._values.length - 1];
|
||||
}
|
||||
|
||||
* [Symbol.iterator]() {
|
||||
yield* this._values;
|
||||
}
|
||||
|
||||
duration() {
|
||||
if (this.isEmpty()) return 0;
|
||||
return this.last().time - this.first().time;
|
||||
}
|
||||
|
||||
forEachChunkSize(count, fn) {
|
||||
if (this.isEmpty()) return;
|
||||
const increment = this.duration() / count;
|
||||
let currentTime = this.first().time + increment;
|
||||
let index = 0;
|
||||
@ -162,24 +175,12 @@ class Timeline {
|
||||
return minIndex;
|
||||
}
|
||||
|
||||
_initializeTypes() {
|
||||
const types = new Map();
|
||||
let index = 0;
|
||||
for (const entry of this.all) {
|
||||
let entries = types.get(entry.type);
|
||||
if (entries != undefined) {
|
||||
entries.push(entry)
|
||||
} else {
|
||||
entries = [entry];
|
||||
entries.index = index++;
|
||||
types.set(entry.type, entries)
|
||||
}
|
||||
getBreakdown(keyFunction) {
|
||||
if (keyFunction) return breakdown(this._values, keyFunction);
|
||||
if (this._breakdown === undefined) {
|
||||
this._breakdown = breakdown(this._values, each => each.type);
|
||||
}
|
||||
return this._uniqueTypes = types;
|
||||
}
|
||||
|
||||
get uniqueTypes() {
|
||||
return this._uniqueTypes ?? this._initializeTypes();
|
||||
return this._breakdown;
|
||||
}
|
||||
|
||||
depthHistogram() {
|
||||
@ -259,26 +260,7 @@ class Chunk {
|
||||
}
|
||||
|
||||
getBreakdown(keyFunction) {
|
||||
if (this.items.length === 0) return [];
|
||||
if (keyFunction === void 0) {
|
||||
keyFunction = each => each;
|
||||
}
|
||||
const typeToindex = new Map();
|
||||
const breakdown = [];
|
||||
// This is performance critical, resorting to for-loop
|
||||
for (let i = 0; i < this.items.length; i++) {
|
||||
const each = this.items[i];
|
||||
const type = keyFunction(each);
|
||||
const index = typeToindex.get(type);
|
||||
if (index === void 0) {
|
||||
typeToindex.set(type, breakdown.length);
|
||||
breakdown.push([type, 0]);
|
||||
} else {
|
||||
breakdown[index][1]++;
|
||||
}
|
||||
}
|
||||
// Sort by count
|
||||
return breakdown.sort((a, b) => a[1] - b[1]);
|
||||
return breakdown(this.items, keyFunction);
|
||||
}
|
||||
|
||||
filter() {
|
||||
@ -286,4 +268,26 @@ class Chunk {
|
||||
}
|
||||
}
|
||||
|
||||
function breakdown(array, keyFunction) {
|
||||
if (array.length === 0) return [];
|
||||
if (keyFunction === undefined) keyFunction = each => each;
|
||||
const typeToindex = new Map();
|
||||
const breakdown = [];
|
||||
let id = 0;
|
||||
// This is performance critical, resorting to for-loop
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const each = array[i];
|
||||
const type = keyFunction(each);
|
||||
const index = typeToindex.get(type);
|
||||
if (index === void 0) {
|
||||
typeToindex.set(type, breakdown.length);
|
||||
breakdown.push({type: type, count: 0, id: id++});
|
||||
} else {
|
||||
breakdown[index].count++;
|
||||
}
|
||||
}
|
||||
// Sort by count
|
||||
return breakdown.sort((a, b) => b.count - a.count);
|
||||
}
|
||||
|
||||
export {Timeline, Chunk};
|
||||
|
@ -30,7 +30,7 @@ export class SelectTimeEvent extends CustomEvent {
|
||||
static get name() {
|
||||
return 'timerangeselect';
|
||||
}
|
||||
constructor(start, end) {
|
||||
constructor(start = -1, end = Infinity) {
|
||||
super(SelectTimeEvent.name, {bubbles: true, composed: true});
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
|
@ -13,7 +13,7 @@ class CSSColor {
|
||||
if (color === undefined) {
|
||||
throw new Error(`CSS color does not exist: ${name}`);
|
||||
}
|
||||
this._cache.set(name, color);
|
||||
this._cache.set(name, color.trim());
|
||||
return color;
|
||||
}
|
||||
static reset() {
|
||||
@ -74,51 +74,61 @@ class CSSColor {
|
||||
static get violet() {
|
||||
return this.get('violet');
|
||||
}
|
||||
static at(index) {
|
||||
return this.list[index % this.list.length];
|
||||
}
|
||||
static get list() {
|
||||
if (!this._colors) {
|
||||
this._colors = [
|
||||
this.green,
|
||||
this.violet,
|
||||
this.orange,
|
||||
this.yellow,
|
||||
this.primaryColor,
|
||||
this.red,
|
||||
this.blue,
|
||||
this.yellow,
|
||||
this.secondaryColor,
|
||||
];
|
||||
}
|
||||
return this._colors;
|
||||
}
|
||||
}
|
||||
|
||||
const kColors = [
|
||||
CSSColor.green,
|
||||
CSSColor.violet,
|
||||
CSSColor.orange,
|
||||
CSSColor.yellow,
|
||||
CSSColor.primaryColor,
|
||||
CSSColor.red,
|
||||
CSSColor.blue,
|
||||
CSSColor.yellow,
|
||||
CSSColor.secondaryColor,
|
||||
];
|
||||
|
||||
class DOM {
|
||||
static element(type, classes) {
|
||||
const node = document.createElement(type);
|
||||
if (classes === undefined) return node;
|
||||
if (typeof classes === 'string') {
|
||||
node.className = classes;
|
||||
} else {
|
||||
classes.forEach(cls => node.classList.add(cls));
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
static text(string) {
|
||||
return document.createTextNode(string);
|
||||
}
|
||||
|
||||
static div(classes) {
|
||||
const node = document.createElement('div');
|
||||
if (classes !== void 0) {
|
||||
if (typeof classes === 'string') {
|
||||
node.className = classes;
|
||||
} else {
|
||||
classes.forEach(cls => node.classList.add(cls));
|
||||
}
|
||||
}
|
||||
return node;
|
||||
return this.element('div', classes);
|
||||
}
|
||||
|
||||
static span(classes) {
|
||||
const node = document.createElement('span');
|
||||
if (classes !== undefined) {
|
||||
if (typeof classes === 'string') {
|
||||
node.className = classes;
|
||||
} else {
|
||||
classes.forEach(cls => node.classList.add(cls));
|
||||
}
|
||||
}
|
||||
return node;
|
||||
return this.element('span', classes);
|
||||
}
|
||||
static table(className) {
|
||||
const node = document.createElement('table');
|
||||
if (className) node.className = className;
|
||||
return node;
|
||||
|
||||
static table(classes) {
|
||||
return this.element('table', classes);
|
||||
}
|
||||
|
||||
static tbody(classes) {
|
||||
return this.element('tbody', classes);
|
||||
}
|
||||
|
||||
static td(textOrNode, className) {
|
||||
const node = document.createElement('td');
|
||||
const node = this.element('td');
|
||||
if (typeof textOrNode === 'object') {
|
||||
node.appendChild(textOrNode);
|
||||
} else if (textOrNode) {
|
||||
@ -128,14 +138,8 @@ class DOM {
|
||||
return node;
|
||||
}
|
||||
|
||||
static tr(className) {
|
||||
const node = document.createElement('tr');
|
||||
if (className) node.className = className;
|
||||
return node;
|
||||
}
|
||||
|
||||
static text(string) {
|
||||
return document.createTextNode(string);
|
||||
static tr(classes) {
|
||||
return this.element('tr', classes);
|
||||
}
|
||||
|
||||
static removeAllChildren(node) {
|
||||
@ -225,7 +229,6 @@ export * from '../helper.mjs';
|
||||
export {
|
||||
DOM,
|
||||
$,
|
||||
kColors,
|
||||
V8CustomElement,
|
||||
CSSColor,
|
||||
LazyTable,
|
||||
|
@ -12,6 +12,7 @@ import {DOM, V8CustomElement} from './helper.mjs';
|
||||
DOM.defineCustomElement(
|
||||
'view/ic-panel', (templateText) => class ICPanel extends V8CustomElement {
|
||||
_selectedLogEntries;
|
||||
_selectedLogEntry;
|
||||
_timeline;
|
||||
|
||||
_detailsClickHandler = this.handleDetailsClick.bind(this);
|
||||
@ -54,6 +55,10 @@ DOM.defineCustomElement(
|
||||
this.update();
|
||||
}
|
||||
|
||||
set selectedLogEntry(entry) {
|
||||
// TODO: show details
|
||||
}
|
||||
|
||||
_update() {
|
||||
this._updateCount();
|
||||
this._updateTable();
|
||||
|
@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
import {FocusEvent, ToolTipEvent} from '../events.mjs';
|
||||
import {kColors} from '../helper.mjs';
|
||||
import {CSSColor} from '../helper.mjs';
|
||||
import {DOM, V8CustomElement} from '../helper.mjs';
|
||||
|
||||
DOM.defineCustomElement(
|
||||
@ -10,6 +10,7 @@ DOM.defineCustomElement(
|
||||
(templateText) => class MapTransitions extends V8CustomElement {
|
||||
_timeline;
|
||||
_map;
|
||||
_edgeToColor = new Map();
|
||||
_selectedMapLogEntries;
|
||||
_displayedMapsInTree;
|
||||
_toggleSubtreeHandler = this._handleToggleSubtree.bind(this);
|
||||
@ -42,6 +43,10 @@ DOM.defineCustomElement(
|
||||
|
||||
set timeline(timeline) {
|
||||
this._timeline = timeline;
|
||||
this._edgeToColor.clear();
|
||||
timeline.getBreakdown().forEach(breakdown => {
|
||||
this._edgeToColor.set(breakdown.type, CSSColor.at(breakdown.id));
|
||||
});
|
||||
}
|
||||
|
||||
set selectedMapLogEntries(list) {
|
||||
@ -53,11 +58,6 @@ DOM.defineCustomElement(
|
||||
return this._selectedMapLogEntries;
|
||||
}
|
||||
|
||||
_edgeToColor(edge) {
|
||||
const index = this._timeline.uniqueTypes.get(edge.type).index
|
||||
return kColors[index % kColors.length];
|
||||
}
|
||||
|
||||
_handleTransitionViewChange(e) {
|
||||
this.tooltip.style.left = e.pageX + 'px';
|
||||
this.tooltip.style.top = e.pageY + 'px';
|
||||
@ -124,7 +124,7 @@ DOM.defineCustomElement(
|
||||
_addTransitionEdge(map) {
|
||||
let classes = ['transitionEdge'];
|
||||
let edge = DOM.div(classes);
|
||||
edge.style.backgroundColor = this._edgeToColor(map.edge);
|
||||
edge.style.backgroundColor = this._edgeToColor.get(map.edge.type);
|
||||
let labelNode = DOM.div('transitionLabel');
|
||||
labelNode.innerText = map.edge.toString();
|
||||
edge.appendChild(labelNode);
|
||||
@ -153,7 +153,8 @@ DOM.defineCustomElement(
|
||||
|
||||
_addMapNode(map) {
|
||||
let node = DOM.div('map');
|
||||
if (map.edge) node.style.backgroundColor = this._edgeToColor(map.edge);
|
||||
if (map.edge)
|
||||
node.style.backgroundColor = this._edgeToColor.get(map.edge.type);
|
||||
node.map = map;
|
||||
node.onclick = this._selectMapHandler
|
||||
node.onmouseover = this._mouseoverMapHandler
|
||||
|
@ -85,6 +85,10 @@ found in the LICENSE file. -->
|
||||
#legend td:nth-of-type(4n+4) {
|
||||
text-align: right;
|
||||
}
|
||||
/* Center colors */
|
||||
#legend td:nth-of-type(4n+1) {
|
||||
text-align: center;;
|
||||
}
|
||||
|
||||
.legendTypeColumn {
|
||||
width: 100%;
|
||||
@ -94,8 +98,8 @@ found in the LICENSE file. -->
|
||||
background-color: var(--timeline-background-color);
|
||||
}
|
||||
|
||||
#timeline .rightHandle,
|
||||
#timeline .leftHandle {
|
||||
#rightHandle,
|
||||
#leftHandle {
|
||||
background-color: rgba(200, 200, 200, 0.5);
|
||||
height: 100%;
|
||||
width: 5px;
|
||||
@ -103,14 +107,14 @@ found in the LICENSE file. -->
|
||||
z-index: 3;
|
||||
cursor: col-resize;
|
||||
}
|
||||
#timeline .leftHandle {
|
||||
#leftHandle {
|
||||
border-left: 1px solid var(--on-surface-color);
|
||||
}
|
||||
#timeline .rightHandle {
|
||||
#rightHandle {
|
||||
border-right: 1px solid var(--on-surface-color);
|
||||
}
|
||||
|
||||
#timeline .selection {
|
||||
#selectionBackground {
|
||||
background-color: rgba(133, 68, 163, 0.5);
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
@ -125,13 +129,14 @@ found in the LICENSE file. -->
|
||||
<td>Percent</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="legendContent">
|
||||
</tbody>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<div id="timeline">
|
||||
<div class="leftHandle"></div>
|
||||
<div class="selection"></div>
|
||||
<div class="rightHandle"></div>
|
||||
<div id="selection">
|
||||
<div id="leftHandle"></div>
|
||||
<div id="selectionBackground"></div>
|
||||
<div id="rightHandle"></div>
|
||||
</div>
|
||||
<div id="timelineLabel">Frequency</div>
|
||||
<div id="timelineChunks"></div>
|
||||
<canvas id="timelineCanvas"></canvas>
|
||||
|
@ -5,113 +5,59 @@
|
||||
import {kChunkHeight, kChunkWidth} from '../../log/map.mjs';
|
||||
import {MapLogEntry} from '../../log/map.mjs';
|
||||
import {FocusEvent, SelectionEvent, SelectTimeEvent, SynchronizeSelectionEvent, ToolTipEvent,} from '../events.mjs';
|
||||
import {CSSColor, DOM, kColors, V8CustomElement} from '../helper.mjs';
|
||||
import {CSSColor, DOM, V8CustomElement} from '../helper.mjs';
|
||||
|
||||
DOM.defineCustomElement('view/timeline/timeline-track',
|
||||
(templateText) =>
|
||||
class TimelineTrack extends V8CustomElement {
|
||||
// TODO turn into static field once Safari supports it.
|
||||
static get SELECTION_OFFSET() {
|
||||
return 10
|
||||
};
|
||||
_timeline;
|
||||
_nofChunks = 400;
|
||||
_chunks;
|
||||
_selectedEntry;
|
||||
_timeToPixel;
|
||||
_timeSelection = {start: -1, end: Infinity};
|
||||
_timeStartOffset;
|
||||
_selectionOriginTime;
|
||||
_typeToColor;
|
||||
_legend;
|
||||
|
||||
_entryTypeDoubleClickHandler = this.handleEntryTypeDoubleClick.bind(this);
|
||||
_chunkMouseMoveHandler = this.handleChunkMouseMove.bind(this);
|
||||
_chunkClickHandler = this.handleChunkClick.bind(this);
|
||||
_chunkDoubleClickHandler = this.handleChunkDoubleClick.bind(this);
|
||||
_chunkMouseMoveHandler = this._handleChunkMouseMove.bind(this);
|
||||
_chunkClickHandler = this._handleChunkClick.bind(this);
|
||||
_chunkDoubleClickHandler = this._handleChunkDoubleClick.bind(this);
|
||||
|
||||
constructor() {
|
||||
super(templateText);
|
||||
this.timeline.addEventListener('scroll', e => this.handleTimelineScroll(e));
|
||||
this.timeline.addEventListener(
|
||||
'mousedown', e => this.handleTimeSelectionMouseDown(e));
|
||||
this.timeline.addEventListener(
|
||||
'mouseup', e => this.handleTimeSelectionMouseUp(e));
|
||||
this.timeline.addEventListener(
|
||||
'mousemove', e => this.handleTimeSelectionMouseMove(e));
|
||||
this._selectionHandler = new SelectionHandler(this);
|
||||
this._legend = new Legend(this.$('#legend'));
|
||||
this.timelineNode.addEventListener(
|
||||
'scroll', e => this._handleTimelineScroll(e));
|
||||
this.timelineNode.ondblclick = (e) =>
|
||||
this._selectionHandler.clearSelection();
|
||||
this.isLocked = false;
|
||||
}
|
||||
|
||||
handleTimeSelectionMouseDown(e) {
|
||||
let xPosition = e.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);
|
||||
}
|
||||
|
||||
isOnLeftHandle(posX) {
|
||||
return (
|
||||
Math.abs(this.leftHandlePosX - posX) <= TimelineTrack.SELECTION_OFFSET);
|
||||
}
|
||||
|
||||
isOnRightHandle(posX) {
|
||||
return (
|
||||
Math.abs(this.rightHandlePosX - posX) <=
|
||||
TimelineTrack.SELECTION_OFFSET);
|
||||
}
|
||||
|
||||
handleTimeSelectionMouseMove(e) {
|
||||
if (!this._isSelecting) return;
|
||||
const currentTime = this.positionToTime(e.clientX);
|
||||
this.dispatchEvent(new SynchronizeSelectionEvent(
|
||||
Math.min(this._selectionOriginTime, currentTime),
|
||||
Math.max(this._selectionOriginTime, currentTime)));
|
||||
}
|
||||
|
||||
handleTimeSelectionMouseUp(e) {
|
||||
this._selectionOriginTime = -1;
|
||||
const delta = this._timeSelection.end - this._timeSelection.start;
|
||||
if (delta <= 1 || isNaN(delta)) return;
|
||||
this.dispatchEvent(new SelectTimeEvent(
|
||||
this._timeSelection.start, this._timeSelection.end));
|
||||
set data(timeline) {
|
||||
this._timeline = timeline;
|
||||
this._typeToColor = new Map();
|
||||
timeline.getBreakdown().forEach(
|
||||
each => this._typeToColor.set(each.type, CSSColor.at(each.id)));
|
||||
this._legend.timeline = timeline;
|
||||
this._updateChunks();
|
||||
this.update();
|
||||
}
|
||||
|
||||
set timeSelection(selection) {
|
||||
this._timeSelection.start = selection.start;
|
||||
this._timeSelection.end = selection.end;
|
||||
this._selectionHandler.timeSelection = selection;
|
||||
this.updateSelection();
|
||||
}
|
||||
|
||||
get _isSelecting() {
|
||||
return this._selectionOriginTime >= 0;
|
||||
}
|
||||
|
||||
updateSelection() {
|
||||
const startPosition = this.timeToPosition(this._timeSelection.start);
|
||||
const endPosition = this.timeToPosition(this._timeSelection.end);
|
||||
const delta = endPosition - startPosition;
|
||||
this.leftHandle.style.left = startPosition + 'px';
|
||||
this.selection.style.left = startPosition + 'px';
|
||||
this.rightHandle.style.left = endPosition + 'px';
|
||||
this.selection.style.width = delta + 'px';
|
||||
}
|
||||
|
||||
get leftHandlePosX() {
|
||||
return this.leftHandle.getBoundingClientRect().x;
|
||||
}
|
||||
|
||||
get rightHandlePosX() {
|
||||
return this.rightHandle.getBoundingClientRect().x;
|
||||
this._selectionHandler.update();
|
||||
this._legend.update();
|
||||
}
|
||||
|
||||
// Maps the clicked x position to the x position on timeline canvas
|
||||
positionOnTimeline(posX) {
|
||||
let rect = this.timeline.getBoundingClientRect();
|
||||
let posClickedX = posX - rect.left + this.timeline.scrollLeft;
|
||||
let rect = this.timelineNode.getBoundingClientRect();
|
||||
let posClickedX = posX - rect.left + this.timelineNode.scrollLeft;
|
||||
return posClickedX;
|
||||
}
|
||||
|
||||
@ -122,22 +68,10 @@ DOM.defineCustomElement('view/timeline/timeline-track',
|
||||
|
||||
timeToPosition(time) {
|
||||
let posX = time * this._timeToPixel;
|
||||
posX -= this._timeStartOffset
|
||||
posX -= this._timeStartOffset;
|
||||
return posX;
|
||||
}
|
||||
|
||||
get leftHandle() {
|
||||
return this.$('.leftHandle');
|
||||
}
|
||||
|
||||
get rightHandle() {
|
||||
return this.$('.rightHandle');
|
||||
}
|
||||
|
||||
get selection() {
|
||||
return this.$('.selection');
|
||||
}
|
||||
|
||||
get timelineCanvas() {
|
||||
return this.$('#timelineCanvas');
|
||||
}
|
||||
@ -146,41 +80,13 @@ DOM.defineCustomElement('view/timeline/timeline-track',
|
||||
return this.$('#timelineChunks');
|
||||
}
|
||||
|
||||
get timeline() {
|
||||
get timelineNode() {
|
||||
return this.$('#timeline');
|
||||
}
|
||||
|
||||
get timelineLegend() {
|
||||
return this.$('#legend');
|
||||
}
|
||||
|
||||
get timelineLegendContent() {
|
||||
return this.$('#legendContent');
|
||||
}
|
||||
|
||||
set data(value) {
|
||||
this._timeline = value;
|
||||
this._resetTypeToColorCache();
|
||||
// Only update legend if the timeline data has changed.
|
||||
this._updateLegend();
|
||||
this._updateChunks();
|
||||
this.update();
|
||||
}
|
||||
|
||||
_update() {
|
||||
this._updateTimeline();
|
||||
}
|
||||
|
||||
_resetTypeToColorCache() {
|
||||
this._typeToColor = new Map();
|
||||
let lastIndex = 0;
|
||||
for (const type of this.data.uniqueTypes.keys()) {
|
||||
this._typeToColor.set(type, kColors[lastIndex++]);
|
||||
}
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this._timeline;
|
||||
this._legend.update();
|
||||
}
|
||||
|
||||
set nofChunks(count) {
|
||||
@ -194,7 +100,7 @@ DOM.defineCustomElement('view/timeline/timeline-track',
|
||||
}
|
||||
|
||||
_updateChunks() {
|
||||
this._chunks = this.data.chunks(this.nofChunks);
|
||||
this._chunks = this._timeline.chunks(this.nofChunks);
|
||||
}
|
||||
|
||||
get chunks() {
|
||||
@ -211,43 +117,7 @@ DOM.defineCustomElement('view/timeline/timeline-track',
|
||||
}
|
||||
|
||||
set scrollLeft(offset) {
|
||||
this.timeline.scrollLeft = offset;
|
||||
}
|
||||
|
||||
typeToColor(type) {
|
||||
return this._typeToColor.get(type);
|
||||
}
|
||||
|
||||
_updateLegend() {
|
||||
let timelineLegendContent = this.timelineLegendContent;
|
||||
DOM.removeAllChildren(timelineLegendContent);
|
||||
this._timeline.uniqueTypes.forEach((entries, type) => {
|
||||
let row = DOM.tr('clickable');
|
||||
row.entries = entries;
|
||||
row.ondblclick = this.entryTypeDoubleClickHandler_;
|
||||
let color = this.typeToColor(type);
|
||||
if (color !== null) {
|
||||
let div = DOM.div('colorbox');
|
||||
div.style.backgroundColor = color;
|
||||
row.appendChild(DOM.td(div));
|
||||
} else {
|
||||
row.appendChild(DOM.td());
|
||||
}
|
||||
let td = DOM.td(type);
|
||||
row.appendChild(td);
|
||||
row.appendChild(DOM.td(entries.length));
|
||||
let percent = (entries.length / this.data.all.length) * 100;
|
||||
row.appendChild(DOM.td(percent.toFixed(1) + '%'));
|
||||
timelineLegendContent.appendChild(row);
|
||||
});
|
||||
// Add Total row.
|
||||
let row = DOM.tr();
|
||||
row.appendChild(DOM.td(''));
|
||||
row.appendChild(DOM.td('All'));
|
||||
row.appendChild(DOM.td(this.data.all.length));
|
||||
row.appendChild(DOM.td('100%'));
|
||||
timelineLegendContent.appendChild(row);
|
||||
this.timelineLegend.appendChild(timelineLegendContent);
|
||||
this.timelineNode.scrollLeft = offset;
|
||||
}
|
||||
|
||||
handleEntryTypeDoubleClick(e) {
|
||||
@ -255,10 +125,10 @@ DOM.defineCustomElement('view/timeline/timeline-track',
|
||||
}
|
||||
|
||||
timelineIndicatorMove(offset) {
|
||||
this.timeline.scrollLeft += offset;
|
||||
this.timelineNode.scrollLeft += offset;
|
||||
}
|
||||
|
||||
handleTimelineScroll(e) {
|
||||
_handleTimelineScroll(e) {
|
||||
let horizontal = e.currentTarget.scrollLeft;
|
||||
this.dispatchEvent(new CustomEvent(
|
||||
'scrolltrack', {bubbles: true, composed: true, detail: horizontal}));
|
||||
@ -271,11 +141,9 @@ DOM.defineCustomElement('view/timeline/timeline-track',
|
||||
let increment = 0;
|
||||
let lastHeight = 0.0;
|
||||
const stops = [];
|
||||
const breakDown = chunk.getBreakdown(map => map.type);
|
||||
for (let i = 0; i < breakDown.length; i++) {
|
||||
let [type, count] = breakDown[i];
|
||||
const color = this.typeToColor(type);
|
||||
increment += count;
|
||||
for (let breakdown of chunk.getBreakdown(map => map.type)) {
|
||||
const color = CSSColor.at(breakdown.id);
|
||||
increment += breakdown.count;
|
||||
let height = (increment / total * kHeight) | 0;
|
||||
stops.push(`${color} ${lastHeight}px ${height}px`)
|
||||
lastHeight = height;
|
||||
@ -288,8 +156,8 @@ DOM.defineCustomElement('view/timeline/timeline-track',
|
||||
let fragment = new DocumentFragment();
|
||||
let chunks = this.chunks;
|
||||
let max = chunks.max(each => each.size());
|
||||
let start = this.data.startTime;
|
||||
let end = this.data.endTime;
|
||||
let start = this._timeline.startTime;
|
||||
let end = this._timeline.endTime;
|
||||
let duration = end - start;
|
||||
this._timeToPixel = chunks.length * kChunkWidth / duration;
|
||||
this._timeStartOffset = start * this._timeToPixel;
|
||||
@ -307,7 +175,7 @@ DOM.defineCustomElement('view/timeline/timeline-track',
|
||||
node = DOM.div('chunk');
|
||||
node.onmousemove = this._chunkMouseMoveHandler;
|
||||
node.onclick = this._chunkClickHandler;
|
||||
node.ondblclick = this.chunkDoubleClickHandler;
|
||||
node.ondblclick = this._chunkDoubleClickHandler;
|
||||
}
|
||||
const style = node.style;
|
||||
style.left = `${((chunk.start - start) * this._timeToPixel) | 0}px`;
|
||||
@ -352,26 +220,28 @@ DOM.defineCustomElement('view/timeline/timeline-track',
|
||||
this.redraw();
|
||||
}
|
||||
|
||||
handleChunkMouseMove(event) {
|
||||
_handleChunkMouseMove(event) {
|
||||
if (this.isLocked) return false;
|
||||
if (this._isSelecting) return false;
|
||||
if (this._selectionHandler.isSelecting) return false;
|
||||
let chunk = event.target.chunk;
|
||||
if (!chunk) return;
|
||||
if (chunk.isEmpty()) return;
|
||||
// topmost map (at chunk.height) == map #0.
|
||||
let relativeIndex =
|
||||
Math.round(event.layerY / event.target.offsetHeight * chunk.size());
|
||||
let relativeIndex = Math.round(
|
||||
event.layerY / event.target.offsetHeight * (chunk.size() - 1));
|
||||
let logEntry = chunk.at(relativeIndex);
|
||||
this.dispatchEvent(new FocusEvent(logEntry));
|
||||
this.dispatchEvent(new ToolTipEvent(logEntry.toString(), event.target));
|
||||
}
|
||||
|
||||
handleChunkClick(event) {
|
||||
_handleChunkClick(event) {
|
||||
this.isLocked = !this.isLocked;
|
||||
}
|
||||
|
||||
handleChunkDoubleClick(event) {
|
||||
_handleChunkDoubleClick(event) {
|
||||
let chunk = event.target.chunk;
|
||||
if (!chunk) return;
|
||||
event.stopPropagation();
|
||||
this.dispatchEvent(new SelectTimeEvent(chunk.start, chunk.end));
|
||||
}
|
||||
|
||||
@ -391,13 +261,14 @@ DOM.defineCustomElement('view/timeline/timeline-track',
|
||||
if (!this.selectedEntry || !this.selectedEntry.edge) return;
|
||||
this.drawEdges(ctx);
|
||||
}
|
||||
|
||||
setMapStyle(map, ctx) {
|
||||
ctx.fillStyle = map.edge && map.edge.from ? CSSColor.onBackgroundColor :
|
||||
CSSColor.onPrimaryColor;
|
||||
}
|
||||
|
||||
setEdgeStyle(edge, ctx) {
|
||||
let color = this.typeToColor(edge.type);
|
||||
let color = this._typeToColor.get(edge.type);
|
||||
ctx.strokeStyle = color;
|
||||
ctx.fillStyle = color;
|
||||
}
|
||||
@ -495,7 +366,7 @@ DOM.defineCustomElement('view/timeline/timeline-track',
|
||||
ctx.lineTo(centerX + offsetX, centerY - labelOffset);
|
||||
ctx.stroke();
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillStyle = this.typeToColor(edge.type);
|
||||
ctx.fillStyle = this._typeToColor.get(edge.type);
|
||||
ctx.fillText(
|
||||
edge.toString(), centerX + offsetX + 2, centerY - labelOffset);
|
||||
}
|
||||
@ -515,3 +386,171 @@ DOM.defineCustomElement('view/timeline/timeline-track',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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(e) {
|
||||
let xPosition = e.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(e) {
|
||||
if (!this.isSelecting) return;
|
||||
const currentTime = this.positionToTime(e.clientX);
|
||||
this._timeline.dispatchEvent(new SynchronizeSelectionEvent(
|
||||
Math.min(this._selectionOriginTime, currentTime),
|
||||
Math.max(this._selectionOriginTime, currentTime)));
|
||||
}
|
||||
|
||||
_handleTimeSelectionMouseUp(e) {
|
||||
this._selectionOriginTime = -1;
|
||||
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;
|
||||
|
||||
constructor(table) {
|
||||
this._table = table;
|
||||
}
|
||||
|
||||
set timeline(timeline) {
|
||||
this._timeline = timeline;
|
||||
}
|
||||
|
||||
update() {
|
||||
const tbody = DOM.tbody();
|
||||
const selection = this._timeline.selection ?? this._timeline;
|
||||
const length = selection.length;
|
||||
selection.getBreakdown().forEach(breakdown => {
|
||||
const color = CSSColor.at(breakdown.id);
|
||||
let colorDiv;
|
||||
if (color !== null) {
|
||||
colorDiv = DOM.div('colorbox');
|
||||
colorDiv.style.backgroundColor = color;
|
||||
}
|
||||
let percent = `${(breakdown.count / length * 100).toFixed(1)}%`;
|
||||
tbody.appendChild(
|
||||
this.row(colorDiv, breakdown.type, breakdown.count, percent));
|
||||
});
|
||||
tbody.appendChild(this.row('', 'Selection', length, '100%'));
|
||||
tbody.appendChild(this.row('', 'All', this._timeline.length, '100%'));
|
||||
this._table.tBodies[0].replaceWith(tbody);
|
||||
}
|
||||
|
||||
row(color, type, count, percent) {
|
||||
const row = DOM.tr();
|
||||
row.appendChild(DOM.td(color));
|
||||
row.appendChild(DOM.td(type));
|
||||
row.appendChild(DOM.td(count));
|
||||
row.appendChild(DOM.td(percent));
|
||||
return row
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user