miniBarChart.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. // Import to ensure echarts components are loaded.
  2. import './components/markPoint';
  3. import {useTheme} from '@emotion/react';
  4. import set from 'lodash/set';
  5. import {getFormattedDate} from 'sentry/utils/dates';
  6. import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
  7. import {BarChart, BarChartProps, BarChartSeries} from './barChart';
  8. import type BaseChart from './baseChart';
  9. import {truncationFormatter} from './utils';
  10. type Marker = {
  11. color: string;
  12. name: string;
  13. value: string | number | Date;
  14. symbolSize?: number;
  15. };
  16. type ChartProps = React.ComponentProps<typeof BaseChart>;
  17. interface Props extends Omit<ChartProps, 'css' | 'colors' | 'series' | 'height'> {
  18. /**
  19. * Chart height
  20. */
  21. height: number;
  22. /**
  23. * Colors to use on the chart.
  24. */
  25. colors?: string[];
  26. /**
  27. * A list of colors to use on hover.
  28. * By default hover state will shift opacity from 0.6 to 1.0.
  29. * You can use this prop to also shift colors on hover.
  30. */
  31. emphasisColors?: string[];
  32. /**
  33. * Delay time for hiding tooltip, in ms.
  34. */
  35. hideDelay?: number;
  36. /**
  37. * Show max/min values on yAxis
  38. */
  39. labelYAxisExtents?: boolean;
  40. /**
  41. * A list of series to be rendered as markLine components on the chart
  42. * This is often used to indicate start/end markers on the xAxis
  43. */
  44. markers?: Marker[];
  45. series?: BarChartProps['series'];
  46. /**
  47. * Whether not we show a MarkLine label
  48. */
  49. showMarkLineLabel?: boolean;
  50. /**
  51. * Whether not the series should be stacked.
  52. *
  53. * Some of our stats endpoints return data where the 'total' series includes
  54. * breakdown data (issues). For these results `stacked` should be false.
  55. * Other endpoints return decomposed results that need to be stacked (outcomes).
  56. */
  57. stacked?: boolean;
  58. /**
  59. * Function to format tooltip values
  60. */
  61. tooltipFormatter?: (value: number) => string;
  62. /**
  63. * Whether timestamps are should be shown in UTC or local timezone.
  64. */
  65. utc?: boolean;
  66. }
  67. function MiniBarChart({
  68. markers,
  69. emphasisColors,
  70. series,
  71. hideDelay,
  72. tooltipFormatter,
  73. colors,
  74. stacked = false,
  75. labelYAxisExtents = false,
  76. showMarkLineLabel = false,
  77. height,
  78. ...props
  79. }: Props) {
  80. const {ref: _ref, ...barChartProps} = props;
  81. const theme = useTheme();
  82. const colorList = Array.isArray(colors)
  83. ? colors
  84. : [theme.gray200, theme.purple300, theme.purple300];
  85. let chartSeries: BarChartSeries[] = [];
  86. // Ensure bars overlap and that empty values display as we're disabling the axis lines.
  87. if (!!series?.length) {
  88. chartSeries = series.map((original, i: number) => {
  89. const updated: BarChartSeries = {
  90. ...original,
  91. cursor: 'normal',
  92. type: 'bar',
  93. };
  94. if (i === 0) {
  95. updated.barMinHeight = 1;
  96. if (stacked === false) {
  97. updated.barGap = '-100%';
  98. }
  99. }
  100. if (stacked) {
  101. updated.stack = 'stack1';
  102. }
  103. set(updated, 'itemStyle.color', colorList[i]);
  104. set(updated, 'itemStyle.opacity', 0.6);
  105. set(updated, 'emphasis.itemStyle.opacity', 1.0);
  106. set(updated, 'emphasis.itemStyle.color', emphasisColors?.[i] ?? colorList[i]);
  107. return updated;
  108. });
  109. }
  110. if (markers) {
  111. const markerTooltip = {
  112. show: true,
  113. trigger: 'item',
  114. formatter: ({data}) => {
  115. const time = getFormattedDate(data.coord[0], 'MMM D, YYYY LT', {
  116. local: !props.utc,
  117. });
  118. const name = truncationFormatter(data.name, props?.xAxis?.truncate);
  119. return [
  120. '<div class="tooltip-series">',
  121. `<div><span class="tooltip-label"><strong>${name}</strong></span></div>`,
  122. '</div>',
  123. '<div class="tooltip-date">',
  124. time,
  125. '</div>',
  126. '</div>',
  127. '<div class="tooltip-arrow"></div>',
  128. ].join('');
  129. },
  130. };
  131. const markPoint = {
  132. data: markers.map((marker: Marker) => ({
  133. name: marker.name,
  134. coord: [marker.value, 0],
  135. tooltip: markerTooltip,
  136. symbol: 'circle',
  137. symbolSize: marker.symbolSize ?? 8,
  138. itemStyle: {
  139. color: marker.color,
  140. borderColor: theme.background,
  141. },
  142. })),
  143. };
  144. chartSeries[0].markPoint = markPoint;
  145. }
  146. const yAxisOptions = labelYAxisExtents
  147. ? {
  148. showMinLabel: true,
  149. showMaxLabel: true,
  150. interval: Infinity,
  151. axisLabel: {
  152. formatter(value: number) {
  153. if (tooltipFormatter) {
  154. return tooltipFormatter(value);
  155. }
  156. return formatAbbreviatedNumber(value);
  157. },
  158. },
  159. }
  160. : {
  161. axisLabel: {
  162. show: false,
  163. },
  164. };
  165. const chartOptions: Omit<BarChartProps, 'series'> = {
  166. tooltip: {
  167. trigger: 'axis',
  168. hideDelay,
  169. valueFormatter: tooltipFormatter
  170. ? (value: number) => tooltipFormatter(value)
  171. : undefined,
  172. },
  173. yAxis: {
  174. max(value: {max: number; min: number}) {
  175. // This keeps small datasets from looking 'scary'
  176. // by having full bars for < 10 values.
  177. if (value.max < 10) {
  178. return 10;
  179. }
  180. // Adds extra spacing at the top of the chart canvas, ensuring the series doesn't hit the ceiling, leaving more empty space.
  181. // When the user hovers over an empty space, a tooltip with all series information is displayed.
  182. return (value.max * (height + 10)) / height;
  183. },
  184. splitLine: {
  185. show: false,
  186. },
  187. ...yAxisOptions,
  188. },
  189. grid: {
  190. // Offset to ensure there is room for the marker symbols at the
  191. // default size.
  192. top: labelYAxisExtents || showMarkLineLabel ? 6 : 0,
  193. bottom: markers || labelYAxisExtents || showMarkLineLabel ? 4 : 0,
  194. left: markers ? 8 : showMarkLineLabel ? 35 : 4,
  195. right: markers ? 4 : 0,
  196. },
  197. xAxis: {
  198. axisLine: {
  199. show: false,
  200. },
  201. axisTick: {
  202. show: false,
  203. alignWithLabel: true,
  204. },
  205. axisLabel: {
  206. show: false,
  207. },
  208. axisPointer: {
  209. type: 'line' as const,
  210. label: {
  211. show: false,
  212. },
  213. lineStyle: {
  214. width: 0,
  215. },
  216. },
  217. },
  218. options: {
  219. animation: false,
  220. },
  221. };
  222. return (
  223. <BarChart series={chartSeries} height={height} {...chartOptions} {...barChartProps} />
  224. );
  225. }
  226. export default MiniBarChart;