miniBarChart.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. // Import to ensure echarts components are loaded.
  2. import './components/markPoint';
  3. import {useMemo} from 'react';
  4. import {useTheme} from '@emotion/react';
  5. import type {GridComponentOption} from 'echarts';
  6. import set from 'lodash/set';
  7. import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
  8. import type {BarChartProps, BarChartSeries} from './barChart';
  9. import {BarChart} from './barChart';
  10. import type {BaseChartProps} from './baseChart';
  11. function makeBaseChartOptions({
  12. animateBars,
  13. height,
  14. hideDelay,
  15. tooltipFormatter,
  16. labelYAxisExtents,
  17. showMarkLineLabel,
  18. markLineLabelSide,
  19. grid,
  20. yAxisOptions,
  21. showXAxisLine,
  22. xAxisLineColor,
  23. }: {
  24. animateBars: boolean;
  25. height: number;
  26. markLineLabelSide: 'right' | 'left';
  27. showXAxisLine: boolean;
  28. xAxisLineColor: string;
  29. grid?: GridComponentOption;
  30. hideDelay?: number;
  31. labelYAxisExtents?: boolean;
  32. showMarkLineLabel?: boolean;
  33. tooltipFormatter?: (value: number) => string;
  34. yAxisOptions?: BarChartProps['yAxis'];
  35. }): Omit<BarChartProps, 'series' | 'barOpacity'> {
  36. return {
  37. tooltip: {
  38. trigger: 'axis',
  39. hideDelay,
  40. valueFormatter: tooltipFormatter
  41. ? (value: number) => tooltipFormatter(value)
  42. : undefined,
  43. },
  44. yAxis: {
  45. max: getYAxisMaxFn(height),
  46. splitLine: {
  47. show: false,
  48. },
  49. ...yAxisOptions,
  50. },
  51. grid: grid ?? {
  52. // Offset to ensure there is room for the marker symbols at the
  53. // default size.
  54. top: labelYAxisExtents || showMarkLineLabel ? 6 : 0,
  55. bottom: labelYAxisExtents || showMarkLineLabel ? 4 : 0,
  56. left: markLineLabelSide === 'left' ? (showMarkLineLabel ? 35 : 4) : 0,
  57. right: markLineLabelSide === 'right' ? (showMarkLineLabel ? 25 : 4) : 0,
  58. },
  59. xAxis: {
  60. axisLine: showXAxisLine
  61. ? {
  62. show: true,
  63. lineStyle: {
  64. color: xAxisLineColor,
  65. },
  66. onZero: false, // Enables offset for x-axis line
  67. }
  68. : {show: false},
  69. axisTick: {
  70. show: false,
  71. alignWithLabel: true,
  72. },
  73. offset: showXAxisLine ? -1 : 0,
  74. axisLabel: {
  75. show: false,
  76. },
  77. axisPointer: {
  78. type: 'line' as const,
  79. label: {
  80. show: false,
  81. },
  82. lineStyle: {
  83. width: 0,
  84. },
  85. },
  86. },
  87. options: animateBars
  88. ? {
  89. animation: true,
  90. animationEasing: 'circularOut',
  91. }
  92. : {
  93. animation: false,
  94. },
  95. };
  96. }
  97. function makeLabelYAxisOptions(tooltipFormatter: Props['tooltipFormatter']) {
  98. return {
  99. showMinLabel: true,
  100. showMaxLabel: true,
  101. interval: Infinity,
  102. axisLabel: {
  103. formatter(value: number) {
  104. if (tooltipFormatter) {
  105. return tooltipFormatter(value);
  106. }
  107. return formatAbbreviatedNumber(value);
  108. },
  109. },
  110. };
  111. }
  112. const noLabelYAxisOptions = {
  113. axisLabel: {
  114. show: false,
  115. },
  116. };
  117. interface Props extends Omit<BaseChartProps, 'css' | 'colors' | 'series' | 'height'> {
  118. /**
  119. * Chart height
  120. */
  121. height: number;
  122. /**
  123. * Whether to animate the bars on initial render.
  124. * If true, bars will rise from the x-axis to their final height.
  125. */
  126. animateBars?: boolean;
  127. /**
  128. * Opacity of each bar in the graph (0-1)
  129. */
  130. barOpacity?: number;
  131. /**
  132. * Colors to use on the chart.
  133. */
  134. colors?: string[];
  135. /**
  136. * A list of colors to use on hover.
  137. * By default hover state will shift opacity from 0.6 to 1.0.
  138. * You can use this prop to also shift colors on hover.
  139. */
  140. emphasisColors?: string[];
  141. /**
  142. * Override the default grid padding
  143. */
  144. grid?: GridComponentOption;
  145. /**
  146. * Delay time for hiding tooltip, in ms.
  147. */
  148. hideDelay?: number;
  149. /**
  150. * Whether to hide the bar for zero values in the chart.
  151. */
  152. hideZeros?: boolean;
  153. /**
  154. * Show max/min values on yAxis
  155. */
  156. labelYAxisExtents?: boolean;
  157. /**
  158. * Which side of the chart the mark line label shows on.
  159. * Requires `showMarkLineLabel` to be true.
  160. */
  161. markLineLabelSide?: 'right' | 'left';
  162. /**
  163. * Series data to display
  164. */
  165. series?: BarChartProps['series'];
  166. /**
  167. * Whether not we show a MarkLine label
  168. */
  169. showMarkLineLabel?: boolean;
  170. /**
  171. * Whether or not to show the x-axis line
  172. */
  173. showXAxisLine?: boolean;
  174. /**
  175. * Whether not the series should be stacked.
  176. *
  177. * Some of our stats endpoints return data where the 'total' series includes
  178. * breakdown data (issues). For these results `stacked` should be false.
  179. * Other endpoints return decomposed results that need to be stacked (outcomes).
  180. */
  181. stacked?: boolean;
  182. /**
  183. * Function to format tooltip values
  184. */
  185. tooltipFormatter?: (value: number) => string;
  186. /**
  187. * Whether timestamps are should be shown in UTC or local timezone.
  188. */
  189. utc?: boolean;
  190. }
  191. export function getYAxisMaxFn(height: number) {
  192. return (value: {max: number; min: number}) => {
  193. // This keeps small datasets from looking 'scary'
  194. // by having full bars for < 10 values.
  195. if (value.max < 10) {
  196. return 10;
  197. }
  198. // Adds extra spacing at the top of the chart canvas, ensuring the series doesn't hit the ceiling, leaving more empty space.
  199. // When the user hovers over an empty space, a tooltip with all series information is displayed.
  200. return (value.max * (height + 10)) / height;
  201. };
  202. }
  203. function MiniBarChart({
  204. animateBars = false,
  205. barOpacity = 0.6,
  206. emphasisColors,
  207. series,
  208. hideDelay,
  209. hideZeros = false,
  210. tooltipFormatter,
  211. colors,
  212. stacked = false,
  213. labelYAxisExtents = false,
  214. showMarkLineLabel = false,
  215. markLineLabelSide = 'left',
  216. showXAxisLine = false,
  217. height,
  218. grid,
  219. ...props
  220. }: Props) {
  221. const theme = useTheme();
  222. const xAxisLineColor: string = theme.gray200;
  223. const updatedSeries: BarChartSeries[] = useMemo(() => {
  224. if (!series?.length) {
  225. return [];
  226. }
  227. const chartSeries: BarChartSeries[] = [];
  228. const colorList = Array.isArray(colors)
  229. ? colors
  230. : [theme.gray200, theme.purple300, theme.purple300];
  231. for (let i = 0; i < series.length; i++) {
  232. const original = series[i];
  233. const updated: BarChartSeries = {
  234. ...original,
  235. cursor: 'normal',
  236. type: 'bar',
  237. };
  238. if (i === 0) {
  239. updated.barMinHeight = 1;
  240. if (stacked === false) {
  241. updated.barGap = '-100%';
  242. }
  243. }
  244. if (stacked) {
  245. updated.stack = 'stack1';
  246. }
  247. set(updated, 'itemStyle.color', colorList[i]);
  248. set(updated, 'itemStyle.borderRadius', [1, 1, 0, 0]); // Rounded corners on top of the bar
  249. set(updated, 'emphasis.itemStyle.color', emphasisColors?.[i] ?? colorList[i]);
  250. chartSeries.push(updated);
  251. }
  252. return chartSeries;
  253. }, [series, emphasisColors, stacked, colors, theme.gray200, theme.purple300]);
  254. const chartOptions = useMemo(() => {
  255. const yAxisOptions = labelYAxisExtents
  256. ? makeLabelYAxisOptions(tooltipFormatter)
  257. : noLabelYAxisOptions;
  258. const options = makeBaseChartOptions({
  259. animateBars,
  260. height,
  261. hideDelay,
  262. tooltipFormatter,
  263. labelYAxisExtents,
  264. showMarkLineLabel,
  265. markLineLabelSide,
  266. grid,
  267. yAxisOptions,
  268. showXAxisLine,
  269. xAxisLineColor,
  270. });
  271. return options;
  272. }, [
  273. animateBars,
  274. grid,
  275. height,
  276. hideDelay,
  277. labelYAxisExtents,
  278. markLineLabelSide,
  279. showMarkLineLabel,
  280. showXAxisLine,
  281. tooltipFormatter,
  282. xAxisLineColor,
  283. ]);
  284. return (
  285. <BarChart
  286. barOpacity={barOpacity}
  287. hideZeros={hideZeros}
  288. series={updatedSeries}
  289. height={height}
  290. {...chartOptions}
  291. {...props}
  292. />
  293. );
  294. }
  295. export default MiniBarChart;