miniBarChart.tsx 6.3 KB

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