blob: d2196f1a0eaa61de7ff3178f3d8b1811d72b1720 [file] [log] [blame]
// Vue component for drop-down menu; here the metrics,
// stories and diagnostics are chosen through selection.
'use strict';
const app = new Vue({
el: '#app',
data: {
state: {
parsedMetrics: [],
gridData: [],
typesOfPlot: ['Cumulative frequency plot', 'Dot plot'],
chosenTypeOfPlot: null,
searchQuery: '',
currentState: true,
},
sampleArr: [],
guidValue: null,
graph: new GraphData(),
gridColumns: ['metric'],
columnsForChosenDiagnostic: null,
defaultGridData: [],
stateManager: new StateManager(),
},
methods: {
// Reset the table content by returning to the
// previous default way with all the components
// available.
resetTableData() {
this.state.typesOfPlot = ['Cumulative frequency plot', 'Dot plot'];
this.state.gridData = this.defaultGridData;
},
// Get all stories for a specific metric.
getStoriesByMetric(entry) {
const stories = [];
for (const e of this.sampleArr) {
if (e.name !== entry) {
continue;
}
let nameOfStory = this.guidValue.get(e.diagnostics.stories);
if (nameOfStory === undefined) {
continue;
}
if (typeof nameOfStory !== 'number') {
nameOfStory = nameOfStory[0];
}
stories.push(nameOfStory);
}
return _.uniq(stories);
},
// Extract a diagnostic from a specific
// element (like metric). This should be 'parsed' because
// sometimes it might be either a number or a
// single element array.
getDiagnostic(elem, diagnostic) {
let currentDiagnostic = this.guidValue.
get(elem.diagnostics[diagnostic]);
if (currentDiagnostic === undefined) {
return undefined;
}
if (currentDiagnostic !== 'number') {
currentDiagnostic = currentDiagnostic[0];
}
return currentDiagnostic;
},
// This method creates an object for multiple metrics,
// multiple stories and some diagnostics:
// {labelName: {storyName: { metricName: sampleValuesArray}}}
computeDataForStackPlot(metricsDependingOnGrid,
storiesName, labelsName) {
const obj = {};
for (const elem of metricsDependingOnGrid) {
const currentDiagnostic = this.getDiagnostic(elem, 'labels');
if (currentDiagnostic === undefined) {
continue;
}
const nameOfStory = this.getDiagnostic(elem, 'stories');
if (nameOfStory === undefined) {
continue;
}
if (storiesName.includes(nameOfStory) &&
labelsName.includes(currentDiagnostic)) {
let storyToMetricValues = {};
if (obj.hasOwnProperty(currentDiagnostic)) {
storyToMetricValues = obj[currentDiagnostic];
}
let metricToSampleValues = {};
if (storyToMetricValues.hasOwnProperty(nameOfStory)) {
metricToSampleValues = storyToMetricValues[nameOfStory];
}
let array = [];
if (metricToSampleValues.hasOwnProperty(elem.name)) {
array = metricToSampleValues[elem.name];
}
array = array.concat(average(elem.sampleValues));
metricToSampleValues[elem.name] = array;
storyToMetricValues[nameOfStory] = metricToSampleValues;
obj[currentDiagnostic] = storyToMetricValues;
}
}
return obj;
},
// Draw a bar chart.
plotBarChart(data) {
this.pushCurrentState();
this.graph.xAxis('Story')
.yAxis('Memory used (MiB)')
.title('Labels')
.setData(data, story => app.$emit('bar_clicked', story))
.plotBar();
},
// Draw a dot plot depending on the target value.
// This is mainly for results from the table.
plotDotPlot(target, story, traces) {
this.pushCurrentState();
const openTrace = (label, index) => {
window.open(traces[label][index]);
};
this.graph
.yAxis('')
.xAxis('Memory used (MiB)')
.title(story)
.setData(target, openTrace)
.plotDot();
},
// Draw a cumulative frequency plot depending on the target value.
// This is mainly for the results from the table.
plotCumulativeFrequencyPlot(target, story, traces) {
const openTrace = (label, index) => {
window.open(traces[label][index]);
};
this.pushCurrentState();
this.graph.yAxis('Cumulative frequency')
.xAxis('Memory used (MiB)')
.title(story)
.setData(target, openTrace)
.plotCumulativeFrequency();
},
plotStackBar(obj, title) {
this.pushCurrentState();
this.graph.xAxis('Stories')
.yAxis('Memory used (MiB)')
.title(title)
.setData(obj, metric => app.$emit('stack_clicked', metric))
.plotStackedBar();
},
/**
* Pushes the current state of data being displayed onto a stack to
* be retrieved when an undo occurs.
*/
pushCurrentState() {
// Only states just popped of the stack have current state set to
// false and should not be pushed back on.
if (this.state.currentState) {
// The state that is pushed is not current and should be marked
// as such so that it is not pushed back onto the stack when an
// undo occurs and the graph is plotted.
this.state.currentState = false;
// Deep clone the state objects so that their state is saved
// and not mutated by future changes in Vue's state object.
const clone = obj => JSON.parse(JSON.stringify(obj));
this.stateManager.pushState({
app: clone(this.state),
menu: clone(menu.state),
table: clone(this.$refs.tableComponent.state),
});
}
this.state.currentState = true;
},
/**
* Pops the previous state from the top of the stack and amends the
* state objects of the vue components. This will trigger the
* affected watchers and cause the graph to be plotted again based
* on the retrieved data.
*/
undo() {
const savedState = this.stateManager.popState();
this.replaceState(this.state, savedState.app);
this.replaceState(menu.state, savedState.menu);
this.replaceState(this.$refs.tableComponent.state, savedState.table);
},
/**
* Checks if the state manager has any state saved onto the stack.
*/
hasHistory() {
return this.stateManager.hasHistory();
},
replaceState(oldState, newState) {
Object.keys(oldState).forEach((key) => {
// Only replace properties which have actually changed to
// avoid triggering watchers for data which has not really
// changed.
if (!_.isEqual(oldState[key], newState[key])) {
oldState[key] = newState[key];
}
});
},
// Being given a metric, a story, a diagnostic and a set of
// subdiagnostics (i.e. 3 labels from the total of 4), the
// method return the sample values for each subdiagnostic.
getSubdiagnostics(
getTargetValueFromSample, metric, story, diagnostic, diagnostics) {
const result = this.sampleArr
.filter(value => value.name === metric &&
this.guidValue
.get(value.diagnostics.stories)[0] ===
story);
const content = new Map();
for (const val of result) {
const diagnosticItem = this.guidValue.get(
val.diagnostics[diagnostic]);
if (diagnosticItem === undefined) {
continue;
}
let currentDiagnostic = '';
if (typeof diagnosticItem === 'number') {
currentDiagnostic = diagnosticItem;
} else {
currentDiagnostic = diagnosticItem[0];
}
const targetValue = getTargetValueFromSample(val);
if (content.has(currentDiagnostic)) {
const aux = content.get(currentDiagnostic);
content.set(currentDiagnostic, aux.concat(targetValue));
} else {
content.set(currentDiagnostic, targetValue);
}
}
const obj = {};
for (const [key, value] of content.entries()) {
if (diagnostics === undefined ||
diagnostics.includes(key.toString())) {
obj[key] = value;
}
}
return obj;
},
getSampleValues(sample) {
const toMiB = (x) => (x / MiB).toFixed(5);
const values = sample.sampleValues;
return values.map(value => toMiB(value));
},
getTraceLinks(sample) {
const traceId = sample.diagnostics.traceUrls;
return this.guidValue.get(traceId);
},
// Draw a plot by default with all the sub-diagnostics
// in the same plot;
plotSingleMetricWithAllSubdiagnostics(metric, story, diagnostic) {
const obj = this.getSubdiagnostics(
this.getSampleValues, metric, story, diagnostic);
const traces = this.targetForMultipleDiagnostics(
this.getTraceLinks, metric, story, diagnostic);
this.plotCumulativeFrequencyPlot(obj, story, traces);
},
// Draw a plot depending on the target value which is made
// of a metric, a story, a diagnostic and a couple of sub-diagnostics
// and the chosen type of plot. All are chosen from the table.
plotSingleMetric(metric, story, diagnostic,
diagnostics, chosenPlot) {
this.state.chosenTypeOfPlot = chosenPlot;
const target = this.targetForMultipleDiagnostics(
this.getSampleValues, metric, story, diagnostic, diagnostics);
const traces = this.targetForMultipleDiagnostics(
this.getTraceLinks, metric, story, diagnostic, diagnostics);
if (chosenPlot === 'Dot plot') {
this.plotDotPlot(target, story, traces);
} else {
this.plotCumulativeFrequencyPlot(target, story, traces);
}
},
// Compute the target when the metric, story, diagnostics and
// sub-diagnostics are chosen from the table, not from the drop-down menu.
// It should be the same for both components but for now they should
// be divided.
targetForMultipleDiagnostics(
getTargetValueFromSample, metric, story, diagnostic, diagnostics) {
if (metric === null || story === null ||
diagnostic === null || diagnostics === null) {
return undefined;
}
return this.getSubdiagnostics(
getTargetValueFromSample, metric, story, diagnostic, diagnostics);
}
},
computed: {
gridDataLoaded() {
return this.state.gridData.length > 0;
},
data_loaded() {
return this.sampleArr.length > 0;
}
},
watch: {
// Whenever we have new inputs from the menu (parsed inputs that
// where obtained by choosing from the tree) these should be
// added in the table (adding the average sample value).
// Also it creates by default a stack plot for all the metrics
// obtained from the tree-menu, all the stories from the top-level
// metric and all available labels.
'state.parsedMetrics'() {
if (this.state.parsedMetrics.length === 0) {
// The menu has no metrics selected (the default state) so show
// the entire table.
this.resetTableData();
return;
}
const newGridData = [];
for (const metric of this.state.parsedMetrics) {
for (const elem of this.defaultGridData) {
if (elem.metric === metric) {
newGridData.push(elem);
}
}
}
this.state.gridData = newGridData;
// We select from sampleValues all the metrics thath
// corespond to the result from tree menu (gridData)
const metricsDependingOnGrid = [];
const gridMetricsName = [];
for (const metric of this.state.gridData) {
gridMetricsName.push(metric.metric);
}
for (const metric of this.sampleArr) {
if (gridMetricsName.includes(metric.name)) {
metricsDependingOnGrid.push(metric);
}
}
// The top level metric is taken as source in
// computing stories.
const storiesName =
this.getStoriesByMetric(this.state.gridData[0].metric);
const labelsName = this.columnsForChosenDiagnostic;
const obj = this.computeDataForStackPlot(metricsDependingOnGrid,
storiesName, labelsName);
this.plotStackBar(obj, newGridData[0].metric);
// From now on the user will be able to switch between
// this 2 types of plot (taking into consideration that
// the scope of the tree-menu is to analyse using the
// the stacked plot and bar plot, we avoid for the moment
// other types of plot that should be actually used without
// using the tree menu)
this.state.typesOfPlot = ['Bar chart plot', 'Stacked bar plot',
'Cumulative frequency plot', 'Dot plot'];
this.state.chosenTypeOfPlot = 'Stacked bar plot';
}
}
});