lineChartWidget.stories.tsx 9.8 KB

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