Skip to content

Commit

Permalink
feat(discover): By day charts only show top 10 (#9563)
Browse files Browse the repository at this point in the history
On the by-day charts, we only show the top 10 series ordered by day/aggregation.

Updated the tests since data is now ordered by descending aggregation count.
  • Loading branch information
lynnagara committed Aug 30, 2018
1 parent 4b2168c commit 13e0731
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 61 deletions.
Expand Up @@ -83,4 +83,6 @@ const CONDITION_OPERATORS = [
'NOT LIKE',
];

export {COLUMNS, PROMOTED_TAGS, CONDITION_OPERATORS};
const NUMBER_OF_SERIES_BY_DAY = 10;

export {COLUMNS, PROMOTED_TAGS, CONDITION_OPERATORS, NUMBER_OF_SERIES_BY_DAY};
Expand Up @@ -11,6 +11,7 @@ import Tooltip from 'app/components/charts/components/tooltip';

import Table from './table';
import {getChartData, getChartDataByDay, formatTooltip} from './utils';
import {NUMBER_OF_SERIES_BY_DAY} from '../data';

export default class Result extends React.Component {
static propTypes = {
Expand Down Expand Up @@ -92,6 +93,10 @@ export default class Result extends React.Component {
);
}

renderNote() {
return <Note>{t(`Displaying up to ${NUMBER_OF_SERIES_BY_DAY} results`)}</Note>;
}

render() {
const {data, query, chartQuery, chartData} = this.props;
const {view} = this.state;
Expand All @@ -116,22 +121,28 @@ export default class Result extends React.Component {
/>
)}
{view === 'line-by-day' && (
<LineChart
series={getChartDataByDay(chartData.data, chartQuery)}
height={300}
/>
<React.Fragment>
<LineChart
series={getChartDataByDay(chartData.data, chartQuery)}
height={300}
/>
{this.renderNote()}
</React.Fragment>
)}
{view === 'bar-by-day' && (
<BarChart
series={getChartDataByDay(chartData.data, chartQuery)}
stacked={true}
height={300}
options={{
tooltip: Tooltip({
formatter: formatTooltip,
}),
}}
/>
<React.Fragment>
<BarChart
series={getChartDataByDay(chartData.data, chartQuery)}
stacked={true}
height={300}
options={{
tooltip: Tooltip({
formatter: formatTooltip,
}),
}}
/>
{this.renderNote()}
</React.Fragment>
)}
{this.renderSummary()}
</div>
Expand All @@ -143,3 +154,7 @@ const Summary = styled(Box)`
color: ${p => p.theme.gray6};
font-size: 12px;
`;

const Note = styled(Box)`
text-align: center;
`;
Expand Up @@ -3,6 +3,11 @@
import React from 'react';
import styled from 'react-emotion';
import moment from 'moment';
import {orderBy} from 'lodash';

import {NUMBER_OF_SERIES_BY_DAY} from '../data';

const CHART_KEY = '__CHART_KEY__';

/**
* Returns data formatted for basic line and bar charts, with each aggregation
Expand Down Expand Up @@ -36,47 +41,89 @@ export function getChartData(data, query) {
* @param {Object} query Query state corresponding to data
* @returns {Array}
*/
export function getChartDataByDay(data, query) {
export function getChartDataByDay(rawData, query) {
// We only chart the first aggregation for now
const aggregate = query.aggregations[0][2];

const data = getDataWithKeys(rawData, query);

// We only want to show the top 10 series
const top10Series = getTopSeries(data, aggregate);

const dates = [...new Set(rawData.map(entry => formatDate(entry.time)))];

// Temporarily store series as object with series names as keys
const seriesHash = getEmptySeriesHash(top10Series, dates);

// Insert data into series if it's in a top 10 series
data.forEach(row => {
const key = row[CHART_KEY];

const dateIdx = dates.indexOf(formatDate(row.time));

if (top10Series.has(key)) {
seriesHash[key][dateIdx].value = row[aggregate];
}
});

// Format for echarts
return Object.entries(seriesHash).map(([seriesName, series]) => {
return {
seriesName,
data: series,
};
});
}

// Return placeholder empty series object with all series and dates listed and
// all values set to null
function getEmptySeriesHash(seriesSet, dates) {
const output = {};

[...seriesSet].forEach(series => {
output[series] = getEmptySeries(dates);
});

return output;
}

function getEmptySeries(dates) {
return dates.map(date => {
return {
value: null,
name: date,
};
});
}

// Get the top series ranked by latest time / largest aggregate
function getTopSeries(data, aggregate) {
const allData = orderBy(data, ['time', aggregate], ['desc', 'desc']);

return new Set(
[...new Set(allData.map(row => row[CHART_KEY]))].slice(0, NUMBER_OF_SERIES_BY_DAY)
);
}

function getDataWithKeys(data, query) {
const {aggregations, fields} = query;
// We only chart the first aggregation for now
const aggregate = aggregations[0][2];
const dates = [
...new Set(data.map(entry => moment.utc(entry.time * 1000).format('MMM Do'))),
];
const output = {};
data.forEach(res => {

return data.map(row => {
const key = fields.length
? fields.map(field => getLabel(res[field])).join(',')
? fields.map(field => getLabel(row[field])).join(',')
: aggregate;
if (key in output) {
output[key].data.push({
value: res[aggregate],
name: moment.utc(res.time * 1000).format('MMM Do'),
});
} else {
output[key] = {
data: [
{value: res[aggregate], name: moment.utc(res.time * 1000).format('MMM Do')},
],
};
}
});
const result = [];
for (let key in output) {
const addDates = dates.filter(
date => !output[key].data.map(entry => entry.name).includes(date)
);
for (let i = 0; i < addDates.length; i++) {
output[key].data.push({
value: null,
name: addDates[i],
});
}

result.push({seriesName: key, data: output[key].data});
}
return {
...row,
[CHART_KEY]: key,
};
});
}

return result;
function formatDate(datetime) {
return moment.utc(datetime * 1000).format('MMM Do');
}

export function formatTooltip(seriesParams) {
Expand All @@ -90,6 +137,7 @@ export function formatTooltip(seriesParams) {
].join('');
}

// Truncates labels for tooltip
function truncateLabel(seriesName) {
let result = seriesName;
if (seriesName.length > 80) {
Expand Down
24 changes: 12 additions & 12 deletions tests/js/spec/views/organizationDiscover/result/utils.spec.jsx
Expand Up @@ -101,35 +101,35 @@ describe('Utils', function() {
const expected = [
{
data: [
{name: 'Jul 9th', value: 6},
{name: 'Jul 10th', value: 20},
{name: 'Jul 20th', value: null},
{name: 'Jul 9th', value: 14},
{name: 'Jul 10th', value: null},
{name: 'Jul 20th', value: 30},
],
seriesName: 'python,ZeroDivisionError',
seriesName: 'python,SnubaError',
},
{
data: [
{name: 'Jul 9th', value: 6},
{name: 'Jul 20th', value: 5},
{name: 'Jul 10th', value: null},
{name: 'Jul 20th', value: 8},
],
seriesName: 'javascript,Type Error',
seriesName: 'php,Exception',
},
{
data: [
{name: 'Jul 9th', value: 6},
{name: 'Jul 20th', value: 8},
{name: 'Jul 10th', value: null},
{name: 'Jul 20th', value: 5},
],
seriesName: 'php,Exception',
seriesName: 'javascript,Type Error',
},
{
data: [
{name: 'Jul 9th', value: 14},
{name: 'Jul 20th', value: 30},
{name: 'Jul 10th', value: null},
{name: 'Jul 9th', value: 6},
{name: 'Jul 10th', value: 20},
{name: 'Jul 20th', value: null},
],
seriesName: 'python,SnubaError',
seriesName: 'python,ZeroDivisionError',
},
];

Expand Down
1 change: 1 addition & 0 deletions webpack.config.js
Expand Up @@ -166,6 +166,7 @@ var appConfig = {
collections: true,
currying: true, // these are enabled to support lodash/fp/ features
flattening: true, // used by a dependency of react-mentions
shorthands: true,
}),
new webpack.optimize.CommonsChunkPlugin({
names: localeEntries.concat(['vendor']), // 'vendor' must be last entry
Expand Down

0 comments on commit 13e0731

Please sign in to comment.