lineChartWidget.stories.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  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.
  63. </p>
  64. <SmallSizingWindow>
  65. <LineChartWidget
  66. title="eps()"
  67. description="Number of events per second"
  68. timeseries={[throughputTimeSeries]}
  69. meta={{
  70. fields: {
  71. 'eps()': 'rate',
  72. },
  73. units: {
  74. 'eps()': '1/second',
  75. },
  76. }}
  77. />
  78. </SmallSizingWindow>
  79. <p>
  80. The <code>dataCompletenessDelay</code> prop indicates that this data is live,
  81. and the last few buckets might not have complete data. The delay is a number in
  82. seconds. Any data bucket that happens in that delay window will be plotted with
  83. a dotted line. By default the delay is <code>0</code>.
  84. </p>
  85. <SideBySide>
  86. <MediumWidget>
  87. <LineChartWidget
  88. title="span.duration"
  89. dataCompletenessDelay={60 * 60 * 3}
  90. timeseries={[
  91. shiftTimeserieToNow(durationTimeSeries1),
  92. shiftTimeserieToNow(durationTimeSeries2),
  93. ]}
  94. meta={{
  95. fields: {
  96. 'p99(span.duration)': 'duration',
  97. 'p50(span.duration)': 'duration',
  98. },
  99. units: {
  100. 'p99(span.duration)': 'millisecond',
  101. 'p50(span.duration)': 'millisecond',
  102. },
  103. }}
  104. />
  105. </MediumWidget>
  106. </SideBySide>
  107. </Fragment>
  108. );
  109. });
  110. story('State', () => {
  111. return (
  112. <Fragment>
  113. <p>
  114. <JSXNode name="LineChartWidget" /> supports the usual loading and error states.
  115. The loading state shows a spinner. The error state shows a message, and an
  116. optional "Retry" button.
  117. </p>
  118. <SideBySide>
  119. <SmallWidget>
  120. <LineChartWidget title="Loading Count" isLoading />
  121. </SmallWidget>
  122. <SmallWidget>
  123. <LineChartWidget title="Missing Count" />
  124. </SmallWidget>
  125. <SmallWidget>
  126. <LineChartWidget
  127. title="Count Error"
  128. error={new Error('Something went wrong!')}
  129. />
  130. </SmallWidget>
  131. <SmallWidget>
  132. <LineChartWidget
  133. title="Data Error"
  134. error={new Error('Something went wrong!')}
  135. onRetry={() => {}}
  136. />
  137. </SmallWidget>
  138. </SideBySide>
  139. </Fragment>
  140. );
  141. });
  142. story('Colors', () => {
  143. const theme = useTheme();
  144. return (
  145. <Fragment>
  146. <p>
  147. You can control the color of each timeseries by setting the <code>color</code>{' '}
  148. attribute to a string that contains a valid hex color code.
  149. </p>
  150. <MediumWidget>
  151. <LineChartWidget
  152. title="error_rate()"
  153. description="Rate of Errors"
  154. timeseries={[
  155. {
  156. ...sampleThroughputTimeSeries,
  157. field: 'error_rate()',
  158. color: theme.error,
  159. } as unknown as TimeseriesData,
  160. ]}
  161. meta={{
  162. fields: {
  163. 'error_rate()': 'rate',
  164. },
  165. units: {
  166. 'error_rate()': '1/second',
  167. },
  168. }}
  169. />
  170. </MediumWidget>
  171. </Fragment>
  172. );
  173. });
  174. story('Releases', () => {
  175. const releases = [
  176. {
  177. version: 'ui@0.1.2',
  178. timestamp: sampleThroughputTimeSeries.data.at(2)?.timestamp,
  179. },
  180. {
  181. version: 'ui@0.1.3',
  182. timestamp: sampleThroughputTimeSeries.data.at(20)?.timestamp,
  183. },
  184. ].filter(hasTimestamp);
  185. return (
  186. <Fragment>
  187. <p>
  188. <JSXNode name="LineChartWidget" /> supports the <code>releases</code> prop. If
  189. passed in, the widget will plot every release as a vertical line that overlays
  190. the chart data. Clicking on a release line will open the release details page.
  191. </p>
  192. <MediumWidget>
  193. <LineChartWidget
  194. title="error_rate()"
  195. timeseries={[
  196. {
  197. ...sampleThroughputTimeSeries,
  198. field: 'error_rate()',
  199. } as unknown as TimeseriesData,
  200. ]}
  201. releases={releases}
  202. meta={{
  203. fields: {
  204. 'error_rate()': 'rate',
  205. },
  206. units: {
  207. 'error_rate()': '1/second',
  208. },
  209. }}
  210. />
  211. </MediumWidget>
  212. </Fragment>
  213. );
  214. });
  215. });
  216. const MediumWidget = styled('div')`
  217. width: 420px;
  218. height: 250px;
  219. `;
  220. const SmallWidget = styled('div')`
  221. width: 360px;
  222. height: 160px;
  223. `;
  224. const SmallSizingWindow = styled(SizingWindow)`
  225. width: 50%;
  226. height: 300px;
  227. `;
  228. function toTimeSeriesSelection(
  229. timeSeries: TimeseriesData,
  230. start: DateString | null,
  231. end: DateString | null
  232. ): TimeseriesData {
  233. return {
  234. ...timeSeries,
  235. data: timeSeries.data.filter(datum => {
  236. if (start && moment(datum.timestamp).isBefore(moment.utc(start))) {
  237. return false;
  238. }
  239. if (end && moment(datum.timestamp).isAfter(moment.utc(end))) {
  240. return false;
  241. }
  242. return true;
  243. }),
  244. };
  245. }
  246. function hasTimestamp(release: Partial<Release>): release is Release {
  247. return Boolean(release?.timestamp);
  248. }