blob: 64be036276eb5b267091f424c5d02fa80600a007 [file] [log] [blame]
import '@google-web-components/google-chart';
import { DataFrame } from '../json';
import { DataTable } from './dataframe_context';
import { formatSpecialFunctions as formatSpecialFunctions } from '../paramtools';
export const labelKeys = [
'master',
'bot',
'benchmark',
'improvement_direction',
'stat',
'unit',
'test',
'subtest_1',
'subtest_2',
'subtest_3',
'subtest_4',
'subtest_5',
'subtest_6',
];
/**
* getAttributes extracts the attributes from the traceKeys
* The attributes are the keys to the trace.
* i.e. benchmark, test, story, subtest.
*
* The dataframe can have inconsistent attributes from one
* trace to the other. This function gets all unique attributes
* across every trace.
*/
// TODO(b/362831653): Create or modify this function to match
// the functionality of fromKey(): Params.
export const getAttributes = (df: DataFrame): string[] => {
const traceKeys = Object.keys(df.traceset);
const pairs = convertKeysToPairs(traceKeys);
const attributes = pairs.map((p) => p.split('=')[0]);
return Array.from(new Set(attributes));
};
/**
* getTitle converts the traceKeys into the title.
* The title is all common traceKey key-value pairs
* @param df The dataframe
* @returns title as a hashmap with (key, value) = (traceKey, attribute)
*
* @example
* a df with keys = [
* ",benchmark=JetStream2,story=Total,test=avg",
* ",benchmark=JetStream2,story=Air,test=std"
* ]
* title = {
* "benchmark": "JetStream2"
* }
*/
// One option with getTitle and getLegend is to create separate versions
// for DataTable and DataFrame. They would use the same logic. The only
// difference is getting the traceKeys:
// const traceKeys = Object.keys(df!.traceset);
// Just depends on whether explore-simple-sk needs to invoke this logic
export const getTitle = (dt: DataTable): object => {
const numCols = dt!.getNumberOfColumns();
const traceKeys: string[] = [];
// skip the first two columns since they are domains
for (let i = 2; i < numCols; i++) {
traceKeys.push(dt!.getColumnLabel(i));
}
const pairs = convertKeysToPairs(traceKeys);
// filters key-value pairs that appear across all keys
// assumes that key-value pairs cannot appear more than once
// within the same traceKey
const commonPairs = pairs.filter((pair, _, arr) => {
const count = arr.filter((x) => x === pair).length;
return count === traceKeys.length;
});
return Object.fromEntries(
[...new Set(commonPairs)].map((value) => {
return value.split('=');
})
);
};
/**
* getLegend converts the traceKeys into the legend.
* The legend is any element that is not common across all traceKeys
* @param df The dataframe
* @returns legend as a hashmap array with (key, value) = (traceKey, attribute)
* Each entry belongs to the corresponding trace
*
* @example
* a df with keys = [
* ",benchmark=JetStream2,story=Total,test=avg",
* ",benchmark=JetStream2,story=Air,test=std"
* ",benchmark=JetStream2,story=Air"
* ]
* legend = [{
* "story":"Total",
* "test":"avg",
* }, {
* "story":"Air",
* "test":"std",
* }, {
* "story":"Air",
* "test":"untitled_key",
* }]
*/
export const getLegend = (dt: DataTable): object[] => {
const numCols = dt!.getNumberOfColumns();
const traceKeys = [];
// skip the first two columns since they are domains
for (let i = 2; i < numCols; i++) {
const k = dt!.getColumnLabel(i);
const formattedKey = formatSpecialFunctions(k);
// There are special traces with no data, only "special_zero" as the trace name.
// They do not show any data on the graph, so we remove them.
if (formattedKey !== 'special_zero') {
traceKeys.push(formattedKey);
}
}
const pairs = convertKeysToPairs(traceKeys);
const uniqKVP = [];
// for each key, split the traceKey into key-value pairs
// and filter for entries that do not appear across all traceKeys
if (traceKeys.length >= 1) {
for (const key of traceKeys) {
const kvp = key.split(',').filter((pair) => pair.length);
const uniq = kvp.filter((item) => pairs.filter((x) => x === item).length < traceKeys.length);
uniqKVP.push(
Object.fromEntries(
uniq.map((item) => {
return item.split('=');
})
)
);
}
}
// Get all unique keys from the objects in the array
const allKeys = new Set(uniqKVP.flatMap(Object.keys));
// Fill in missing or blank keys in the map with empty string
const legend = uniqKVP.map((obj) => {
const newObj: { [key: string]: string } = {};
Array.from(allKeys)
.sort() // Sort the keys alphabetically
.forEach((key) => {
newObj[key] = obj[key] ? obj[key] : 'untitled_key';
});
return newObj;
});
return legend;
};
/**
* converts traceKeys to a list of individual key-value pairs and
* removing any trailing spaces and commas. If any of the trace keys
* are special function keys like norm(,a=A,b=B,c=C,), extract out
* the actual key ,a=A,b=B,c=C, from the trace name.
*/
function convertKeysToPairs(traceKeys: string[]) {
return traceKeys
.map((key) => {
const formattedKey = formatSpecialFunctions(key);
return formattedKey.split(',').filter((pair) => pair.length);
})
.flat();
}
/**
* titleFormatter converts the getTitle dictionary into a string
* @param df The output of getTitle()
* @returns Title with extra attributes omitted
*
* @example
* title = {
* "benchmark": "JetStream2",
* "story": "Box2D",
* }
* returns "JetStream2/Box2D"
*/
export function titleFormatter(title: object): string {
return Object.values(title).join('/');
}
/**
* legendFormatter converts the getLegend dictionary into a string
* @param df The output of getLegend()
* @returns Legend with extra attributes omitted
*
* @example
* legend = [{
* "story":"Total",
* "test":"avg"
* }, {
* "story":"Air"
* "test":"std"
* }]
* returns ["Total/avg", "Air/std"]
*/
export function legendFormatter(legend: object[]): string[] {
return legend.map((entry) =>
Object.values(entry)
.filter((value) => value)
.join('/')
);
}
/**
* getLegendKeysTitle converts a label from Google chart into a string that combines all legends key
* @returns Title with extra keys omitted
*
* @example
* legend = {
* "subtest_1": "fencedframe",
* "subtest_2": "PageLoad.Clients",
* }
* returns "subtest_1/subtest_2"
*/
export function getLegendKeysTitle(label: object): string {
return Object.keys(label).join('/');
}
/**
* isSingleTrace identifies if there is only one trace in the Dataframe
* @param df: Dataframe or undefined dataframe
* @returns: null if undefined dataframe. Otherwise true/false.
*/
export function isSingleTrace(dt: DataTable | undefined): boolean | null {
if (!dt) {
return null;
}
// first two cols are domains (commit position / date)
return dt!.getNumberOfColumns() === 3;
}
/**
* updateTraceByLegend function identifies the trace's last value
* and sets the visibility of the specified property
* based on the checkbox's checked state
* @param dt: DataTable or undefined dataframe
* @param legendTraceId: legend value from side panel
* @param isChecked: checkbox checked state
*/
export function findTraceByLabel(dt: DataTable | undefined, legendTraceId: string): string | null {
if (!dt) {
return null;
}
const numCols = dt!.getNumberOfColumns();
// skip the first two columns since they are domains (commit position / date)
for (let i = 2; i < numCols; i++) {
const label = dt!.getColumnLabel(i);
if (legendTraceId === label) {
return label;
}
}
return null;
}
/**
* Finds traces that contain the given key values param pair.
* @param dt DataTable or undefined dataframe
* @param paramKey Param key
* @param paramValues Param values
* @returns List of trace labels containing the key value pair.
*/
export function findTracesForParam(
dt: DataTable | undefined,
paramKey: string,
paramValues: string[]
): string[] | null {
if (!dt) {
return null;
}
// A matching trace will contain the key value pair in the form 'key=value'
const expectedLabels: string[] = [];
paramValues.forEach((paramValue) => {
const expectedLabelContent = ',' + paramKey + '=' + paramValue + ',';
expectedLabels.push(expectedLabelContent);
});
const numCols = dt!.getNumberOfColumns();
const traces: string[] = [];
// skip the first two columns since they are domains (commit position / date)
for (let i = 2; i < numCols; i++) {
const label = dt!.getColumnLabel(i);
expectedLabels.forEach((expectedLabelContent) => {
if (label.includes(expectedLabelContent)) {
traces.push(label);
}
});
}
return traces;
}