lineChartWidget.stories.tsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. import {Fragment} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import moment from 'moment-timezone';
  5. import JSXNode from 'sentry/components/stories/jsxNode';
  6. import SideBySide from 'sentry/components/stories/sideBySide';
  7. import SizingWindow from 'sentry/components/stories/sizingWindow';
  8. import storyBook from 'sentry/stories/storyBook';
  9. import type {DateString} from 'sentry/types/core';
  10. import usePageFilters from 'sentry/utils/usePageFilters';
  11. import type {Release, TimeseriesData} from '../common/types';
  12. import {LineChartWidget} from './lineChartWidget';
  13. import sampleDurationTimeSeries from './sampleDurationTimeSeries.json';
  14. import sampleThroughputTimeSeries from './sampleThroughputTimeSeries.json';
  15. import {shiftTimeserieToNow} from './shiftTimeserieToNow';
  16. const sampleDurationTimeSeries2 = {
  17. ...sampleDurationTimeSeries,
  18. field: 'p50(span.duration)',
  19. data: sampleDurationTimeSeries.data.map(datum => {
  20. return {
  21. ...datum,
  22. value: datum.value * 0.3 + 30 * Math.random(),
  23. };
  24. }),
  25. };
  26. export default storyBook(LineChartWidget, story => {
  27. story('Getting Started', () => {
  28. return (
  29. <Fragment>
  30. <p>
  31. <JSXNode name="LineChartWidget" /> is a Dashboard Widget Component. It displays
  32. a timeseries chart with one or more timeseries. Used to visualize data that
  33. changes over time in Project Details, Dashboards, Performance, and other UIs.
  34. </p>
  35. </Fragment>
  36. );
  37. });
  38. story('Visualization', () => {
  39. const {selection} = usePageFilters();
  40. const {datetime} = selection;
  41. const {start, end} = datetime;
  42. const throughputTimeSeries = toTimeSeriesSelection(
  43. sampleThroughputTimeSeries as unknown as TimeseriesData,
  44. start,
  45. end
  46. );
  47. const durationTimeSeries1 = toTimeSeriesSelection(
  48. sampleDurationTimeSeries as unknown as TimeseriesData,
  49. start,
  50. end
  51. );
  52. const durationTimeSeries2 = toTimeSeriesSelection(
  53. sampleDurationTimeSeries2,
  54. start,
  55. end
  56. );
  57. return (
  58. <Fragment>
  59. <p>
  60. The visualization of <JSXNode name="LineChartWidget" /> a line chart. It has
  61. some bells and whistles including automatic axes labels, and a hover tooltip.
  62. Like other widgets, it automatically fills the parent element. The{' '}
  63. <code>utc</code> prop controls whether the X Axis timestamps are shown in UTC or
  64. not.
  65. </p>
  66. <SmallSizingWindow>
  67. <LineChartWidget
  68. title="eps()"
  69. description="Number of events per second"
  70. timeseries={[throughputTimeSeries]}
  71. meta={{
  72. fields: {
  73. 'eps()': 'rate',
  74. },
  75. units: {
  76. 'eps()': '1/second',
  77. },
  78. }}
  79. />
  80. </SmallSizingWindow>
  81. <p>
  82. The <code>dataCompletenessDelay</code> prop indicates that this data is live,
  83. and the last few buckets might not have complete data. The delay is a number in
  84. seconds. Any data bucket that happens in that delay window will be plotted with
  85. a dotted line. By default the delay is <code>0</code>.
  86. </p>
  87. <SideBySide>
  88. <MediumWidget>
  89. <LineChartWidget
  90. title="span.duration"
  91. dataCompletenessDelay={60 * 60 * 3}
  92. timeseries={[
  93. shiftTimeserieToNow(durationTimeSeries1),
  94. shiftTimeserieToNow(durationTimeSeries2),
  95. ]}
  96. utc
  97. meta={{
  98. fields: {
  99. 'p99(span.duration)': 'duration',
  100. 'p50(span.duration)': 'duration',
  101. },
  102. units: {
  103. 'p99(span.duration)': 'millisecond',
  104. 'p50(span.duration)': 'millisecond',
  105. },
  106. }}
  107. />
  108. </MediumWidget>
  109. </SideBySide>
  110. </Fragment>
  111. );
  112. });
  113. story('State', () => {
  114. return (
  115. <Fragment>
  116. <p>
  117. <JSXNode name="LineChartWidget" /> supports the usual loading and error states.
  118. The loading state shows a spinner. The error state shows a message, and an
  119. optional "Retry" button.
  120. </p>
  121. <SideBySide>
  122. <SmallWidget>
  123. <LineChartWidget title="Loading Count" isLoading />
  124. </SmallWidget>
  125. <SmallWidget>
  126. <LineChartWidget title="Missing Count" />
  127. </SmallWidget>
  128. <SmallWidget>
  129. <LineChartWidget
  130. title="Count Error"
  131. error={new Error('Something went wrong!')}
  132. />
  133. </SmallWidget>
  134. <SmallWidget>
  135. <LineChartWidget
  136. title="Data Error"
  137. error={new Error('Something went wrong!')}
  138. onRetry={() => {}}
  139. />
  140. </SmallWidget>
  141. </SideBySide>
  142. </Fragment>
  143. );
  144. });
  145. story('Colors', () => {
  146. const theme = useTheme();
  147. return (
  148. <Fragment>
  149. <p>
  150. You can control the color of each timeseries by setting the <code>color</code>{' '}
  151. attribute to a string that contains a valid hex color code.
  152. </p>
  153. <MediumWidget>
  154. <LineChartWidget
  155. title="error_rate()"
  156. description="Rate of Errors"
  157. timeseries={[
  158. {
  159. ...sampleThroughputTimeSeries,
  160. field: 'error_rate()',
  161. color: theme.error,
  162. } as unknown as TimeseriesData,
  163. ]}
  164. meta={{
  165. fields: {
  166. 'error_rate()': 'rate',
  167. },
  168. units: {
  169. 'error_rate()': '1/second',
  170. },
  171. }}
  172. />
  173. </MediumWidget>
  174. </Fragment>
  175. );
  176. });
  177. story('Releases', () => {
  178. const releases = [
  179. {
  180. version: 'ui@0.1.2',
  181. timestamp: sampleThroughputTimeSeries.data.at(2)?.timestamp,
  182. },
  183. {
  184. version: 'ui@0.1.3',
  185. timestamp: sampleThroughputTimeSeries.data.at(20)?.timestamp,
  186. },
  187. ].filter(hasTimestamp);
  188. return (
  189. <Fragment>
  190. <p>
  191. <JSXNode name="LineChartWidget" /> supports the <code>releases</code> prop. If
  192. passed in, the widget will plot every release as a vertical line that overlays
  193. the chart data. Clicking on a release line will open the release details page.
  194. </p>
  195. <MediumWidget>
  196. <LineChartWidget
  197. title="error_rate()"
  198. timeseries={[
  199. {
  200. ...sampleThroughputTimeSeries,
  201. field: 'error_rate()',
  202. } as unknown as TimeseriesData,
  203. ]}
  204. releases={releases}
  205. meta={{
  206. fields: {
  207. 'error_rate()': 'rate',
  208. },
  209. units: {
  210. 'error_rate()': '1/second',
  211. },
  212. }}
  213. />
  214. </MediumWidget>
  215. </Fragment>
  216. );
  217. });
  218. });
  219. const MediumWidget = styled('div')`
  220. width: 420px;
  221. height: 250px;
  222. `;
  223. const SmallWidget = styled('div')`
  224. width: 360px;
  225. height: 160px;
  226. `;
  227. const SmallSizingWindow = styled(SizingWindow)`
  228. width: 50%;
  229. height: 300px;
  230. `;
  231. function toTimeSeriesSelection(
  232. timeSeries: TimeseriesData,
  233. start: DateString | null,
  234. end: DateString | null
  235. ): TimeseriesData {
  236. return {
  237. ...timeSeries,
  238. data: timeSeries.data.filter(datum => {
  239. if (start && moment(datum.timestamp).isBefore(moment.utc(start))) {
  240. return false;
  241. }
  242. if (end && moment(datum.timestamp).isAfter(moment.utc(end))) {
  243. return false;
  244. }
  245. return true;
  246. }),
  247. };
  248. }
  249. function hasTimestamp(release: Partial<Release>): release is Release {
  250. return Boolean(release?.timestamp);
  251. }