blob: f1f872e40855e7760b9c4dd71a703f3eb0f796b5 [file] [log] [blame]
/**
* @module modules/pivot-table-sk
* @description <h2><code>pivot-table-sk</code></h2>
*
* Displays a DataFrame that has been pivoted and contains summary values (as
* opposed to a DataFrame that has been pivoted and contains summary traces).
* These values are displayed in a table, as opposed to being displayed on in a
* plot.
*
* The inputs required are a DataFrame and a pivot.Request, which has details on
* how the input DataFrame was pivoted.
*
* @evt Emits a change event with the sort history encoded as a string when the
* user sorts on a column.
*/
import { define } from 'elements-sk/define';
import { html, TemplateResult } from 'lit-html';
import { toParamSet } from 'common-sk/modules/query';
import { ElementSk } from '../../../infra-sk/modules/ElementSk';
import { pivot, DataFrame, TraceSet } from '../json';
import { operationDescriptions, validateAsPivotTable } from '../pivotutil';
import '../../../infra-sk/modules/paramset-sk';
import 'elements-sk/icon/sort-icon-sk';
import 'elements-sk/icon/arrow-drop-down-icon-sk';
import 'elements-sk/icon/arrow-drop-up-icon-sk';
import { fromKey } from '../paramtools';
/** The direction a column is sorted in. */
export type direction = 'up' | 'down';
/** The different kinds of columns. */
export type columnKind = 'keyValues' | 'summaryValues';
/** Type for a function that can be passed to Array.sort(). */
export type compareFunc = (a: string, b: string)=> number;
/** For each key in a traceset, this stores the values for key,value pair in the
* traceid that appear in pivot.Request.group_by, and holds them in the order as
* determined by pivot.Request.group_by.
*/
export type KeyValues = {[key: string]: string[]};
// The event detail is the sort history of the table encoded as a string.
export type PivotTableSkChangeEventDetail = string;
/** Represents a how a single column in the table is to be sorted.
*/
export class SortSelection {
// The column to sort on, the value is interpreted differently based
// on the value of this.sortKind:
//
// If this.kind === 'keyValues' then it is an index into the keyValues.
// If this.Kind === 'summaryValues' then it is an index into the trace values.
column: number = 0;
kind: columnKind = 'summaryValues';
dir: direction = 'up';
constructor(column: number, kind: columnKind, dir: direction) {
this.column = column;
this.kind = kind;
this.dir = dir;
}
toggleDirection(): void {
if (this.dir === 'down') {
this.dir = 'up';
} else {
this.dir = 'down';
}
}
/** Returns a compareFunc that sorts based on the state of this SortSelection.
*/
buildCompare(traceset: TraceSet, keyValues: {[key: string]: string[]}): compareFunc {
const compare = (a: string, b: string): number => {
let ret = 0;
if (this.kind === 'keyValues') {
const aString = keyValues[a][this.column];
const bString = keyValues[b][this.column];
if (aString < bString) {
ret = -1;
} else if (bString < aString) {
ret = 1;
} else {
return 0;
}
} else {
ret = traceset[a][this.column] - traceset[b][this.column];
}
if (this.dir === 'down') {
ret = -ret;
}
return ret;
};
return compare;
}
/** Encodes the SortSelection as a string. */
encode(): string {
const encodedDir = this.dir === 'up' ? 'u' : 'd';
const encodedKind = this.kind === 'keyValues' ? 'k' : 's';
return `${encodedDir}${encodedKind}${this.column}`;
}
/** Decode an encoded SortSelection from a string encoded by
* SortSelection.encode(). */
static decode(s: string): SortSelection {
const dir = s[0] === 'u' ? 'up' : 'down';
const kind: columnKind = s[1] === 'k' ? 'keyValues' : 'summaryValues';
const column = +s.slice(2);
return new SortSelection(column, kind, dir);
}
}
/**
* Keeps one SortSelection for each column being displayed. As the user clicks
* on columns the function `selectColumnToSortOn` can be called to keep
* `this.history` up to date.
*
* This enables better sorting behavior, i.e. when you click on col A to sort,
* then on col B to sort, if there are ties in col B they are broken by the
* existing order in col A, just like you would get when sorting by columns in a
* spreadsheet.
*
* This is not technically 'stable sort', while each sort action by the user
* looks like it is doing a stable sort, which is the goal, we are really doing
* an absolute sort based on a memory of all previous sort actions.
*/
export class SortHistory {
/** Columns will be sorted by the first entry in history. If that yields a
* tie, then the second entry in history will be used to break the tie, etc.
*/
history: SortSelection[] = []
constructor(numGroupBy: number, numSummaryValues: number) {
for (let i = 0; i < numSummaryValues; i++) {
this.history.push(new SortSelection(i, 'summaryValues', 'up'));
}
for (let i = 0; i < numGroupBy; i++) {
this.history.push(new SortSelection(i, 'keyValues', 'up'));
}
}
/** Moves the selected column to the front of the list for sorting, and also
* reverses its current direction.
*/
selectColumnToSortOn(column: number, kind: columnKind): void {
// Remove the matching SortSelection from history.
let removed: SortSelection[] = [];
for (let i = 0; i < this.history.length; i++) {
if (column === this.history[i].column && kind === this.history[i].kind) {
removed = this.history.splice(i, 1);
break;
}
}
// Toggle its direction.
removed[0].toggleDirection();
// Then add back to the beginning of the list.
this.history.unshift(removed[0]);
}
/** Returns a compareFunc that sorts based on the state of all the
* SortSelections in history.
*/
buildCompare(traceset: TraceSet, keyValues: {[key: string]: string[]}): compareFunc {
const compares = this.history.map((sel: SortSelection) => sel.buildCompare(traceset, keyValues));
const compare = (a: string, b: string): number => {
let ret = 0;
// Call each compareFunc in `compares` until one of them produces a
// non-zero result. If all calls return 0 then this compare function also
// returns 0.
compares.some((colCompare: compareFunc) => {
ret = colCompare(a, b);
return ret;
});
return ret;
};
return compare;
}
/** Encodes the SortHistory as a string.
*
* The format is of all the serialized history members joined by
* dashes.
*/
encode(): string {
return this.history.map((sel: SortSelection) => sel.encode()).join('-');
}
/** Decodes a string previously encoded via this.encode() and uses it to set
* the history state. */
decode(s: string): void {
this.history = s.split('-').map((encodedSortSelection: string) => SortSelection.decode(encodedSortSelection));
}
}
export function keyValuesFromTraceSet(traceset: TraceSet, req: pivot.Request): KeyValues {
const ret: KeyValues = {};
Object.keys(traceset).forEach((traceKey) => {
// Parse the key.
const ps = fromKey(traceKey);
// Store the values for each key in group_by order.
ret[traceKey] = req.group_by!.map((colName) => ps[colName]);
});
return ret;
}
export class PivotTableSk extends ElementSk {
private df: DataFrame | null = null;
private req: pivot.Request | null = null;
private query: string = ''
/** Maps each traceKey to a list of the values for each key in the traceID,
* where the order is determined by this.req.group_by.
*
* That is ',arch=arm,config=8888,' maps to ['8888', 'arm'] if
* this.req.group_by is ['config', 'arch'].
* */
private keyValues: KeyValues = {}
private sortHistory: SortHistory | null = null;
// The comparison function to use to sort the table.
private compare: compareFunc | null = null;
constructor() {
super(PivotTableSk.template);
}
private static template = (ele: PivotTableSk) => {
const invalidMessage = validateAsPivotTable(ele.req);
if (invalidMessage) {
return html`<h2>Cannot display: ${invalidMessage}</h2>`;
}
if (!ele.df) {
return html`<h2>Cannot display: Data is missing.</h2>`;
}
return html`
${ele.queryDefinition()}
<table>
${ele.tableHeader()}
${ele.tableRows()}
</table>`;
}
connectedCallback(): void {
super.connectedCallback();
this._render();
}
set(df: DataFrame, req: pivot.Request, query: string, encodedHistory: string = ''): void {
this.df = df;
this.req = req;
this.query = query;
this.keyValues = keyValuesFromTraceSet(this.df.traceset, this.req);
this.sortHistory = new SortHistory(req.group_by!.length, req.summary!.length);
if (encodedHistory !== '') {
this.sortHistory.decode(encodedHistory);
}
this.compare = this.sortHistory.buildCompare(this.df.traceset, this.keyValues);
this._render();
}
private queryDefinition(): TemplateResult {
return html`
<div class=querydef>
<div>
<span class=title>Query</span>
<paramset-sk .paramsets=${[toParamSet(this.query)]}></paramset-sk>
</div>
<div>
<span class=title>Group by:</span>
${this.req!.group_by!.join(', ')}
</div>
<div>
<span class=title>Operation:</span>
${operationDescriptions[this.req!.operation]}
</div>
<div>
<span class=title>Summaries:</span>
${this.req!.summary!.map((op: pivot.Operation) => operationDescriptions[op]).join(', ')}
</div>
</div>`;
}
private tableHeader(): TemplateResult {
return html`
<tr>
${this.keyColumnHeaders()}
${this.summaryColumnHeaders()}
</tr>`;
}
private keyColumnHeaders(): TemplateResult[] {
return this.req!.group_by!.map((groupBy: string, index: number) => html`<th>${this.sortArrow(index, 'keyValues')} ${groupBy}</th>`);
}
private summaryColumnHeaders(): TemplateResult[] {
return this.req!.summary!.map((summaryOperation, index) => html`<th>${this.sortArrow(index, 'summaryValues')} ${operationDescriptions[summaryOperation]}</th>`);
}
private sortArrow(column: number, kind: columnKind): TemplateResult {
const firstSortSelection = this.sortHistory!.history[0];
if (firstSortSelection.kind === kind) {
if (column === firstSortSelection.column) {
if (firstSortSelection.dir === 'up') {
return html`<arrow-drop-up-icon-sk title="Change sort order to descending." @click=${() => this.changeSort(column, kind)}></arrow-drop-up-icon-sk>`;
}
return html`<arrow-drop-down-icon-sk title="Change sort order to ascending." @click=${() => this.changeSort(column, kind)}></arrow-drop-down-icon-sk>`;
}
}
return html`<sort-icon-sk title="Sort this column." @click=${() => this.changeSort(column, kind)}></sort-icon-sk>`;
}
private changeSort(column: number, kind: columnKind) {
this.sortHistory!.selectColumnToSortOn(column, kind);
this.compare = this.sortHistory!.buildCompare(this.df!.traceset, this.keyValues);
this.dispatchEvent(new CustomEvent<PivotTableSkChangeEventDetail>('change', { detail: this.sortHistory!.encode(), bubbles: true }));
this._render();
}
private tableRows(): TemplateResult[] {
const traceset = this.df!.traceset;
const sortedRowKeys = Object.keys(traceset).sort(this.compare!);
const ret: TemplateResult[] = [];
sortedRowKeys.forEach((key) => {
ret.push(html`<tr>${this.keyRowValues(key)}${this.summaryRowValues(key)}</tr>`);
});
return ret;
}
private keyRowValues(traceKey: string): TemplateResult[] {
return this.keyValues[traceKey].map((value) => html`<th class=key>${value}</th>`);
}
private summaryRowValues(key: string): TemplateResult[] {
return this.df!.traceset[key]!.map((value) => html`<td>${PivotTableSk.displayValue(value)}</td>`);
}
/** Converts vec32.MissingDataSentinel values into '-'. */
private static displayValue(value: number): string {
// TODO(jcgregorio) Have a common definition of vec32.MissingDataSentinel in
// TS and Go code.
if (value === 1e32) {
return '-';
}
return value.toPrecision(4);
}
}
define('pivot-table-sk', PivotTableSk);