Skip to content

Commit

Permalink
feat(dashboards): More robust documentation for `TimeSeriesWidgetVisu…
Browse files Browse the repository at this point in the history
…alization` (#85286)

Instead of sparse and duplicated documentation for `LineChartWidget`,
`AreaChartWidget`, and `BarChartWidget`, I'm directing that
documentation to a new page, for `TimeSeriesWidgetVisualization`.

First of all, we are going to be discouraging people from using
`LineChartWidget`, but rather asking them to compose widgets from the
`Widget` component. Second of all, the `TimeSeriesWidgetVisualization`
page needed to list really all the options and their significance.

Once _this_ is merged, I'll be in touch with the affected teams about
their code, if needed, but realistically I'll be the one who replaces
uses of `XWidget` components with composed versions.
  • Loading branch information
gggritso authored Feb 19, 2025
1 parent 4fb28df commit e668dda
Show file tree
Hide file tree
Showing 9 changed files with 1,294 additions and 1,007 deletions.
Original file line number Diff line number Diff line change
@@ -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 (
<Fragment>
<p>
<JSXNode name="AreaChartWidget" /> 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 <JSXNode name="LineChartWidget" />, though it doesn't
support features like "Previous Period Data".
</p>
<p>
<em>NOTE:</em> This chart is not appropriate for showing a single timeseries!
You should use <JSXNode name="LineChartWidget" /> instead.
🚨 <JSXNode name="AreaChartWidget" /> is deprecated! Instead, see the stories
for <JSXNode name="Widget" />, 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
<JSXNode name="TimeSeriesWidgetVisualization" />.
</p>
</Fragment>
);
});

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 (
<Fragment>
<p>
The visualization of <JSXNode name="AreaChartWidget" /> 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.
</p>
<SmallSizingWindow>
<AreaChartWidget
title="Duration Breakdown"
description="Explains what proportion of total duration is taken up by latency vs. span duration"
timeSeries={[latencyTimeSeries, spanDurationTimeSeries]}
/>
</SmallSizingWindow>

<p>
The <code>dataCompletenessDelay</code> 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 <code>0</code>.
</p>

<SideBySide>
<MediumWidget>
<AreaChartWidget
title="span.duration"
dataCompletenessDelay={60 * 60 * 3}
timeSeries={[
shiftTimeserieToNow(latencyTimeSeries),
shiftTimeserieToNow(spanDurationTimeSeries),
]}
/>
</MediumWidget>
</SideBySide>
</Fragment>
);
});

story('State', () => {
return (
<Fragment>
<p>
<JSXNode name="AreaChartWidget" /> supports the usual loading and error states.
The loading state shows a spinner. The error state shows a message, and an
optional "Retry" button.
</p>

<SideBySide>
<SmallWidget>
<AreaChartWidget title="Loading Count" isLoading />
</SmallWidget>
<SmallWidget>
<AreaChartWidget title="Missing Count" />
</SmallWidget>
<SmallWidget>
<AreaChartWidget
title="Count Error"
error={new Error('Something went wrong!')}
/>
</SmallWidget>
<SmallWidget>
<AreaChartWidget
title="Data Error"
error={new Error('Something went wrong!')}
onRetry={() => {}}
/>
</SmallWidget>
</SideBySide>
</Fragment>
);
});

story('Colors', () => {
const theme = useTheme();

return (
<Fragment>
<p>
You can control the color of each timeseries by setting the <code>color</code>{' '}
attribute to a string that contains a valid hex color code.
</p>

<MediumWidget>
<AreaChartWidget
title="error_rate()"
description="Rate of Errors"
timeSeries={[
{...sampleLatencyTimeSeries, color: theme.error},

{...sampleSpanDurationTimeSeries, color: theme.warning},
]}
/>
</MediumWidget>
</Fragment>
);
});

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 (
<Fragment>
<p>
<JSXNode name="AreaChartWidget" /> supports the <code>releases</code> 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.
</p>

<MediumWidget>
<AreaChartWidget
title="error_rate()"
timeSeries={[sampleLatencyTimeSeries, sampleSpanDurationTimeSeries]}
releases={releases}
/>
</MediumWidget>
</Fragment>
);
});
});

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>): release is Release {
return Boolean(release?.timestamp);
}
Original file line number Diff line number Diff line change
@@ -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 (
<Fragment>
<p>
<JSXNode name="BarChartWidget" /> 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 <JSXNode name="AreaChartWidget" />
</p>
<p>
<em>NOTE:</em> Prefer <JSXNode name="LineChartWidget" /> and{' '}
<JSXNode name="AreaChartWidget" /> for timeseries visualizations! This should be
used for discontinuous categorized data.
</p>
</Fragment>
);
});

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 (
<Fragment>
<p>
The visualization of <JSXNode name="BarChartWidget" /> 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.
🚨 <JSXNode name="BarChartWidget" /> is deprecated! Instead, see the stories for{' '}
<JSXNode name="Widget" />, 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
<JSXNode name="TimeSeriesWidgetVisualization" />.
</p>
<p>
The <code>stacked</code> boolean prop controls stacking. Bar charts are not
stacked by default.
</p>
<SideBySide>
<SmallSizingWindow>
<BarChartWidget
title="Duration Breakdown"
description="Explains what proportion of total duration is taken up by latency vs. span duration"
timeSeries={[
shiftTimeserieToNow(latencyTimeSeries),
shiftTimeserieToNow(spanDurationTimeSeries),
]}
dataCompletenessDelay={600}
/>
</SmallSizingWindow>
<SmallSizingWindow>
<BarChartWidget
title="Duration Breakdown"
description="Explains what proportion of total duration is taken up by latency vs. span duration"
timeSeries={[
shiftTimeserieToNow(latencyTimeSeries),
shiftTimeserieToNow(spanDurationTimeSeries),
]}
stacked
/>
</SmallSizingWindow>
</SideBySide>
</Fragment>
);
});
});

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;
}),
};
}
Loading

0 comments on commit e668dda

Please sign in to comment.