miniBarChart.tsx 5.8 KB

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