diff --git a/src/sentry/static/sentry/app/components/charts/areaChart.jsx b/src/sentry/static/sentry/app/components/charts/areaChart.jsx index 6e043da2d70791..25c65d2d77eb58 100644 --- a/src/sentry/static/sentry/app/components/charts/areaChart.jsx +++ b/src/sentry/static/sentry/app/components/charts/areaChart.jsx @@ -1,68 +1,40 @@ -import moment from 'moment'; import React from 'react'; +import PropTypes from 'prop-types'; import theme from 'app/utils/theme'; -import SentryTypes from 'app/sentryTypes'; import AreaSeries from './series/areaSeries'; import BaseChart from './baseChart'; -import LineSeries from './series/lineSeries'; -import XAxis from './components/xAxis'; -import YAxis from './components/yAxis'; class AreaChart extends React.Component { static propTypes = { ...BaseChart.propTypes, - - /** - * Display previous period as a line - */ - previousPeriod: SentryTypes.SeriesUnit, + stacked: PropTypes.bool, }; render() { - const {series, previousPeriod, ...props} = this.props; - if (!series.length) return null; + const {series, stacked, ...props} = this.props; + const colors = + (series && series.length && theme.charts.getColorPalette(series.length)) || {}; return ( moment(value).format('MMM D'), + series={series.map((s, i) => + AreaSeries({ + stack: stacked ? 'area' : false, + name: s.seriesName, + data: s.data.map(({name, value}) => [name, value]), + lineStyle: { + color: '#fff', + width: 2, + }, + areaStyle: { + color: colors[i], + opacity: 1.0, }, - }), - yAxis: YAxis({}), - series: [ - ...series.map((s, i) => - AreaSeries({ - stack: 'test', - name: s.seriesName, - data: s.data.map(({name, value}) => [name, value]), - lineStyle: { - color: '#fff', - width: 2, - }, - areaStyle: { - color: theme.charts.colors[i], - opacity: 1.0, - }, - }) - ), - previousPeriod && - LineSeries({ - name: previousPeriod.seriesName, - data: previousPeriod.data.map(({name, value}) => [name, value]), - lineStyle: { - color: theme.gray1, - type: 'dotted', - }, - }), - ], - }} + }) + )} /> ); } diff --git a/src/sentry/static/sentry/app/components/charts/barChart.jsx b/src/sentry/static/sentry/app/components/charts/barChart.jsx index 0667d58491896a..6f6266ac4b69f1 100644 --- a/src/sentry/static/sentry/app/components/charts/barChart.jsx +++ b/src/sentry/static/sentry/app/components/charts/barChart.jsx @@ -2,8 +2,6 @@ import React from 'react'; import BarSeries from './series/barSeries.jsx'; import BaseChart from './baseChart'; -import YAxis from './components/yAxis'; -import XAxis from './components/xAxis'; export default class BarChart extends React.Component { static propTypes = { @@ -11,25 +9,19 @@ export default class BarChart extends React.Component { }; render() { - const {series, stacked} = this.props; + const {series, stacked, xAxis, ...props} = this.props; return ( { - return BarSeries({ - name: s.seriesName, - stack: stacked ? 'stack1' : null, - data: s.data.map(({value, name}) => [name, value]), - }); - }), - ...this.props.options, - }} + {...props} + xAxis={xAxis !== null ? {...(xAxis || {}), boundaryGap: true} : null} + series={series.map((s, i) => { + return BarSeries({ + name: s.seriesName, + stack: stacked ? 'stack1' : null, + data: s.data.map(({value, name}) => [name, value]), + }); + })} /> ); } diff --git a/src/sentry/static/sentry/app/components/charts/baseChart.jsx b/src/sentry/static/sentry/app/components/charts/baseChart.jsx index d3c09871743c87..f86ca903986499 100644 --- a/src/sentry/static/sentry/app/components/charts/baseChart.jsx +++ b/src/sentry/static/sentry/app/components/charts/baseChart.jsx @@ -5,10 +5,14 @@ import React from 'react'; import ReactEchartsCore from 'echarts-for-react/lib/core'; import echarts from 'echarts/lib/echarts'; +import SentryTypes from 'app/sentryTypes'; import theme from 'app/utils/theme'; import Grid from './components/grid'; +import LineSeries from './series/lineSeries'; import Tooltip from './components/tooltip'; +import YAxis from './components/yAxis'; +import XAxis from './components/xAxis'; // If dimension is a number conver it to pixels, otherwise use dimension without transform const getDimensionValue = dimension => { @@ -23,12 +27,29 @@ class BaseChart extends React.Component { static propTypes = { // TODO: Pull out props from generic `options` object // so that we can better document them in prop types - // e.g: - // series: SentryTypes.Series, - // see: https://ecomfe.github.io/echarts-doc/public/en/option.html options: PropTypes.object, + // Chart Series + // This is different than the interface to higher level charts, these need to be + // an array of ECharts "Series" components. + series: SentryTypes.EChartsSeries, + + // Array of color codes to use in charts + colors: PropTypes.arrayOf(PropTypes.string), + + // Must be explicitly `null` to disable xAxis + xAxis: SentryTypes.EChartsXAxis, + + // Must be explicitly `null` to disable yAxis + yAxis: SentryTypes.EChartsYAxis, + + // Tooltip options + tooltip: SentryTypes.EChartsTooltip, + + // ECharts Grid options + grid: SentryTypes.EChartsGrid, + // Chart height height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), @@ -48,9 +69,6 @@ class BaseChart extends React.Component { // example theme: https://github.com/apache/incubator-echarts/blob/master/theme/dark.js theme: PropTypes.string, - // Default array of color codes to use in charts - colors: PropTypes.arrayOf(PropTypes.string), - // states whether or not to merge with previous `option` notMerge: PropTypes.bool, @@ -68,6 +86,16 @@ class BaseChart extends React.Component { // Forwarded Ref forwardedRef: PropTypes.object, + + // Custom chart props that are implemented by us (and not a feature of eCharts) + /** + * Display previous period as a LineSeries + */ + previousPeriod: SentryTypes.SeriesUnit, + + // If data is grouped by date, then apply default date formatting to + // x-axis and tooltips. + isGroupedByDate: PropTypes.bool, }; static defaultProps = { @@ -76,8 +104,13 @@ class BaseChart extends React.Component { renderer: 'svg', notMerge: true, lazyUpdate: false, - options: {}, onChartReady: () => {}, + options: {}, + + series: [], + xAxis: {}, + yAxis: {}, + isGroupedByDate: false, }; handleChartReady = (...args) => { @@ -86,7 +119,7 @@ class BaseChart extends React.Component { }; getColorPalette = () => { - let {series} = this.props.options; + let {series} = this.props; return series && series.length ? theme.charts.getColorPalette(series.length) @@ -95,12 +128,21 @@ class BaseChart extends React.Component { render() { let { + options, colors, + grid, + tooltip, + series, + yAxis, + xAxis, + + isGroupedByDate, + previousPeriod, + devicePixelRatio, height, width, renderer, - options, notMerge, lazyUpdate, silent, @@ -113,12 +155,6 @@ class BaseChart extends React.Component { [name, value]), + lineStyle: { + color: theme.gray1, + type: 'dotted', + }, + }), + ], + }} /> ); } diff --git a/src/sentry/static/sentry/app/components/charts/components/grid.jsx b/src/sentry/static/sentry/app/components/charts/components/grid.jsx index 9cb531c4a1fee3..5765b2a8773d77 100644 --- a/src/sentry/static/sentry/app/components/charts/components/grid.jsx +++ b/src/sentry/static/sentry/app/components/charts/components/grid.jsx @@ -14,5 +14,7 @@ export default function Grid(props = {}) { left: '10%', right: '10%', + + ...props, }; } diff --git a/src/sentry/static/sentry/app/components/charts/components/tooltip.jsx b/src/sentry/static/sentry/app/components/charts/components/tooltip.jsx index f5b21a4f9b529d..1110990169748f 100644 --- a/src/sentry/static/sentry/app/components/charts/components/tooltip.jsx +++ b/src/sentry/static/sentry/app/components/charts/components/tooltip.jsx @@ -1,9 +1,69 @@ +import moment from 'moment'; import 'echarts/lib/component/tooltip'; -export default function Tooltip(props = {}) { +const DEFAULT_TRUNCATE_LENGTH = 80; + +// Truncates labels for tooltip +function truncateLabel(seriesName, truncate) { + if (!truncate) { + return seriesName; + } + + let result = seriesName; + let truncateLength = typeof truncate === 'number' ? truncate : DEFAULT_TRUNCATE_LENGTH; + 0; + + if (seriesName.length > truncateLength) { + result = seriesName.substring(0, truncateLength) + '…'; + } + return result; +} + +function formatAxisLabel(value, isTimestamp) { + if (!isTimestamp) { + return value; + } + + return moment(value).format('MMM D, YYYY'); +} + +function getFormatter({filter, isGroupedByDate, truncate}) { + const getFilter = seriesParam => { + const value = seriesParam.data[1]; + if (typeof filter === 'function') { + return filter(value); + } + + return true; + }; + + return seriesParams => { + const label = + seriesParams.length && + formatAxisLabel(seriesParams[0].axisValueLabel, isGroupedByDate); + return [ + `
${truncateLabel(label, truncate)}
`, + seriesParams + .filter(getFilter) + .map( + s => + `
${s.marker} ${truncateLabel(s.seriesName, truncate)}: ${s + .data[1]}
` + ) + .join(''), + ].join(''); + }; +} + +export default function Tooltip( + {filter, isGroupedByDate, formatter, truncate, ...props} = {} +) { + formatter = formatter || getFormatter({filter, isGroupedByDate, truncate}); + return { show: true, trigger: 'axis', + formatter, ...props, }; } diff --git a/src/sentry/static/sentry/app/components/charts/components/xAxis.jsx b/src/sentry/static/sentry/app/components/charts/components/xAxis.jsx index 630d79f18bff6d..6c15dcde19541b 100644 --- a/src/sentry/static/sentry/app/components/charts/components/xAxis.jsx +++ b/src/sentry/static/sentry/app/components/charts/components/xAxis.jsx @@ -1,8 +1,15 @@ +import moment from 'moment'; + import theme from 'app/utils/theme'; -export default function XAxis(props = {}) { +export default function XAxis({isGroupedByDate, ...props} = {}) { + const axisLabelFormatter = isGroupedByDate + ? (value, index) => moment.utc(value).format('MMM Do') + : undefined; + return { - boundaryGap: true, + type: 'category', + boundaryGap: false, axisLine: { lineStyle: { color: theme.gray1, @@ -17,6 +24,7 @@ export default function XAxis(props = {}) { }, axisLabel: { margin: 12, + formatter: axisLabelFormatter, ...(props.axisLabel || {}), }, ...props, diff --git a/src/sentry/static/sentry/app/components/charts/lineChart.jsx b/src/sentry/static/sentry/app/components/charts/lineChart.jsx index 121472bdabca9b..7069efc9365fd3 100644 --- a/src/sentry/static/sentry/app/components/charts/lineChart.jsx +++ b/src/sentry/static/sentry/app/components/charts/lineChart.jsx @@ -1,7 +1,5 @@ import React from 'react'; import BaseChart from './baseChart'; -import XAxis from './components/xAxis'; -import YAxis from './components/yAxis'; import LineSeries from './series/lineSeries'; export default class LineChart extends React.Component { @@ -15,19 +13,12 @@ export default class LineChart extends React.Component { return ( { - return LineSeries({ - name: s.seriesName, - data: s.data.map(({value, name}) => [name, value]), - }); - }), - }} + series={series.map(s => { + return LineSeries({ + name: s.seriesName, + data: s.data.map(({value, name}) => [name, value]), + }); + })} /> ); } diff --git a/src/sentry/static/sentry/app/components/charts/percentageBarChart.jsx b/src/sentry/static/sentry/app/components/charts/percentageBarChart.jsx index 294c1028ebeb70..bed2779a17aca4 100644 --- a/src/sentry/static/sentry/app/components/charts/percentageBarChart.jsx +++ b/src/sentry/static/sentry/app/components/charts/percentageBarChart.jsx @@ -4,9 +4,6 @@ import moment from 'moment'; import BarSeries from './series/barSeries.jsx'; import BaseChart from './baseChart'; -import Tooltip from './components/tooltip'; -import XAxis from './components/xAxis'; -import YAxis from './components/yAxis'; const FILLER_NAME = '__filler'; @@ -72,49 +69,41 @@ export default class PercentageBarChart extends React.Component { } render() { - const series = this.getSeries(); return ( { - // Filter series that have 0 counts - const date = - `${seriesParams.length && - moment(seriesParams[0].axisValue).format('MMM D, YYYY')}
` || ''; - return `${date} ${seriesParams - .filter( - ({seriesName, data}) => data[1] > 0.001 && seriesName !== FILLER_NAME - ) - .map( - ({marker, seriesName, data}) => - `${marker} ${seriesName}: ${data[1]}%` - ) - .join('
')}`; - }, - }), - xAxis: XAxis({ - type: 'time', - axisLabel: { - formatter: (value, index) => moment(value).format('MMM D'), - }, - }), - yAxis: YAxis({ - min: 0, - max: 100, - type: 'value', - interval: 25, - splitNumber: 4, - data: [0, 25, 50, 100], - axisLabel: { - formatter: '{value}%', - }, - }), - series, + tooltip={{ + // Make sure tooltip is inside of chart (because of overflow: hidden) + confine: true, + formatter: seriesParams => { + // Filter series that have 0 counts + const date = + `${seriesParams.length && + moment(seriesParams[0].axisValue).format('MMM D, YYYY')}
` || ''; + return `${date} ${seriesParams + .filter( + ({seriesName, data}) => data[1] > 0.001 && seriesName !== FILLER_NAME + ) + .map( + ({marker, seriesName, data}) => + `${marker} ${seriesName}: ${data[1]}%` + ) + .join('
')}`; + }, + }} + xAxis={{boundaryGap: true}} + yAxis={{ + min: 0, + max: 100, + type: 'value', + interval: 25, + splitNumber: 4, + data: [0, 25, 50, 100], + axisLabel: { + formatter: '{value}%', + }, }} + series={this.getSeries()} /> ); } diff --git a/src/sentry/static/sentry/app/components/charts/pieChart.jsx b/src/sentry/static/sentry/app/components/charts/pieChart.jsx index 2198379f68a8bb..7434dc6da9b587 100644 --- a/src/sentry/static/sentry/app/components/charts/pieChart.jsx +++ b/src/sentry/static/sentry/app/components/charts/pieChart.jsx @@ -135,39 +135,41 @@ class PieChart extends React.Component { : ''}`; }, }), - series: [ - PieSeries({ - name: firstSeries.seriesName, - data: firstSeries.data, - avoidLabelOverlap: false, - label: { - normal: { - formatter: ({name, percent, dataIndex}) => { - return `${name}\n${percent}%`; - }, - show: false, - position: 'center', + }} + series={[ + PieSeries({ + name: firstSeries.seriesName, + data: firstSeries.data, + avoidLabelOverlap: false, + label: { + normal: { + formatter: ({name, percent, dataIndex}) => { + return `${name}\n${percent}%`; }, - emphasis: { - show: true, - textStyle: { - fontSize: '18', - }, + show: false, + position: 'center', + }, + emphasis: { + show: true, + textStyle: { + fontSize: '18', }, }, - itemStyle: { - normal: { - label: { - show: false, - }, - labelLine: { - show: false, - }, + }, + itemStyle: { + normal: { + label: { + show: false, + }, + labelLine: { + show: false, }, }, - }), - ], - }} + }, + }), + ]} + xAxis={null} + yAxis={null} /> ); } diff --git a/src/sentry/static/sentry/app/sentryTypes.jsx b/src/sentry/static/sentry/app/sentryTypes.jsx index ba608772bc3fda..5e17febeae269b 100644 --- a/src/sentry/static/sentry/app/sentryTypes.jsx +++ b/src/sentry/static/sentry/app/sentryTypes.jsx @@ -303,17 +303,385 @@ export const ProjectKey = PropTypes.shape({ cdnSdkUrl: PropTypes.string, }); -export const SeriesUnit = PropTypes.shape({ - seriesName: PropTypes.string, - data: PropTypes.oneOfType([ - PropTypes.arrayOf( +export const EChartsSeriesUnit = PropTypes.shape({ + type: PropTypes.oneOf(['line', 'bar', 'pie']), + showSymbol: PropTypes.bool, + name: PropTypes.string, + data: PropTypes.arrayOf( + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])) + ), +}); + +export const EChartsSeries = PropTypes.arrayOf(EChartsSeriesUnit); + +// See https://ecomfe.github.io/echarts-doc/public/en/option.html#xAxis +export const EChartsAxis = PropTypes.shape({ + // Component ID, not specified by default. If specified, it can be used to refer the component in option or API. + id: PropTypes.string, + + // If show x axis. + show: PropTypes.bool, + + gridIndex: PropTypes.number, + // The index of grid which the x axis belongs to. Defaults to be in the first grid. + + // The position of x axis. + // The first x axis in grid defaults to be on the bottom of the grid, and the second x axis is on the other side against the first x axis. + position: PropTypes.oneOf(['top', 'bottom']), + + // Offset of x axis relative to default position. Useful when multiple x axis has same position value. + offset: PropTypes.number, + + // Type of axis + // Option: + // 'value' Numerical axis, suitable for continuous data. + // 'category' Category axis, suitable for discrete category data. Data should only be set via data for this type. + // 'time' Time axis, suitable for continuous time series data. As compared to value axis, it has a better formatting for time and a different tick calculation method. For example, it decides to use month, week, day or hour for tick based on the range of span. + // 'log' Log axis, suitable for log data. + type: PropTypes.oneOf(['value', 'category', 'time', 'log']), + + // Name of axis. + name: PropTypes.string, + + // Location of axis name. + nameLocation: PropTypes.oneOf(['start', 'middle', 'center', 'end']), + + // Text style of axis name. + nameTextStyle: PropTypes.object, + + // Gap between axis name and axis line. + nameGap: PropTypes.number, + + // Rotation of axis name. + nameRotate: PropTypes.number, + + // Whether axis is inversed. New option from ECharts 3. + inverse: PropTypes.bool, + + // The boundary gap on both sides of a coordinate axis. The setting and behavior of category axes and non-category axes are different. + // The boundaryGap of category axis can be set to either true or false. Default value is set to be true, in which case axisTick is served only as a separation line, and labels and data appear only in the center part of two axis ticks, which is called band. + // For non-category axis, including time, numerical value, and log axes, boundaryGap is an array of two values, representing the spanning range between minimum and maximum value. The value can be set in numeric value or relative percentage, which becomes invalid after setting min and max. Example: + boundaryGap: PropTypes.oneOfType([PropTypes.bool, PropTypes.array]), + + // The minimun value of axis. + // It can be set to a special value 'dataMin' so that the minimum value on this axis is set to be the minimum label. + // It will be automatically computed to make sure axis tick is equally distributed when not set. + // In category axis, it can also be set as the ordinal number. For example, if a catergory axis has data: ['categoryA', 'categoryB', 'categoryC'], and the ordinal 2 represents 'categoryC'. Moreover, it can be set as negative number, like -3. + min: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + + // The maximum value of axis. + // It can be set to a special value 'dataMax' so that the minimum value on this axis is set to be the maximum label. + // It will be automatically computed to make sure axis tick is equally distributed when not set. + // In category axis, it can also be set as the ordinal number. For example, if a catergory axis has data: ['categoryA', 'categoryB', 'categoryC'], and the ordinal 2 represents 'categoryC'. Moreover, it can be set as negative number, like -3. + max: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + + // It is available only in numerical axis, i.e., type: 'value'. + // It specifies whether not to contain zero position of axis compulsively. When it is set to be true, the axis may not contain zero position, which is useful in the scatter chart for both value axes. + // This configuration item is unavailable when the min and max are set. + scale: PropTypes.bool, + + // Number of segments that the axis is split into. Note that this number serves only as a recommendation, and the true segments may be adjusted based on readability. + // This is unavailable for category axis. + splitNumber: PropTypes.number, + + // Maximum gap between split lines. + // For example, in time axis (type is 'time'), it can be set to be 3600 * 24 * 1000 to make sure that the gap between axis labels is less than or equal to one day. + // { + // maxInterval: 3600 * 1000 * 24 + // } + // It is available only for axis of type 'value' or 'time'. + minInterval: PropTypes.number, + + // Compulsively set segmentation interval for axis. + // As splitNumber is a recommendation value, the calculated tick may not be the same as expected. In this case, interval should be used along with min and max to compulsively set tickings. But in most cases, we do not suggest using this, out automatic calculation is enough for most situations. + + // This is unavailable for category axis. Timestamp should be passed for type: 'time' axis. Logged value should be passed for type: 'log' axis. + + interval: PropTypes.number, + + // Base of logarithm, which is valid only for numeric axes with type: 'log'. + logBase: PropTypes.number, + + // True for axis that cannot be interacted with. + silent: PropTypes.bool, + + // Whether the labels of axis triggers and reacts to mouse events. + // Parameters of event includes: + + // { + // Component type: xAxis, yAxis, radiusAxis, angleAxis + // Each of which has an attribute for index, e.g., xAxisIndex for xAxis + // componentType: string, + // Value on axis before being formatted. + // Click on value label to trigger event. + // value: '', + // Name of axis. + // Click on laben name to trigger event. + // name: '' + // } + triggerEvent: PropTypes.bool, + + // Settings related to axis line. + axisLine: PropTypes.object, + + // Settings related to axis tick. + axisTick: PropTypes.object, + + // Settings related to axis label. + axisLabel: PropTypes.object, + + // SplitLine of axis in grid area. + splitLine: PropTypes.object, + + // Split area of axis in grid area, not shown by default. + splitArea: PropTypes.object, + + // Category data, available in type: 'category' axis. + // If type is not specified, but axis.data is specified, the type is auto set as 'category'. + // If type is specified as 'category', but axis.data is not specified, axis.data will be auto collected from series.data. It brings convenience, but we should notice that axis.data provides then value range of the 'category' axis. If it is auto collected from series.data, Only the values appearing in series.data can be collected. For example, if series.data is empty, nothing will be collected. + // Example: + + // // Name list of all categories + // data: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + // // Each item could also be a specific configuration item. + // // In this case, `value` is used as the category name. + // data: [{ + // value: 'Monday', + // // Highlight Monday + // textStyle: { + // fontSize: 20, + // color: 'red' + // } + // }, 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + data: PropTypes.arrayOf( + PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])), PropTypes.shape({ + name: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), value: PropTypes.number, - category: PropTypes.string, - }) - ), - PropTypes.arrayOf(PropTypes.number), - ]), + }), + ]) + ), + + // axisPointer settings on the axis. + axisPointer: PropTypes.object, + + // zlevel value of all graghical elements in x axis. + // zlevel is used to make layers with Canvas. Graphical elements with different zlevel values will be placed in different Canvases, which is a common optimization technique. We can put those frequently changed elements (like those with animations) to a seperate zlevel. Notice that too many Canvases will increase memory cost, and should be used carefully on mobile phones to avoid crash. + // Canvases with bigger zlevel will be placed on Canvases with smaller zlevel. + zlevel: PropTypes.number, + + z: PropTypes.number, +}); + +export const EChartsTooltip = PropTypes.shape({ + // custom filter function + filter: PropTypes.func, + + // If this is true, then format date + isGroupedByDate: PropTypes.bool, + + // Truncate labels to this length + truncate: PropTypes.oneOfType([PropTypes.bool, PropTypes.number]), + + /** echarts tooltip properties **/ + // Whether to show the tooltip component, including tooltip floating layer and axisPointer. + show: PropTypes.bool, + + // Type of triggering. + // Options: + // 'item' + // Triggered by data item, which is mainly used for charts that don't have a category axis like scatter charts or pie charts. + // 'axis' + // Triggered by axes, which is mainly used for charts that have category axes, like bar charts or line charts. + // ECharts 2.x only supports axis trigger for category axis. In ECharts 3, it is supported for all types of axes in grid or polar. Also, you may assign axis with axisPointer.axis. + // 'none' + // Trigger nothing. + trigger: PropTypes.oneOf(['item', 'axis', 'none']), + + // Configuration item for axis indicator. + // See https://ecomfe.github.io/echarts-doc/public/en/option.html#tooltip.axisPointer + axisPointer: PropTypes.object, + + // Whether to show the tooltip floating layer, whose default value is true. It should be configurated to be false, if you only need tooltip to trigger the event or show the axisPointer without content. + showContent: PropTypes.bool, + + // Whether to show tooltip content all the time. By default, it will be hidden after some time. It can be set to be true to preserve displaying. + // This attribute is newly added to ECharts 3.0. + alwaysShowContent: PropTypes.bool, + + // Conditions to trigger tooltip. Options: + // 'mousemove' + + // Trigger when mouse moves. + + // 'click' + + // Trigger when mouse clicks. + + // 'mousemove|click' + + // Trigger when mouse clicks and moves. + + // 'none' + + // Do not triggered by 'mousemove' and 'click'. Tooltip can be triggered and hidden manually by calling action.tooltip.showTip and action.tooltip.hideTip. It can also be triggered by axisPointer.handle in this case. + + // This attribute is new to ECharts 3.0. + triggerOn: PropTypes.oneOf(['mousemove', 'click', 'mousemove|click', 'none']), + + // Delay time for showing tooltip, in ms. No delay by default, and it is not recommended to set. Only valid when triggerOn is set to be 'mousemove'. + showDelay: PropTypes.number, + + // Delay time for hiding tooltip, in ms. It will be invalid when alwaysShowContent is true. + hideDelay: PropTypes.number, + + // Whether mouse is allowed to enter the floating layer of tooltip, whose default value is false. If you need to interact in the tooltip like with links or buttons, it can be set as true. + enterable: PropTypes.bool, + + // Whether confine tooltip content in the view rect of chart instance. + // Useful when tooltip is cut because of 'overflow: hidden' set on outer dom of chart instance, or because of narrow screen on mobile. + confine: PropTypes.bool, + + // The transition duration of tooltip's animation, in seconds. When it is set to be 0, it would move closely with the mouse. + transitionDuration: PropTypes.number, + + // The position of the tooltip's floating layer, which would follow the position of mouse by default. + // See https://ecomfe.github.io/echarts-doc/public/en/option.html#tooltip.position + position: PropTypes.oneOfType([PropTypes.string, PropTypes.array]), + + // The content formatter of tooltip's floating layer which supports string template and callback function. + // See https://ecomfe.github.io/echarts-doc/public/en/option.html#tooltip.formatter + formatter: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + + // The background color of tooltip's floating layer. + backgroundColor: PropTypes.string, + + // The border color of tooltip's floating layer. + borderColor: PropTypes.string, + + // The border width of tooltip's floating layer. + borderWidth: PropTypes.number, + + // The floating layer of tooltip space around content. The unit is px. Default values for each position are 5. And they can be set to different values with left, right, top, and bottom. + // Examples: + + // // Set padding to be 5 + // padding: 5 + // // Set the top and bottom paddings to be 5, and left and right paddings to be 10 + // padding: [5, 10] + // // Set each of the four paddings seperately + // padding: [ + // 5, // up + // 10, // right + // 5, // down + // 10, // left + // ] + padding: PropTypes.number, + + // The text syle of tooltip's floating layer. + textStyle: PropTypes.object, + + extraCssText: PropTypes.string, +}); + +export const EChartsGrid = PropTypes.shape({ + // Component ID, not specified by default. If specified, it can be used to refer the component in option or API. + id: PropTypes.string, + + // Whether to show the grid in rectangular coordinate. + show: PropTypes.bool, + + // zlevel value of all graghical elements in . + // zlevel is used to make layers with Canvas. Graphical elements with different zlevel values will be placed in different Canvases, which is a common optimization technique. We can put those frequently changed elements (like those with animations) to a seperate zlevel. Notice that too many Canvases will increase memory cost, and should be used carefully on mobile phones to avoid crash. + // Canvases with bigger zlevel will be placed on Canvases with smaller zlevel. + zlevel: PropTypes.number, + + // z value of all graghical elements in , which controls order of drawing graphical components. Components with smaller z values may be overwritten by those with larger z values. + // z has a lower priority to zlevel, and will not create new Canvas. + z: PropTypes.number, + + // Distance between grid component and the left side of the container. + // left value can be instant pixel value like 20; it can also be percentage value relative to container width like '20%'; and it can also be 'left', 'center', or 'right'. + // If the left value is set to be 'left', 'center', or 'right', then the component will be aligned automatically based on position. + left: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + + // Distance between grid component and the top side of the container. + // top value can be instant pixel value like 20; it can also be percentage value relative to container width like '20%'; and it can also be 'top', 'middle', or 'bottom'. + // If the left value is set to be 'top', 'middle', or 'bottom', then the component will be aligned automatically based on position. + top: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + + // Distance between grid component and the right side of the container. + // right value can be instant pixel value like 20; it can also be percentage value relative to container width like '20%'. + right: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + + // Distance between grid component and the bottom side of the container. + // bottom value can be instant pixel value like 20; it can also be percentage value relative to container width like '20%'. + bottom: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + + // Width of grid component. Adaptive by default. + width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + + // Height of grid component. Adaptive by default. + height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + + // Whether the grid region contains axis tick label of axis. + // When containLabel is false: + // grid.left grid.right grid.top grid.bottom grid.width grid.height decide the location and size of the rectangle that is made of by xAxis and yAxis. + // Setting to false will helps when multiple gris need to be align at their axes. + // When containLabel is true: + // grid.left grid.right grid.top grid.bottom grid.width grid.height decide the location and size of the rectangle that is not only contains axes but also contains labels of those axes. + // Setting to true will helps when the length of axis labels is dynamic and is hard to approximate to avoid them overflowing the container or overlapping other components. + containLabel: PropTypes.bool, + + // Background color of grid, which is transparent by default. + // Color can be represented in RGB, for example 'rgb(128, 128, 128)'. RGBA can be used when you need alpha channel, for example 'rgba(128, 128, 128, 0.5)'. You may also use hexadecimal format, for example '#ccc'. + // Attention: Works only if show: true is set. + backgroundColor: PropTypes.string, + + // Border color of grid. Support the same color format as backgroundColor. + // Attention: Works only if show: true is set. + borderColor: PropTypes.string, + + // Border width of grid. + // Attention: Works only if show: true is set. + borderWidth: PropTypes.number, + + // Size of shadow blur. This attribute should be used along with shadowColor,shadowOffsetX, shadowOffsetY to set shadow to component. + // For example: + + // { + // shadowColor: 'rgba(0, 0, 0, 0.5)', + // shadowBlur: 10 + // } + // Attention: This property works only if show: true is configured and backgroundColor is defined other than transparent. + + shadowBlur: PropTypes.number, + + // Shadow color. Support same format as color. + // Attention: This property works only if show: true configured. + shadowColor: PropTypes.string, + + // Offset distance on the horizontal direction of shadow. + // Attention: This property works only if show: true configured. + shadowOffsetX: PropTypes.number, + + // Offset distance on the vertical direction of shadow. + // Attention: This property works only if show: true configured. + shadowOffsetY: PropTypes.number, + + tooltip: EChartsTooltip, +}); + +export const SeriesUnit = PropTypes.shape({ + seriesName: PropTypes.string, + data: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.number, + // Number because datetime + name: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + }) + ), }); export const Series = PropTypes.arrayOf(SeriesUnit); @@ -348,6 +716,14 @@ let SentryTypes = { ProjectKey, Release, User, + + // echarts prop types + EChartsSeries, + EChartsSeriesUnit, + EChartsXAxis: EChartsAxis, + EChartsYAxis: EChartsAxis, + EChartsTooltip, + EChartsGrid, }; export default SentryTypes; diff --git a/src/sentry/static/sentry/app/views/organizationDiscover/result/index.jsx b/src/sentry/static/sentry/app/views/organizationDiscover/result/index.jsx index f64e832e4ddfa9..94ece3fe96850b 100644 --- a/src/sentry/static/sentry/app/views/organizationDiscover/result/index.jsx +++ b/src/sentry/static/sentry/app/views/organizationDiscover/result/index.jsx @@ -10,9 +10,8 @@ import BarChart from 'app/components/charts/barChart'; import LineChart from 'app/components/charts/lineChart'; import Panel from 'app/components/panels/panel'; import space from 'app/styles/space'; -import Tooltip from 'app/components/charts/components/tooltip'; -import {getChartData, getChartDataByDay, formatTooltip, downloadAsCsv} from './utils'; +import {getChartData, getChartDataByDay, downloadAsCsv} from './utils'; import Table from './table'; import {Heading} from '../styles'; import {NUMBER_OF_SERIES_BY_DAY} from '../data'; @@ -118,6 +117,11 @@ export default class Result extends React.Component { const byDayChartData = chartData && getChartDataByDay(chartData.data, chartQuery); + const tooltipOptions = { + filter: value => value !== null, + truncate: 80, + }; + return (
@@ -130,41 +134,17 @@ export default class Result extends React.Component { {view === 'table' && } {view === 'line' && ( - + )} {view === 'bar' && ( - + )} {view === 'line-by-day' && ( - + {this.renderNote()} )} @@ -174,11 +154,7 @@ export default class Result extends React.Component { series={byDayChartData} stacked={true} height={300} - options={{ - tooltip: Tooltip({ - formatter: formatTooltip, - }), - }} + tooltip={tooltipOptions} /> {this.renderNote()} diff --git a/src/sentry/static/sentry/app/views/organizationDiscover/result/utils.jsx b/src/sentry/static/sentry/app/views/organizationDiscover/result/utils.jsx index 2138c8c96bb865..bbb2fa53f59100 100644 --- a/src/sentry/static/sentry/app/views/organizationDiscover/result/utils.jsx +++ b/src/sentry/static/sentry/app/views/organizationDiscover/result/utils.jsx @@ -127,26 +127,6 @@ function formatDate(datetime) { return moment.utc(datetime * 1000).format('MMM Do'); } -export function formatTooltip(seriesParams) { - const label = seriesParams.length && seriesParams[0].axisValueLabel; - return [ - `
${truncateLabel(label)}
`, - seriesParams - .filter(s => s.data[1] !== null) - .map(s => `
${s.marker} ${truncateLabel(s.seriesName)}: ${s.data[1]}
`) - .join(''), - ].join(''); -} - -// Truncates labels for tooltip -function truncateLabel(seriesName) { - let result = seriesName; - if (seriesName.length > 80) { - result = seriesName.substring(0, 80) + '…'; - } - return result; -} - // Converts a value to a string for the chart label. This could // potentially cause incorrect grouping, e.g. if the value null and string // 'null' are both present in the same series they will be merged into 1 value