Revert "Bugfix/legend data order"
parent
1da41c760a
commit
8067b32b17
ui
spec/utils
src/utils
|
@ -16,10 +16,6 @@ describe('timeSeriesToDygraph', () => {
|
||||||
"name":"m1",
|
"name":"m1",
|
||||||
"columns": ["time","f1"],
|
"columns": ["time","f1"],
|
||||||
"values": [[1000, 1],[2000, 2]],
|
"values": [[1000, 1],[2000, 2]],
|
||||||
"tags": {
|
|
||||||
tk1: "tv1",
|
|
||||||
tk2: "tv2",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -29,9 +25,6 @@ describe('timeSeriesToDygraph', () => {
|
||||||
"name":"m1",
|
"name":"m1",
|
||||||
"columns": ["time","f2"],
|
"columns": ["time","f2"],
|
||||||
"values": [[2000, 3],[4000, 4]],
|
"values": [[2000, 3],[4000, 4]],
|
||||||
"tags": {
|
|
||||||
tk3: "tv3",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -45,8 +38,8 @@ describe('timeSeriesToDygraph', () => {
|
||||||
const expected = {
|
const expected = {
|
||||||
labels: [
|
labels: [
|
||||||
'time',
|
'time',
|
||||||
`m1.f1[tk1=tv1][tk2=tv2]`,
|
`m1.f1`,
|
||||||
`m1.f2[tk3=tv3]`,
|
`m1.f2`,
|
||||||
],
|
],
|
||||||
timeSeries: [
|
timeSeries: [
|
||||||
[new Date(1000), 1, null],
|
[new Date(1000), 1, null],
|
||||||
|
@ -54,11 +47,11 @@ describe('timeSeriesToDygraph', () => {
|
||||||
[new Date(4000), null, 4],
|
[new Date(4000), null, 4],
|
||||||
],
|
],
|
||||||
dygraphSeries: {
|
dygraphSeries: {
|
||||||
'm1.f1[tk1=tv1][tk2=tv2]': {
|
'm1.f1': {
|
||||||
axis: 'y',
|
axis: 'y',
|
||||||
strokeWidth,
|
strokeWidth,
|
||||||
},
|
},
|
||||||
'm1.f2[tk3=tv3]': {
|
'm1.f2': {
|
||||||
axis: 'y',
|
axis: 'y',
|
||||||
strokeWidth,
|
strokeWidth,
|
||||||
},
|
},
|
||||||
|
@ -154,18 +147,18 @@ describe('timeSeriesToDygraph', () => {
|
||||||
const actual = timeSeriesToDygraph(influxResponse);
|
const actual = timeSeriesToDygraph(influxResponse);
|
||||||
|
|
||||||
const expected = {
|
const expected = {
|
||||||
'm1.f1': {
|
'm1.f1': {
|
||||||
axis: 'y',
|
axis: 'y',
|
||||||
strokeWidth,
|
strokeWidth,
|
||||||
},
|
},
|
||||||
'm1.f2': {
|
'm1.f2': {
|
||||||
axis: 'y',
|
axis: 'y',
|
||||||
strokeWidth,
|
strokeWidth,
|
||||||
},
|
},
|
||||||
'm3.f3': {
|
'm3.f3': {
|
||||||
axis: 'y2',
|
axis: 'y2',
|
||||||
strokeWidth,
|
strokeWidth,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(actual.dygraphSeries).to.deep.equal(expected);
|
expect(actual.dygraphSeries).to.deep.equal(expected);
|
||||||
|
@ -213,19 +206,19 @@ describe('timeSeriesToDygraph', () => {
|
||||||
labels: [
|
labels: [
|
||||||
'time',
|
'time',
|
||||||
`m1.f1`,
|
`m1.f1`,
|
||||||
`m1.f1`,
|
`m1.f1-1`,
|
||||||
],
|
],
|
||||||
timeSeries: [
|
timeSeries: [
|
||||||
[new Date(1000), 1, null],
|
[new Date(1000), 1, null],
|
||||||
[new Date(2000), 2, 3],
|
[new Date(2000), 2, 3],
|
||||||
[new Date(4000), null, 4],
|
[new Date(4000), 4, null],
|
||||||
],
|
],
|
||||||
dygraphSeries: {
|
dygraphSeries: {
|
||||||
'm1.f1': {
|
'm1.f1': {
|
||||||
axis: 'y',
|
axis: 'y',
|
||||||
strokeWidth,
|
strokeWidth,
|
||||||
},
|
},
|
||||||
'm1.f1': {
|
'm1.f1-1': {
|
||||||
axis: 'y2',
|
axis: 'y2',
|
||||||
strokeWidth,
|
strokeWidth,
|
||||||
},
|
},
|
||||||
|
@ -329,7 +322,7 @@ describe('timeSeriesToDygraph', () => {
|
||||||
expect(dygraphSeries["m2.f2"].strokeWidth).to.be.above(dygraphSeries["m1.f1"].strokeWidth);
|
expect(dygraphSeries["m2.f2"].strokeWidth).to.be.above(dygraphSeries["m1.f1"].strokeWidth);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parses labels alphabetically with the correct field values for multiple series', () => {
|
it('parses a raw InfluxDB response into a dygraph friendly data format', () => {
|
||||||
const influxResponse = [
|
const influxResponse = [
|
||||||
{
|
{
|
||||||
"response":
|
"response":
|
||||||
|
@ -339,22 +332,8 @@ describe('timeSeriesToDygraph', () => {
|
||||||
"series": [
|
"series": [
|
||||||
{
|
{
|
||||||
"name":"mb",
|
"name":"mb",
|
||||||
"columns": ["time","fa"],
|
"columns": ["time","f1"],
|
||||||
"values": [
|
"values": [[1000, 1],[2000, 2]],
|
||||||
[1000, 200],
|
|
||||||
[2000, 300],
|
|
||||||
[4000, 400],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name":"mc",
|
|
||||||
"columns": ["time","fa"],
|
|
||||||
"values": [
|
|
||||||
[1000, 400],
|
|
||||||
[2000, 600],
|
|
||||||
[3000, 800],
|
|
||||||
[5000, 1000],
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -362,12 +341,26 @@ describe('timeSeriesToDygraph', () => {
|
||||||
"series": [
|
"series": [
|
||||||
{
|
{
|
||||||
"name":"ma",
|
"name":"ma",
|
||||||
"columns": ["time","fa","fc","fb"],
|
"columns": ["time","f1"],
|
||||||
"values": [
|
"values": [[1000, 1],[2000, 2]],
|
||||||
[1000, 20, 10, 10],
|
},
|
||||||
[2000, 30, 15, 9],
|
]
|
||||||
[3000, 40, 20, 8],
|
},
|
||||||
],
|
{
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name":"mc",
|
||||||
|
"columns": ["time","f2"],
|
||||||
|
"values": [[2000, 3],[4000, 4]],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name":"mc",
|
||||||
|
"columns": ["time","f1"],
|
||||||
|
"values": [[2000, 3],[4000, 4]],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -378,25 +371,14 @@ describe('timeSeriesToDygraph', () => {
|
||||||
|
|
||||||
const actual = timeSeriesToDygraph(influxResponse);
|
const actual = timeSeriesToDygraph(influxResponse);
|
||||||
|
|
||||||
const expected = {
|
const expected = [
|
||||||
labels: [
|
'time',
|
||||||
'time',
|
`ma.f1`,
|
||||||
`ma.fa`,
|
`mb.f1`,
|
||||||
`ma.fb`,
|
`mc.f1`,
|
||||||
`ma.fc`,
|
`mc.f2`,
|
||||||
`mb.fa`,
|
];
|
||||||
`mc.fa`,
|
|
||||||
],
|
|
||||||
timeSeries: [
|
|
||||||
[new Date(1000), 20, 10, 10, 200, 400],
|
|
||||||
[new Date(2000), 30, 9, 15, 300, 600],
|
|
||||||
[new Date(3000), 40, 8, 20, null, 800],
|
|
||||||
[new Date(4000), null, null, null, 400, null],
|
|
||||||
[new Date(5000), null, null, null, null, 1000],
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(actual.labels).to.deep.equal(expected.labels);
|
expect(actual.labels).to.deep.equal(expected);
|
||||||
expect(actual.timeSeries).to.deep.equal(expected.timeSeries);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,113 +1,181 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import {STROKE_WIDTH} from 'src/shared/constants';
|
import {STROKE_WIDTH} from 'src/shared/constants';
|
||||||
/**
|
/**
|
||||||
* Accepts an array of raw influxdb responses and returns a format
|
* Accepts an array of raw influxdb responses and returns a format
|
||||||
* that Dygraph understands.
|
* that Dygraph understands.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// activeQueryIndex is an optional argument that indicated which query's series we want highlighted.
|
// activeQueryIndex is an optional argument that indicated which query's series
|
||||||
|
// we want highlighted.
|
||||||
export default function timeSeriesToDygraph(raw = [], activeQueryIndex, isInDataExplorer) {
|
export default function timeSeriesToDygraph(raw = [], activeQueryIndex, isInDataExplorer) {
|
||||||
// collect results from each influx response
|
const labels = []; // all of the effective field names (i.e. <measurement>.<field>)
|
||||||
const results = raw.reduce((acc, rawResponse, responseIndex) => {
|
const fieldToIndex = {}; // see parseSeries
|
||||||
const responses = _.get(rawResponse, 'response.results', []);
|
const dates = {}; // map of date as string to date value to minimize string coercion
|
||||||
const indexedResponses = responses.map((response) => ({...response, responseIndex}));
|
const dygraphSeries = {}; // dygraphSeries is a graph legend label and its corresponding y-axis e.g. {legendLabel1: 'y', legendLabel2: 'y2'};
|
||||||
return [...acc, ...indexedResponses];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// collect each series
|
/**
|
||||||
const serieses = results.reduce((acc, {series = [], responseIndex}, index) => {
|
* dateToFieldValue will look like:
|
||||||
return [...acc, ...series.map((item) => ({...item, responseIndex, index}))];
|
*
|
||||||
}, []);
|
* {
|
||||||
|
* Date1: {
|
||||||
|
* effectiveFieldName_1: ValueForField1AtDate1,
|
||||||
|
* effectiveFieldName_2: ValueForField2AtDate1,
|
||||||
|
* ...
|
||||||
|
* },
|
||||||
|
* Date2: {
|
||||||
|
* effectiveFieldName_1: ValueForField1AtDate2,
|
||||||
|
* effectiveFieldName_2: ValueForField2AtDate2,
|
||||||
|
* ...
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
const dateToFieldValue = {};
|
||||||
|
|
||||||
// convert series into cells with rows and columns
|
raw.forEach(({response}, queryIndex) => {
|
||||||
const cells = serieses.reduce((acc, {name, columns, values, index, responseIndex, tags = {}}) => {
|
// If a response is an empty result set or a query returned an error
|
||||||
const rows = values.map((vals) => ({
|
// from InfluxDB, don't try and parse.
|
||||||
name,
|
if (response.results.length) {
|
||||||
columns,
|
if (isEmpty(response) || hasError(response)) {
|
||||||
vals,
|
return;
|
||||||
index,
|
}
|
||||||
}));
|
}
|
||||||
|
|
||||||
// tagSet is each tag key and value for a series
|
/**
|
||||||
const tagSet = Object.keys(tags).map((tag) => `[${tag}=${tags[tag]}]`).sort().join('');
|
* response looks like:
|
||||||
|
* {
|
||||||
|
* results: [
|
||||||
|
* { series: [...] },
|
||||||
|
* { series: [...] },
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
response.results.forEach(parseResult);
|
||||||
|
|
||||||
rows.forEach(({vals, columns: cols, name: measurement, index: seriesIndex}) => {
|
function parseResult(s) {
|
||||||
const [time, ...rowValues] = vals;
|
/*
|
||||||
|
* s looks like:
|
||||||
|
* {
|
||||||
|
* series: [
|
||||||
|
* {
|
||||||
|
* name: "<measurement>",
|
||||||
|
* columns: ["time", "<field name 1>", "<field name 2>", ...],
|
||||||
|
* values: [<time>, <value of field 1>, <value of field 2>, ...],
|
||||||
|
* },
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
s.series.forEach(parseSeries);
|
||||||
|
}
|
||||||
|
|
||||||
rowValues.forEach((value, i) => {
|
function parseSeries(series) {
|
||||||
const field = cols[i + 1];
|
/*
|
||||||
acc.push({
|
* series looks like:
|
||||||
label: `${measurement}.${field}${tagSet}`,
|
* {
|
||||||
value,
|
* name: "<measurement>",
|
||||||
time,
|
* columns: ["time", "<field name 1>", "<field name 2>", ...],
|
||||||
seriesIndex,
|
* values: [
|
||||||
responseIndex,
|
* [<time1>, <value of field 1 @ time1>, <value of field 2 @ time1>, ...],
|
||||||
|
* [<time2>, <value of field 1 @ time2>, <value of field 2 @ time2>, ...],
|
||||||
|
* ]
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
const measurementName = series.name;
|
||||||
|
const columns = series.columns;
|
||||||
|
|
||||||
|
// Tags are only included in an influxdb response under certain circumstances, e.g.
|
||||||
|
// when a query is using GROUP BY (<tag key>).
|
||||||
|
const tags = Object.keys(series.tags || {}).map((key) => {
|
||||||
|
return `[${key}=${series.tags[key]}]`;
|
||||||
|
}).sort().join('');
|
||||||
|
|
||||||
|
columns.slice(1).forEach((fieldName) => {
|
||||||
|
let effectiveFieldName = `${measurementName}.${fieldName}${tags}`;
|
||||||
|
|
||||||
|
// If there are duplicate effectiveFieldNames identify them by their queryIndex
|
||||||
|
if (effectiveFieldName in dygraphSeries) {
|
||||||
|
effectiveFieldName = `${effectiveFieldName}-${queryIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a field name, identify which column in the timeSeries result should hold the field's value
|
||||||
|
// ex given this timeSeries [Date, 10, 20, 30] field index at 2 would correspond to value 20
|
||||||
|
fieldToIndex[effectiveFieldName] = labels.length + 1;
|
||||||
|
labels.push(effectiveFieldName);
|
||||||
|
|
||||||
|
const {light, heavy} = STROKE_WIDTH;
|
||||||
|
|
||||||
|
const dygraphSeriesStyles = {
|
||||||
|
strokeWidth: queryIndex === activeQueryIndex ? heavy : light,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isInDataExplorer) {
|
||||||
|
dygraphSeriesStyles.axis = queryIndex === 0 ? 'y' : 'y2';
|
||||||
|
}
|
||||||
|
|
||||||
|
dygraphSeries[effectiveFieldName] = dygraphSeriesStyles;
|
||||||
|
});
|
||||||
|
|
||||||
|
(series.values || []).forEach(parseRow);
|
||||||
|
|
||||||
|
function parseRow(row) {
|
||||||
|
/**
|
||||||
|
* row looks like:
|
||||||
|
* [<time1>, <value of field 1 @ time1>, <value of field 2 @ time1>, ...]
|
||||||
|
*/
|
||||||
|
const date = row[0];
|
||||||
|
const dateString = date.toString();
|
||||||
|
row.forEach((value, index) => {
|
||||||
|
if (index === 0) {
|
||||||
|
// index 0 in a row is always the timestamp
|
||||||
|
if (!dateToFieldValue[dateString]) {
|
||||||
|
dateToFieldValue[dateString] = {};
|
||||||
|
dates[dateString] = date;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldName = columns[index];
|
||||||
|
let effectiveFieldName = `${measurementName}.${fieldName}${tags}`;
|
||||||
|
|
||||||
|
// If there are duplicate effectiveFieldNames identify them by their queryIndex
|
||||||
|
if (effectiveFieldName in dateToFieldValue[dateString]) {
|
||||||
|
effectiveFieldName = `${effectiveFieldName}-${queryIndex}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
dateToFieldValue[dateString][effectiveFieldName] = value;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function buildTimeSeries() {
|
||||||
|
const allDates = Object.keys(dateToFieldValue);
|
||||||
|
allDates.sort((a, b) => a - b);
|
||||||
|
const rowLength = labels.length + 1;
|
||||||
|
return allDates.map((date) => {
|
||||||
|
const row = new Array(rowLength);
|
||||||
|
|
||||||
|
row.fill(null);
|
||||||
|
row[0] = new Date(dates[date]);
|
||||||
|
|
||||||
|
const fieldsForRow = dateToFieldValue[date];
|
||||||
|
Object.keys(fieldsForRow).forEach((effectiveFieldName) => {
|
||||||
|
row[fieldToIndex[effectiveFieldName]] = fieldsForRow[effectiveFieldName];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return row;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// labels are a unique combination of measurement, fields, and tags that indicate a specific series on the graph legend
|
|
||||||
const labels = cells.reduce((acc, {label, seriesIndex, responseIndex}) => {
|
|
||||||
const existingLabel = acc.find(({
|
|
||||||
label: findLabel,
|
|
||||||
seriesIndex: findSeriesIndex,
|
|
||||||
}) => findLabel === label && findSeriesIndex === seriesIndex);
|
|
||||||
|
|
||||||
if (!existingLabel) {
|
|
||||||
acc.push({
|
|
||||||
label,
|
|
||||||
seriesIndex,
|
|
||||||
responseIndex,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const sortedLabels = _.sortBy(labels, 'label');
|
|
||||||
|
|
||||||
const timeSeries = cells.reduce((acc, cell) => {
|
|
||||||
let existingRowIndex = acc.findIndex(({time}) => cell.time === time);
|
|
||||||
|
|
||||||
if (existingRowIndex === -1) {
|
|
||||||
acc.push({
|
|
||||||
time: cell.time,
|
|
||||||
values: Array(sortedLabels.length).fill(null),
|
|
||||||
});
|
|
||||||
|
|
||||||
existingRowIndex = acc.length - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const values = acc[existingRowIndex].values;
|
|
||||||
const labelIndex = sortedLabels.findIndex(({label, seriesIndex}) => label === cell.label && cell.seriesIndex === seriesIndex);
|
|
||||||
values[labelIndex] = cell.value;
|
|
||||||
acc[existingRowIndex].values = values;
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const sortedTimeSeries = _.sortBy(timeSeries, 'time');
|
|
||||||
|
|
||||||
const {light, heavy} = STROKE_WIDTH;
|
|
||||||
|
|
||||||
const dygraphSeries = sortedLabels.reduce((acc, {label, responseIndex}) => {
|
|
||||||
acc[label] = {
|
|
||||||
strokeWidth: responseIndex === activeQueryIndex ? heavy : light,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isInDataExplorer) {
|
|
||||||
acc[label].axis = responseIndex === 0 ? 'y' : 'y2';
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
timeSeries: sortedTimeSeries.map(({time, values}) => ([new Date(time), ...values])),
|
labels: ['time', ...labels.sort()],
|
||||||
labels: ["time", ...sortedLabels.map(({label}) => label)],
|
timeSeries: buildTimeSeries(),
|
||||||
dygraphSeries,
|
dygraphSeries,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isEmpty(resp) {
|
||||||
|
return !resp.results[0].series;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasError(resp) {
|
||||||
|
return !!resp.results[0].error;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue