diff --git a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx
index 9a6d18739e1772..38b86bbe8959d8 100644
--- a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx
+++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx
@@ -1,221 +1,20 @@
import {Fragment} from 'react';
-import {useTheme} from '@emotion/react';
-import styled from '@emotion/styled';
-import moment from 'moment-timezone';
import JSXNode from 'sentry/components/stories/jsxNode';
-import SideBySide from 'sentry/components/stories/sideBySide';
-import SizingWindow from 'sentry/components/stories/sizingWindow';
import storyBook from 'sentry/stories/storyBook';
-import type {DateString} from 'sentry/types/core';
-import usePageFilters from 'sentry/utils/usePageFilters';
-
-import type {Release, TimeSeries} from '../common/types';
-import {shiftTimeserieToNow} from '../timeSeriesWidget/shiftTimeserieToNow';
-
-import {sampleLatencyTimeSeries} from './fixtures/sampleLatencyTimeSeries';
-import {sampleSpanDurationTimeSeries} from './fixtures/sampleSpanDurationTimeSeries';
-import {AreaChartWidget} from './areaChartWidget';
export default storyBook('AreaChartWidget', story => {
story('Getting Started', () => {
return (
- is a Dashboard Widget Component. It displays
- a timeseries chart with multiple timeseries, and the timeseries are stacked.
- Each timeseries is shown using a solid block of color. This chart is used to
- visualize multiple timeseries that represent parts of something. For example, a
- chart that shows time spent in the app broken down by component. In all other
- ways, it behaves like , though it doesn't
- support features like "Previous Period Data".
-
-
- NOTE: This chart is not appropriate for showing a single timeseries!
- You should use instead.
+ 🚨 is deprecated! Instead, see the stories
+ for , which explain in detail how to compose your own
+ widgets from standard components. If you want information on how to render a
+ time series visualization, see the stories for
+ .
);
});
-
- story('Visualization', () => {
- const {selection} = usePageFilters();
- const {datetime} = selection;
- const {start, end} = datetime;
-
- const latencyTimeSeries = toTimeSeriesSelection(sampleLatencyTimeSeries, start, end);
-
- const spanDurationTimeSeries = toTimeSeriesSelection(
- sampleSpanDurationTimeSeries,
- start,
- end
- );
-
- return (
-
-
- The visualization of a stacked area chart. It
- has some bells and whistles including automatic axes labels, and a hover
- tooltip. Like other widgets, it automatically fills the parent element.
-
-
-
-
-
-
- The dataCompletenessDelay
prop indicates that this data is live,
- and the last few buckets might not have complete data. The delay is a number in
- seconds. Any data bucket that happens in that delay window will be plotted with
- a fainter fill. By default the delay is 0
.
-
-
-
-
-
-
-
-
- );
- });
-
- story('State', () => {
- return (
-
-
- supports the usual loading and error states.
- The loading state shows a spinner. The error state shows a message, and an
- optional "Retry" button.
-
-
-
-
-
-
-
-
-
-
-
-
-
- {}}
- />
-
-
-
- );
- });
-
- story('Colors', () => {
- const theme = useTheme();
-
- return (
-
-
- You can control the color of each timeseries by setting the color
{' '}
- attribute to a string that contains a valid hex color code.
-
-
-
-
-
-
- );
- });
-
- story('Releases', () => {
- const releases = [
- {
- version: 'ui@0.1.2',
- timestamp: sampleLatencyTimeSeries.data.at(2)?.timestamp,
- },
- {
- version: 'ui@0.1.3',
- timestamp: sampleLatencyTimeSeries.data.at(20)?.timestamp,
- },
- ].filter(hasTimestamp);
-
- return (
-
-
- supports the releases
prop. If
- passed in, the widget will plot every release as a vertical line that overlays
- the chart data. Clicking on a release line will open the release details page.
-
-
-
-
-
-
- );
- });
});
-
-const MediumWidget = styled('div')`
- width: 420px;
- height: 250px;
-`;
-
-const SmallWidget = styled('div')`
- width: 360px;
- height: 160px;
-`;
-
-const SmallSizingWindow = styled(SizingWindow)`
- width: 50%;
- height: 300px;
-`;
-
-function toTimeSeriesSelection(
- timeSeries: TimeSeries,
- start: DateString | null,
- end: DateString | null
-): TimeSeries {
- return {
- ...timeSeries,
- data: timeSeries.data.filter(datum => {
- if (start && moment(datum.timestamp).isBefore(moment.utc(start))) {
- return false;
- }
-
- if (end && moment(datum.timestamp).isAfter(moment.utc(end))) {
- return false;
- }
-
- return true;
- }),
- };
-}
-
-function hasTimestamp(release: Partial): release is Release {
- return Boolean(release?.timestamp);
-}
diff --git a/static/app/views/dashboards/widgets/barChartWidget/barChartWidget.stories.tsx b/static/app/views/dashboards/widgets/barChartWidget/barChartWidget.stories.tsx
index e35a4b340e7ebc..6f76bb5a01c43c 100644
--- a/static/app/views/dashboards/widgets/barChartWidget/barChartWidget.stories.tsx
+++ b/static/app/views/dashboards/widgets/barChartWidget/barChartWidget.stories.tsx
@@ -1,114 +1,20 @@
import {Fragment} from 'react';
-import styled from '@emotion/styled';
-import moment from 'moment-timezone';
import JSXNode from 'sentry/components/stories/jsxNode';
-import SideBySide from 'sentry/components/stories/sideBySide';
-import SizingWindow from 'sentry/components/stories/sizingWindow';
import storyBook from 'sentry/stories/storyBook';
-import type {DateString} from 'sentry/types/core';
-import usePageFilters from 'sentry/utils/usePageFilters';
-
-import type {TimeSeries} from '../common/types';
-import {shiftTimeserieToNow} from '../timeSeriesWidget/shiftTimeserieToNow';
-
-import {sampleLatencyTimeSeries} from './fixtures/sampleLatencyTimeSeries';
-import {sampleSpanDurationTimeSeries} from './fixtures/sampleSpanDurationTimeSeries';
-import {BarChartWidget} from './barChartWidget';
export default storyBook('BarChartWidget', story => {
story('Getting Started', () => {
return (
- is a Dashboard Widget Component. It displays a
- timeseries chart with multiple timeseries, and the timeseries and discontinuous.
- In all other ways, it behaves like
-
-
- NOTE: Prefer and{' '}
- for timeseries visualizations! This should be
- used for discontinuous categorized data.
-
-
- );
- });
-
- story('Visualization', () => {
- const {selection} = usePageFilters();
- const {datetime} = selection;
- const {start, end} = datetime;
-
- const latencyTimeSeries = toTimeSeriesSelection(sampleLatencyTimeSeries, start, end);
-
- const spanDurationTimeSeries = toTimeSeriesSelection(
- sampleSpanDurationTimeSeries,
- start,
- end
- );
-
- return (
-
-
- The visualization of is a bar chart. It has
- some bells and whistles including automatic axes labels, and a hover tooltip.
- Like other widgets, it automatically fills the parent element.
+ 🚨 is deprecated! Instead, see the stories for{' '}
+ , which explain in detail how to compose your own
+ widgets from standard components. If you want information on how to render a
+ time series visualization, see the stories for
+ .
-
- The stacked
boolean prop controls stacking. Bar charts are not
- stacked by default.
-
-
-
-
-
-
-
-
-
);
});
});
-
-const SmallSizingWindow = styled(SizingWindow)`
- width: 50%;
- height: 300px;
-`;
-
-function toTimeSeriesSelection(
- timeSeries: TimeSeries,
- start: DateString | null,
- end: DateString | null
-): TimeSeries {
- return {
- ...timeSeries,
- data: timeSeries.data.filter(datum => {
- if (start && moment(datum.timestamp).isBefore(moment.utc(start))) {
- return false;
- }
-
- if (end && moment(datum.timestamp).isAfter(moment.utc(end))) {
- return false;
- }
-
- return true;
- }),
- };
-}
diff --git a/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidget.stories.tsx b/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidget.stories.tsx
index 2b34137c8b7656..6e22f24598678e 100644
--- a/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidget.stories.tsx
+++ b/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidget.stories.tsx
@@ -1,372 +1,20 @@
import {Fragment} from 'react';
-import styled from '@emotion/styled';
import JSXNode from 'sentry/components/stories/jsxNode';
-import JSXProperty from 'sentry/components/stories/jsxProperty';
-import SideBySide from 'sentry/components/stories/sideBySide';
-import SizingWindow from 'sentry/components/stories/sizingWindow';
import storyBook from 'sentry/stories/storyBook';
-import {BigNumberWidget} from 'sentry/views/dashboards/widgets/bigNumberWidget/bigNumberWidget';
export default storyBook('BigNumberWidget', story => {
story('Getting Started', () => {
return (
- is a Dashboard Widget Component. It displays
- a single large value. Used in places like Dashboards Big Number widgets, Project
- Details pages, and Organization Stats pages.
+ 🚨 is deprecated! Instead, see the stories
+ for , which explain in detail how to compose your own
+ widgets from standard components. If you want information on how to render a big
+ auto-scaling number (or string), see the stories for
+ .
);
});
-
- story('Visualization', () => {
- return (
-
-
- The visualization of a large number, just
- like it says on the tin. Depending on the value passed to it, it intelligently
- rounds and humanizes the results. If the number is humanized, hovering over the
- visualization shows a tooltip with the full value.
-
-
-
- also supports string values. This is not
- commonly used, but it's capable of rendering timestamps and in fact most fields
- defined in our field renderer pipeline
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- The maximumValue
prop allows setting the maximum displayable value.
- e.g., imagine a widget that displays a count. A count of more than a million is
- too expensive for the API to compute, so the API returns a maximum of 1,000,000.
- If the API returns exactly 1,000,000, that means the actual number is unknown,
- something higher than the max. Setting{' '}
- will show >1m.
-
-
-
-
-
-
-
- );
- });
-
- story('State', () => {
- return (
-
-
- supports the usual loading and error states.
- The loading state shows a simple placeholder. The error state also shows an
- optional "Retry" button.
-
-
-
-
-
-
-
-
-
-
-
-
-
- {}}
- />
-
-
-
- The contents of the error adjust slightly as the widget gets bigger.
-
-
-
- {}}
- />
-
-
-
- );
- });
-
- story('Previous Period Data', () => {
- return (
-
-
- shows the difference of the current value and
- the previous period value as the difference between the two values, in small
- text next to the main value.
-
-
-
- The preferredPolarity
prop controls the color of the comparison
- string. Setting mean that a
- higher number is better and will paint increases in the value green. Vice
- versa with negative polarity. Omitting a preferred polarity will prevent
- colorization.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- });
-
- story('Thresholds', () => {
- const meta = {
- fields: {
- 'eps()': 'rate',
- },
- units: {
- 'eps()': '1/second',
- },
- };
-
- const thresholds = {
- max_values: {
- max1: 20,
- max2: 50,
- },
- unit: '1/second',
- };
-
- return (
-
-
- supports a thresholds
prop. If
- specified, the value in the widget will be evaluated against these thresholds,
- and indicated using a colorful circle next to the value.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- The thresholds respect the preferred polarity. By default, the preferred
- polarity is positive (higher numbers are good).
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- });
});
-
-const SmallSizingWindow = styled(SizingWindow)`
- width: auto;
- height: 200px;
-`;
-
-const SmallWidget = styled('div')`
- width: 250px;
-`;
-
-const MediumWidget = styled('div')`
- width: 420px;
-`;
diff --git a/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization.stories.tsx b/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization.stories.tsx
new file mode 100644
index 00000000000000..ef4ce0ff0aecf0
--- /dev/null
+++ b/static/app/views/dashboards/widgets/bigNumberWidget/bigNumberWidgetVisualization.stories.tsx
@@ -0,0 +1,374 @@
+import {Fragment} from 'react';
+import styled from '@emotion/styled';
+
+import JSXNode from 'sentry/components/stories/jsxNode';
+import JSXProperty from 'sentry/components/stories/jsxProperty';
+import SideBySide from 'sentry/components/stories/sideBySide';
+import SizingWindow from 'sentry/components/stories/sizingWindow';
+import storyBook from 'sentry/stories/storyBook';
+import {space} from 'sentry/styles/space';
+
+import {BigNumberWidgetVisualization} from './bigNumberWidgetVisualization';
+
+export default storyBook('BigNumberWidgetVisualization', story => {
+ story('Getting Started', () => {
+ return (
+
+
+ is a visualization used for "Big
+ Number" widgets across the app. It displays a single large value. Used in places
+ like Dashboards Big Number widgets, Project Details pages, and Organization
+ Stats pages.
+
+
+ It has features like:
+
+ intelligent value formatting
+ auto-filling the parent
+ full value shown in tooltip
+
+
+
+ You should use this component for showing large significant numbers in the UI!
+ It's highly customizable, with features like previous period data comparison,
+ thresholds, and more.
+
+
+ );
+ });
+
+ story('Basic Visualization', () => {
+ return (
+
+
+ The visualization of a large
+ number, just like it says on the tin. Depending on the value passed to it, it
+ intelligently rounds and humanizes the results. If the number is humanized,
+ hovering over the visualization shows a tooltip with the full value.
+
+
+
+ also supports string values.
+ This is not commonly used, but it's capable of rendering timestamps and in fact
+ most fields defined in our field renderer pipeline
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ });
+
+ story('Loading Placeholder', () => {
+ return (
+
+
+ includes a loading placeholder.
+ You can use it via{' '}
+
+
+
+
+
+
+
+ );
+ });
+
+ story('Maximum Value', () => {
+ return (
+
+
+ The maximumValue
prop allows setting the maximum displayable value.
+ e.g., imagine a widget that displays a count. A count of more than a million is
+ too expensive for the API to compute, so the API returns a maximum of 1,000,000.
+ If the API returns exactly 1,000,000, that means the actual number is unknown,
+ something higher than the max. Setting{' '}
+ will show >1m.
+
+
+
+
+
+
+
+ );
+ });
+
+ story('Previous Period Data', () => {
+ return (
+
+
+ can show the difference of the
+ current value and the previous period value as the difference between the two
+ values, in small text next to the main value.
+
+
+
+ The preferredPolarity
prop controls the color of the comparison
+ string. Setting mean that a
+ higher number is better and will paint increases in the value green. Vice
+ versa with negative polarity. Omitting a preferred polarity will prevent
+ colorization.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ });
+
+ story('Thresholds', () => {
+ const meta = {
+ fields: {
+ 'eps()': 'rate',
+ },
+ units: {
+ 'eps()': '1/second',
+ },
+ };
+
+ const thresholds = {
+ max_values: {
+ max1: 20,
+ max2: 50,
+ },
+ unit: '1/second',
+ };
+
+ return (
+
+
+ supports a{' '}
+ thresholds
prop. If specified, the value in the widget will be
+ evaluated against these thresholds, and indicated using a colorful circle next
+ to the value.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ The thresholds respect the preferred polarity. By default, the preferred
+ polarity is positive (higher numbers are good).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ });
+});
+
+const SmallSizingWindow = styled(SizingWindow)`
+ width: auto;
+ height: 200px;
+`;
+
+interface SmallWidgetProps {
+ children: React.ReactNode;
+}
+function SmallWidget(props: SmallWidgetProps) {
+ return (
+
+ {props.children}
+
+ );
+}
+
+const Padded = styled('div')`
+ border-radius: ${p => p.theme.borderRadius};
+ border: ${p => `1px solid ${p.theme.border}`};
+ padding: ${space(2)} ${space(1)};
+ width: 250px;
+ height: 80px;
+`;
+
+const Container = styled('div')`
+ width: 100%;
+ height: 100%;
+ position: relative;
+`;
diff --git a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx
index 49182c2013a392..7ebf3e8201d0b6 100644
--- a/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx
+++ b/static/app/views/dashboards/widgets/lineChartWidget/lineChartWidget.stories.tsx
@@ -1,359 +1,20 @@
-import {Fragment, useState} from 'react';
-import {useTheme} from '@emotion/react';
-import styled from '@emotion/styled';
-import moment from 'moment-timezone';
+import {Fragment} from 'react';
-import {Button} from 'sentry/components/button';
import JSXNode from 'sentry/components/stories/jsxNode';
-import SideBySide from 'sentry/components/stories/sideBySide';
-import SizingWindow from 'sentry/components/stories/sizingWindow';
import storyBook from 'sentry/stories/storyBook';
-import type {DateString} from 'sentry/types/core';
-import usePageFilters from 'sentry/utils/usePageFilters';
-
-import type {Release, TimeSeries, TimeseriesSelection} from '../common/types';
-import {shiftTimeserieToNow} from '../timeSeriesWidget/shiftTimeserieToNow';
-
-import {sampleDurationTimeSeries} from './fixtures/sampleDurationTimeSeries';
-import {sampleThroughputTimeSeries} from './fixtures/sampleThroughputTimeSeries';
-import {LineChartWidget} from './lineChartWidget';
-
-const sampleDurationTimeSeries2 = {
- ...sampleDurationTimeSeries,
- field: 'p50(span.duration)',
- data: sampleDurationTimeSeries.data.map(datum => {
- return {
- ...datum,
- value: datum.value * 0.3 + 30 * Math.random(),
- };
- }),
- meta: {
- fields: {
- 'p50(span.duration)': 'duration',
- },
- units: {
- 'p50(span.duration)': 'millisecond',
- },
- },
-};
-
-const sectionSize = sampleThroughputTimeSeries.data.length / 10;
-const sectionStart = sectionSize * 2;
-const sectionEnd = sectionSize * 3;
-const sparseThroughputTimeSeries = {
- ...sampleThroughputTimeSeries,
- data: sampleThroughputTimeSeries.data.map((datum, index) => {
- if (index > sectionStart && index < sectionEnd) {
- return {
- ...datum,
- value: null,
- };
- }
- return datum;
- }),
-};
export default storyBook('LineChartWidget', story => {
story('Getting Started', () => {
return (
- is a Dashboard Widget Component. It displays
- a timeseries chart with one or more timeseries. Used to visualize data that
- changes over time in Project Details, Dashboards, Performance, and other UIs.
-
-
- );
- });
-
- story('Visualization', () => {
- const {selection} = usePageFilters();
- const {datetime} = selection;
- const {start, end} = datetime;
-
- const [timeseriesSelection, setTimeseriesSelection] = useState({
- 'p50(span.duration)': true,
- 'p99(span.duration)': true,
- });
-
- const toggleTimeseriesSelection = (seriesName: string): void => {
- setTimeseriesSelection(s => ({...s, [seriesName]: !s[seriesName]}));
- };
-
- const throughputTimeSeries = toTimeSeriesSelection(
- sparseThroughputTimeSeries,
- start,
- end
- );
-
- const durationTimeSeries1 = toTimeSeriesSelection(
- sampleDurationTimeSeries,
- start,
- end
- );
-
- const durationTimeSeries2 = toTimeSeriesSelection(
- sampleDurationTimeSeries2,
- start,
- end
- );
-
- return (
-
-
- The visualization of a line chart. It has
- some bells and whistles including automatic axes labels, and a hover tooltip.
- Like other widgets, it automatically fills the parent element. null
{' '}
- values are supported!
-
-
-
-
-
-
- The dataCompletenessDelay
prop indicates that this data is live,
- and the last few buckets might not have complete data. The delay is a number in
- seconds. Any data bucket that happens in that delay window will be plotted with
- a dotted line. By default the delay is 0
.
-
-
-
- To control the timeseries selection, you can use the{' '}
- timeseriesSelection
and onTimeseriesSelectionChange
{' '}
- props.
-
-
-
-
- {
- setTimeseriesSelection(newSelection);
- }}
- />
-
-
- {
- toggleTimeseriesSelection('p50(span.duration)');
- }}
- >
- Toggle 50th Percentile
-
-
- {
- toggleTimeseriesSelection('p99(span.duration)');
- }}
- >
- Toggle 99th Percentile
-
-
-
-
- will automatically check the types and unit
- of all the incoming timeseries. If they do not all match, it will fall back to a
- plain number scale. If the types match but the units do not, it will fall back
- to a sensible unit
-
-
-
- ({
- ...datum,
- value: datum.value === null ? null : datum.value / 1000,
- })),
- meta: {
- fields: durationTimeSeries2.meta?.fields!,
- units: {
- 'p50(span.duration)': 'second',
- },
- },
- },
- ]}
- />
-
-
- );
- });
-
- story('State', () => {
- return (
-
-
- supports the usual loading and error states.
- The loading state shows a spinner. The error state shows a message, and an
- optional "Retry" button.
-
-
-
-
-
-
-
-
-
-
-
-
-
- {}}
- />
-
-
-
- );
- });
-
- story('Colors', () => {
- const theme = useTheme();
-
- return (
-
-
- You can control the color of each timeseries by setting the color
{' '}
- attribute to a string that contains a valid hex color code.
+ 🚨 is deprecated! Instead, see the stories
+ for , which explain in detail how to compose your own
+ widgets from standard components. If you want information on how to render a
+ time series visualization, see the stories for
+ .
-
-
-
-
-
- );
- });
-
- story('Releases', () => {
- const releases = [
- {
- version: 'ui@0.1.2',
- timestamp: sampleThroughputTimeSeries.data.at(2)?.timestamp,
- },
- {
- version: 'ui@0.1.3',
- timestamp: sampleThroughputTimeSeries.data.at(20)?.timestamp,
- },
- ].filter(hasTimestamp);
-
- return (
-
-
- supports the releases
prop. If
- passed in, the widget will plot every release as a vertical line that overlays
- the chart data. Clicking on a release line will open the release details page.
-
-
-
-
-
);
});
});
-
-const MediumWidget = styled('div')`
- width: 420px;
- height: 250px;
-`;
-
-const SmallWidget = styled('div')`
- width: 360px;
- height: 160px;
-`;
-
-const SmallSizingWindow = styled(SizingWindow)`
- width: 50%;
- height: 300px;
-`;
-
-function toTimeSeriesSelection(
- timeSeries: TimeSeries,
- start: DateString | null,
- end: DateString | null
-): TimeSeries {
- return {
- ...timeSeries,
- data: timeSeries.data.filter(datum => {
- if (start && moment(datum.timestamp).isBefore(moment.utc(start))) {
- return false;
- }
-
- if (end && moment(datum.timestamp).isAfter(moment.utc(end))) {
- return false;
- }
-
- return true;
- }),
- };
-}
-
-function hasTimestamp(release: Partial): release is Release {
- return Boolean(release?.timestamp);
-}
diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/fixtures/sampleDurationTimeSeries.ts b/static/app/views/dashboards/widgets/timeSeriesWidget/fixtures/sampleDurationTimeSeries.ts
new file mode 100644
index 00000000000000..1643235353c29d
--- /dev/null
+++ b/static/app/views/dashboards/widgets/timeSeriesWidget/fixtures/sampleDurationTimeSeries.ts
@@ -0,0 +1,209 @@
+export const sampleDurationTimeSeries = {
+ field: 'p99(span.duration)',
+ meta: {
+ fields: {
+ 'p99(span.duration)': 'duration',
+ },
+ units: {
+ 'p99(span.duration)': 'millisecond',
+ },
+ },
+ data: [
+ {
+ value: 163.26759544018776,
+ timestamp: '2024-10-24T15:00:00-04:00',
+ },
+ {
+ value: 164.07690380778297,
+ timestamp: '2024-10-24T15:30:00-04:00',
+ },
+ {
+ value: 163.99383844776062,
+ timestamp: '2024-10-24T16:00:00-04:00',
+ },
+ {
+ value: 173.63336816976206,
+ timestamp: '2024-10-24T16:30:00-04:00',
+ },
+ {
+ value: 175.3677834806518,
+ timestamp: '2024-10-24T17:00:00-04:00',
+ },
+ {
+ value: 170.97728899915776,
+ timestamp: '2024-10-24T17:30:00-04:00',
+ },
+ {
+ value: 157.01612877746825,
+ timestamp: '2024-10-24T18:00:00-04:00',
+ },
+ {
+ value: 170.16561857179315,
+ timestamp: '2024-10-24T18:30:00-04:00',
+ },
+ {
+ value: 160.16955120038932,
+ timestamp: '2024-10-24T19:00:00-04:00',
+ },
+ {
+ value: 169.36653636105817,
+ timestamp: '2024-10-24T19:30:00-04:00',
+ },
+ {
+ value: 158.14689668805912,
+ timestamp: '2024-10-24T20:00:00-04:00',
+ },
+ {
+ value: 169.15717966126346,
+ timestamp: '2024-10-24T20:30:00-04:00',
+ },
+ {
+ value: 159.93437627752522,
+ timestamp: '2024-10-24T21:00:00-04:00',
+ },
+ {
+ value: 181.2808339108854,
+ timestamp: '2024-10-24T21:30:00-04:00',
+ },
+ {
+ value: 174.5653311041271,
+ timestamp: '2024-10-24T22:00:00-04:00',
+ },
+ {
+ value: 183.84079875522747,
+ timestamp: '2024-10-24T22:30:00-04:00',
+ },
+ {
+ value: 167.7772694683986,
+ timestamp: '2024-10-24T23:00:00-04:00',
+ },
+ {
+ value: 183.82398422937058,
+ timestamp: '2024-10-24T23:30:00-04:00',
+ },
+ {
+ value: 184.52864336855654,
+ timestamp: '2024-10-25T00:00:00-04:00',
+ },
+ {
+ value: 193.65015582927347,
+ timestamp: '2024-10-25T00:30:00-04:00',
+ },
+ {
+ value: 178.46847533061455,
+ timestamp: '2024-10-25T01:00:00-04:00',
+ },
+ {
+ value: 200.36778566817904,
+ timestamp: '2024-10-25T01:30:00-04:00',
+ },
+ {
+ value: 190.90359955365727,
+ timestamp: '2024-10-25T02:00:00-04:00',
+ },
+ {
+ value: 198.59051116263905,
+ timestamp: '2024-10-25T02:30:00-04:00',
+ },
+ {
+ value: 188.65220261321352,
+ timestamp: '2024-10-25T03:00:00-04:00',
+ },
+ {
+ value: 194.43420903582333,
+ timestamp: '2024-10-25T03:30:00-04:00',
+ },
+ {
+ value: 197.647787580193,
+ timestamp: '2024-10-25T04:00:00-04:00',
+ },
+ {
+ value: 190.58833865087888,
+ timestamp: '2024-10-25T04:30:00-04:00',
+ },
+ {
+ value: 178.91675989347792,
+ timestamp: '2024-10-25T05:00:00-04:00',
+ },
+ {
+ value: 190.0340975630367,
+ timestamp: '2024-10-25T05:30:00-04:00',
+ },
+ {
+ value: 187.38906820511914,
+ timestamp: '2024-10-25T06:00:00-04:00',
+ },
+ {
+ value: 189.56698436333198,
+ timestamp: '2024-10-25T06:30:00-04:00',
+ },
+ {
+ value: 190.05121671101094,
+ timestamp: '2024-10-25T07:00:00-04:00',
+ },
+ {
+ value: 190.17890269231188,
+ timestamp: '2024-10-25T07:30:00-04:00',
+ },
+ {
+ value: 185.79800462057017,
+ timestamp: '2024-10-25T08:00:00-04:00',
+ },
+ {
+ value: 189.58731836412431,
+ timestamp: '2024-10-25T08:30:00-04:00',
+ },
+ {
+ value: 194.0240027791402,
+ timestamp: '2024-10-25T09:00:00-04:00',
+ },
+ {
+ value: 194.36826153553162,
+ timestamp: '2024-10-25T09:30:00-04:00',
+ },
+ {
+ value: 188.280130395783,
+ timestamp: '2024-10-25T10:00:00-04:00',
+ },
+ {
+ value: 189.78493654183256,
+ timestamp: '2024-10-25T10:30:00-04:00',
+ },
+ {
+ value: 194.27624664281058,
+ timestamp: '2024-10-25T11:00:00-04:00',
+ },
+ {
+ value: 179.5612344192774,
+ timestamp: '2024-10-25T11:30:00-04:00',
+ },
+ {
+ value: 172.61016559821348,
+ timestamp: '2024-10-25T12:00:00-04:00',
+ },
+ {
+ value: 186.46779136621416,
+ timestamp: '2024-10-25T12:30:00-04:00',
+ },
+ {
+ value: 171.43180664489284,
+ timestamp: '2024-10-25T13:00:00-04:00',
+ },
+ {
+ value: 174.50192368733187,
+ timestamp: '2024-10-25T13:30:00-04:00',
+ },
+ {
+ value: 164.72702271761602,
+ timestamp: '2024-10-25T14:00:00-04:00',
+ },
+ {
+ value: 172.11837107180162,
+ timestamp: '2024-10-25T14:30:00-04:00',
+ },
+ {
+ value: 185.64485387569914,
+ timestamp: '2024-10-25T15:00:00-04:00',
+ },
+ ],
+};
diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/fixtures/sampleThroughputTimeSeries.ts b/static/app/views/dashboards/widgets/timeSeriesWidget/fixtures/sampleThroughputTimeSeries.ts
new file mode 100644
index 00000000000000..e4e740f9813597
--- /dev/null
+++ b/static/app/views/dashboards/widgets/timeSeriesWidget/fixtures/sampleThroughputTimeSeries.ts
@@ -0,0 +1,209 @@
+export const sampleThroughputTimeSeries = {
+ field: 'eps()',
+ meta: {
+ fields: {
+ 'eps()': 'rate',
+ },
+ units: {
+ 'eps()': '1/second',
+ },
+ },
+ data: [
+ {
+ value: 7456.966666666666,
+ timestamp: '2024-10-24T15:00:00-04:00',
+ },
+ {
+ value: 8217.8,
+ timestamp: '2024-10-24T15:30:00-04:00',
+ },
+ {
+ value: 8989.4,
+ timestamp: '2024-10-24T16:00:00-04:00',
+ },
+ {
+ value: 7930.9,
+ timestamp: '2024-10-24T16:30:00-04:00',
+ },
+ {
+ value: 7178.1,
+ timestamp: '2024-10-24T17:00:00-04:00',
+ },
+ {
+ value: 6450.666666666667,
+ timestamp: '2024-10-24T17:30:00-04:00',
+ },
+ {
+ value: 6497.8,
+ timestamp: '2024-10-24T18:00:00-04:00',
+ },
+ {
+ value: 4844.933333333333,
+ timestamp: '2024-10-24T18:30:00-04:00',
+ },
+ {
+ value: 4983.466666666666,
+ timestamp: '2024-10-24T19:00:00-04:00',
+ },
+ {
+ value: 4231.633333333333,
+ timestamp: '2024-10-24T19:30:00-04:00',
+ },
+ {
+ value: 4785.733333333334,
+ timestamp: '2024-10-24T20:00:00-04:00',
+ },
+ {
+ value: 4394.566666666667,
+ timestamp: '2024-10-24T20:30:00-04:00',
+ },
+ {
+ value: 4934.566666666667,
+ timestamp: '2024-10-24T21:00:00-04:00',
+ },
+ {
+ value: 4785.966666666666,
+ timestamp: '2024-10-24T21:30:00-04:00',
+ },
+ {
+ value: 4850.166666666667,
+ timestamp: '2024-10-24T22:00:00-04:00',
+ },
+ {
+ value: 4630.966666666666,
+ timestamp: '2024-10-24T22:30:00-04:00',
+ },
+ {
+ value: 4737.133333333333,
+ timestamp: '2024-10-24T23:00:00-04:00',
+ },
+ {
+ value: 4093.6666666666665,
+ timestamp: '2024-10-24T23:30:00-04:00',
+ },
+ {
+ value: 4393.9,
+ timestamp: '2024-10-25T00:00:00-04:00',
+ },
+ {
+ value: 4763.366666666667,
+ timestamp: '2024-10-25T00:30:00-04:00',
+ },
+ {
+ value: 5913.533333333334,
+ timestamp: '2024-10-25T01:00:00-04:00',
+ },
+ {
+ value: 6001.933333333333,
+ timestamp: '2024-10-25T01:30:00-04:00',
+ },
+ {
+ value: 7880.266666666666,
+ timestamp: '2024-10-25T02:00:00-04:00',
+ },
+ {
+ value: 8901.1,
+ timestamp: '2024-10-25T02:30:00-04:00',
+ },
+ {
+ value: 10684.533333333333,
+ timestamp: '2024-10-25T03:00:00-04:00',
+ },
+ {
+ value: 11362.233333333334,
+ timestamp: '2024-10-25T03:30:00-04:00',
+ },
+ {
+ value: 12708.933333333332,
+ timestamp: '2024-10-25T04:00:00-04:00',
+ },
+ {
+ value: 12494.9,
+ timestamp: '2024-10-25T04:30:00-04:00',
+ },
+ {
+ value: 13224.8,
+ timestamp: '2024-10-25T05:00:00-04:00',
+ },
+ {
+ value: 11910,
+ timestamp: '2024-10-25T05:30:00-04:00',
+ },
+ {
+ value: 11013.433333333332,
+ timestamp: '2024-10-25T06:00:00-04:00',
+ },
+ {
+ value: 9856,
+ timestamp: '2024-10-25T06:30:00-04:00',
+ },
+ {
+ value: 10530.033333333333,
+ timestamp: '2024-10-25T07:00:00-04:00',
+ },
+ {
+ value: 10233.2,
+ timestamp: '2024-10-25T07:30:00-04:00',
+ },
+ {
+ value: 11688.066666666668,
+ timestamp: '2024-10-25T08:00:00-04:00',
+ },
+ {
+ value: 11654.533333333333,
+ timestamp: '2024-10-25T08:30:00-04:00',
+ },
+ {
+ value: 11965.366666666667,
+ timestamp: '2024-10-25T09:00:00-04:00',
+ },
+ {
+ value: 12124.9,
+ timestamp: '2024-10-25T09:30:00-04:00',
+ },
+ {
+ value: 12114.466666666667,
+ timestamp: '2024-10-25T10:00:00-04:00',
+ },
+ {
+ value: 11737.7,
+ timestamp: '2024-10-25T10:30:00-04:00',
+ },
+ {
+ value: 11289.9,
+ timestamp: '2024-10-25T11:00:00-04:00',
+ },
+ {
+ value: 10604.833333333334,
+ timestamp: '2024-10-25T11:30:00-04:00',
+ },
+ {
+ value: 9463.233333333334,
+ timestamp: '2024-10-25T12:00:00-04:00',
+ },
+ {
+ value: 8513.233333333334,
+ timestamp: '2024-10-25T12:30:00-04:00',
+ },
+ {
+ value: 8485.233333333334,
+ timestamp: '2024-10-25T13:00:00-04:00',
+ },
+ {
+ value: 8105.4,
+ timestamp: '2024-10-25T13:30:00-04:00',
+ },
+ {
+ value: 7914.4,
+ timestamp: '2024-10-25T14:00:00-04:00',
+ },
+ {
+ value: 7006.8,
+ timestamp: '2024-10-25T14:30:00-04:00',
+ },
+ {
+ value: 399.3,
+ timestamp: '2024-10-25T15:00:00-04:00',
+ },
+ ],
+};
diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.stories.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.stories.tsx
new file mode 100644
index 00000000000000..10479c7210cafb
--- /dev/null
+++ b/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.stories.tsx
@@ -0,0 +1,457 @@
+import {Fragment, useState} from 'react';
+import {useTheme} from '@emotion/react';
+import styled from '@emotion/styled';
+import moment from 'moment-timezone';
+
+import {CodeSnippet} from 'sentry/components/codeSnippet';
+import JSXNode from 'sentry/components/stories/jsxNode';
+import SideBySide from 'sentry/components/stories/sideBySide';
+import SizingWindow from 'sentry/components/stories/sizingWindow';
+import storyBook from 'sentry/stories/storyBook';
+import type {DateString} from 'sentry/types/core';
+import {decodeScalar} from 'sentry/utils/queryString';
+import useLocationQuery from 'sentry/utils/url/useLocationQuery';
+
+import type {Release, TimeSeries, TimeseriesSelection} from '../common/types';
+import {shiftTimeserieToNow} from '../timeSeriesWidget/shiftTimeserieToNow';
+
+import {sampleDurationTimeSeries} from './fixtures/sampleDurationTimeSeries';
+import {sampleThroughputTimeSeries} from './fixtures/sampleThroughputTimeSeries';
+import {TimeSeriesWidgetVisualization} from './timeSeriesWidgetVisualization';
+
+// eslint-disable-next-line import/no-webpack-loader-syntax
+import types from '!!type-loader!sentry/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization';
+
+const sampleDurationTimeSeries2 = {
+ ...sampleDurationTimeSeries,
+ field: 'p50(span.duration)',
+ data: sampleDurationTimeSeries.data.map(datum => {
+ return {
+ ...datum,
+ value: datum.value * 0.3 + 30 * Math.random(),
+ };
+ }),
+ meta: {
+ fields: {
+ 'p50(span.duration)': 'duration',
+ },
+ units: {
+ 'p50(span.duration)': 'millisecond',
+ },
+ },
+};
+
+export default storyBook('TimeSeriesWidgetVisualization', (story, APIReference) => {
+ APIReference(types.TimeSeriesWidgetVisualization);
+
+ story('Getting Started', () => {
+ return (
+
+
+ is a feature-full time series
+ chart, designed to plot data returned from /events-stats/
endpoints
+ in Explore, Dashboards, and other similar UIs.
+
+
+ It includes features like:
+
+ scaling mis-matched units
+ displaying incomplete ingestion buckets
+
+ stripping legend names of internal information like equation|
{' '}
+ prefixes
+
+ intelligently formatting the axes
+ and more!
+
+ If you (or someone you know) is plotting Sentry data and the X axis is time, you
+ should be using this component! It's highly configurable, and should suit your
+ needs. If it doesn't, reach out to the Dashboards team.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ });
+
+ story('Data Format', () => {
+ return (
+
+
+ accepts the{' '}
+ timeSeries
prop, which contains the plottable data. This is a
+ special format that we're slowly aligning with the server responses. For now,
+ you will probably have to transform your data into this format. Here's an
+ example:
+
+
+ {`
+{
+ "field": "p99(span.duration)",
+ "meta": {
+ "fields": {
+ "p99(span.duration)": "duration",
+ },
+ "units": {
+ "p99(span.duration)": "millisecond",
+ },
+ },
+ "data": [
+ {
+ "value": 163.26759544018776,
+ "timestamp": "2024-10-24T15:00:00-04:00",
+ },
+ {
+ "value": 164.07690380778297,
+ "timestamp": "2024-10-24T15:30:00-04:00",
+ },
+ ]
+}
+ `}
+
+
+ );
+ });
+
+ story('Choosing The Visualization Type', () => {
+ return (
+
+ Here are a few guidelines on how to choose the right visualization:
+
+
+ Only use area charts if you want to plot multiple series and those
+ series represent components of a total. For example, area charts are a good
+ choice to show time spent, broken down by span operation. They are not a good
+ choice for plotting a single duration time series
+
+
+ Bar charts are to your discretion, it's most an aesthetic choice. Generally,
+ bars communicate discrete buckets, and lines communicate continuous data. If
+ you are plotting something like duration, even if it's broken down by time
+ buckets, a line feels right. If you are plotting someting like throughput (a
+ naturally bucketed value) and the buckets are big, a bar chart might be better
+
+ Use line charts when in doubt! They are almost always the right choice
+
+
+ );
+ });
+
+ story('Basic Plotting', () => {
+ return (
+
+
+ can plot multiple time series
+ while accounting for their type and units. It adds X axis formatting, Y axis
+ formatting, a tooltip with correct units, it will scale units of the same type
+ if needed, and it supports null
values. It also supports more
+ advanced features like drag-to-zoom, and marking incomplete data. The component
+ doesn't have its own size, it always respects the side of its parent.
+
+
+
+
+
+
+
+
+ );
+ });
+
+ story('Loading Placeholder', () => {
+ return (
+
+
+ includes a loading placeholder.
+ You can use it via{' '}
+
+
+
+
+
+
+
+ );
+ });
+
+ story('Stacking', () => {
+ return (
+
+
+ Bar charts are unstacked by default. To turn on stacking, use the{' '}
+ stacked
prop. Area charts are always stacked. Line charts are never
+ stacked.
+
+
+ );
+ });
+
+ story('Incomplete Data', () => {
+ const props = {
+ dataCompletenessDelay: 60 * 60 * 3,
+ timeSeries: [
+ shiftTimeserieToNow(sampleDurationTimeSeries),
+ shiftTimeserieToNow(sampleDurationTimeSeries2),
+ ],
+ };
+ return (
+
+
+ The dataCompletenessDelay
prop indicates that this data is live,
+ and the last few buckets might not have complete data. The delay is a number in
+ seconds. By default the delay is 0
.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ });
+
+ story('Drag to Select', () => {
+ const {start, end} = useLocationQuery({
+ fields: {
+ start: decodeScalar,
+ end: decodeScalar,
+ },
+ });
+
+ const [timeseriesSelection, setTimeseriesSelection] = useState({
+ 'p50(span.duration)': true,
+ 'p99(span.duration)': true,
+ });
+
+ const durationTimeSeries1 = toTimeSeriesSelection(
+ sampleDurationTimeSeries,
+ start,
+ end
+ );
+
+ const durationTimeSeries2 = toTimeSeriesSelection(
+ sampleDurationTimeSeries2,
+ start,
+ end
+ );
+
+ return (
+
+
+ supports drag-to-select.
+ Dragging the mouse over the visualization area and releasing the cursor will
+ update the page URL with the new datetime selection. You can press{' '}
+ escape
during selection to cancel selection. Give it a try!
+
+
+
+ {
+ setTimeseriesSelection(newSelection);
+ }}
+ />
+
+
+ );
+ });
+
+ story('Legends', () => {
+ const [timeSeriesSelection, setTimeSeriesSelection] = useState({
+ 'p99(span.duration)': false,
+ });
+
+ return (
+
+
+ supports series legends, and a
+ few features on top of them. By default, if only one series is plotted, the
+ legend does not appear. If there are multiple series, a legend is shown above
+ the charts.
+
+
+ You can control legend selection with the timeseriesSelection
prop.
+ By default, all time series are shown. If any time series is set to{' '}
+ false
it will be hidden. The companion{' '}
+ onTimeseriesSelectionChange
prop is a callback, it will tell you
+ when the user changes the legend selection by clicking on legend labels.
+
+
+ You can also provide aliases for legends to give them a friendlier name. In this
+ example, verbose names like "p99(span.duration)" are truncated, and the p99
+ series is hidden by default.
+
+
+
+
+
+
+ );
+ });
+
+ story('Colors', () => {
+ const theme = useTheme();
+
+ return (
+
+ {' '}
+
+ You can control the color of each time series by setting the color
{' '}
+ attribute to a string that contains a valid hex color code.
+
+
+
+
+
+ );
+ });
+
+ story('Releases', () => {
+ const releases = [
+ {
+ version: 'ui@0.1.2',
+ timestamp: sampleThroughputTimeSeries.data.at(2)?.timestamp,
+ },
+ {
+ version: 'ui@0.1.3',
+ timestamp: sampleThroughputTimeSeries.data.at(20)?.timestamp,
+ },
+ ].filter(hasTimestamp);
+
+ return (
+
+
+ Area and line charts support showing release markers via the{' '}
+ releases
prop. Clicking on a release line will open the release
+ details page.
+
+
+
+
+
+
+ );
+ });
+});
+
+const FillParent = styled('div')`
+ width: 100%;
+ height: 100%;
+`;
+
+const MediumWidget = styled('div')`
+ position: relative;
+ width: 420px;
+ height: 250px;
+`;
+
+const SmallWidget = styled('div')`
+ position: relative;
+ width: 360px;
+ height: 160px;
+`;
+
+const SmallSizingWindow = styled(SizingWindow)`
+ width: 50%;
+ height: 300px;
+`;
+
+function toTimeSeriesSelection(
+ timeSeries: TimeSeries,
+ start: DateString | null,
+ end: DateString | null
+): TimeSeries {
+ return {
+ ...timeSeries,
+ data: timeSeries.data.filter(datum => {
+ if (start && moment(datum.timestamp).isBefore(moment.utc(start))) {
+ return false;
+ }
+
+ if (end && moment(datum.timestamp).isAfter(moment.utc(end))) {
+ return false;
+ }
+
+ return true;
+ }),
+ };
+}
+
+function hasTimestamp(release: Partial): release is Release {
+ return Boolean(release?.timestamp);
+}
diff --git a/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx b/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx
index e1c3818df016c9..823b92a5270031 100644
--- a/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx
+++ b/static/app/views/dashboards/widgets/timeSeriesWidget/timeSeriesWidgetVisualization.tsx
@@ -50,13 +50,37 @@ import {splitSeriesIntoCompleteAndIncomplete} from './splitSeriesIntoCompleteAnd
type VisualizationType = 'area' | 'line' | 'bar';
export interface TimeSeriesWidgetVisualizationProps {
+ /**
+ * An array of time series, each one representing a changing value over time. This is the chart's data. See documentation for examples
+ */
timeSeries: TimeSeries[];
+ /**
+ * Chart type
+ */
visualizationType: VisualizationType;
+ /**
+ * A mapping of time series fields to their user-friendly labels, if needed
+ */
aliases?: Aliases;
+ /**
+ * A duration in seconds. Any items in the time series that fall within that duration of the current time will be visually marked as "incomplete"
+ */
dataCompletenessDelay?: number;
+ /**
+ * Callback that returns an updated `timeseriesSelection` after a user manipulations the selection via the legend
+ */
onTimeseriesSelectionChange?: (selection: TimeseriesSelection) => void;
+ /**
+ * Array of `Release` objects. If provided, they are plotted on line and area visualizations as vertical lines
+ */
releases?: Release[];
+ /**
+ * Only available for `visualizationType="bar"`. If `true`, the bars are stacked
+ */
stacked?: boolean;
+ /**
+ * A mapping of time series field name to boolean. If the value is `false`, the series is hidden from view
+ */
timeseriesSelection?: TimeseriesSelection;
}