From f9e4183b1c107d4f4b6fb14fa2503849c49eced3 Mon Sep 17 00:00:00 2001
From: George Gritsouk <989898+gggritso@users.noreply.github.com>
Date: Wed, 11 Dec 2024 11:03:31 -0500
Subject: [PATCH] Implement `AreaChartWidget`
---
sampleDurationTimeSeries.json | 112 +++++++++
.../areaChartWidget/areaChartWidget.spec.tsx | 87 +++++++
.../areaChartWidget.stories.tsx | 234 ++++++++++++++++++
.../areaChartWidget/areaChartWidget.tsx | 78 ++++++
.../areaChartWidgetVisualization.tsx | 215 ++++++++++++++++
.../sampleLatencyTimeSeries.json | 205 +++++++++++++++
.../sampleSpanDurationTimeSeries.json | 205 +++++++++++++++
7 files changed, 1136 insertions(+)
create mode 100644 sampleDurationTimeSeries.json
create mode 100644 static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.spec.tsx
create mode 100644 static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx
create mode 100644 static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.tsx
create mode 100644 static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx
create mode 100644 static/app/views/dashboards/widgets/areaChartWidget/sampleLatencyTimeSeries.json
create mode 100644 static/app/views/dashboards/widgets/areaChartWidget/sampleSpanDurationTimeSeries.json
diff --git a/sampleDurationTimeSeries.json b/sampleDurationTimeSeries.json
new file mode 100644
index 00000000000000..0714c525203711
--- /dev/null
+++ b/sampleDurationTimeSeries.json
@@ -0,0 +1,112 @@
+[
+ {
+ "field": "avg(span.duration)",
+ "data": [
+ [1733781600, [{"count": 194.32090152293875}]],
+ [1733783400, [{"count": 147.62552997973896}]],
+ [1733785200, [{"count": 167.3125896486525}]],
+ [1733787000, [{"count": 154.9546418433016}]],
+ [1733788800, [{"count": 177.75841778182115}]],
+ [1733790600, [{"count": 149.62071539059102}]],
+ [1733792400, [{"count": 148.37426947953236}]],
+ [1733794200, [{"count": 151.02028675214447}]],
+ [1733796000, [{"count": 143.50677477334426}]],
+ [1733797800, [{"count": 142.88535217056204}]],
+ [1733799600, [{"count": 146.46517983479544}]],
+ [1733801400, [{"count": 147.25420642999308}]],
+ [1733803200, [{"count": 146.49359730424217}]],
+ [1733805000, [{"count": 142.00870233797465}]],
+ [1733806800, [{"count": 157.48923328095663}]],
+ [1733808600, [{"count": 144.61466487144975}]],
+ [1733810400, [{"count": 156.85201565358486}]],
+ [1733812200, [{"count": 148.59436417696972}]],
+ [1733814000, [{"count": 155.72779310291295}]],
+ [1733815800, [{"count": 148.23792988918535}]],
+ [1733817600, [{"count": 155.66213671702405}]],
+ [1733819400, [{"count": 161.74766100502546}]],
+ [1733821200, [{"count": 171.07824116698762}]],
+ [1733823000, [{"count": 164.33299771174853}]],
+ [1733824800, [{"count": 192.93421512847078}]],
+ [1733826600, [{"count": 180.4593461868112}]],
+ [1733828400, [{"count": 172.99345240577236}]],
+ [1733830200, [{"count": 163.5327323114427}]],
+ [1733832000, [{"count": 182.60959861686507}]],
+ [1733833800, [{"count": 173.0523860965734}]],
+ [1733835600, [{"count": 177.17056856145237}]],
+ [1733837400, [{"count": 169.14652081980321}]],
+ [1733839200, [{"count": 164.87272365787706}]],
+ [1733841000, [{"count": 180.67025513785254}]],
+ [1733842800, [{"count": 206.55308274312813}]],
+ [1733844600, [{"count": 254.2190236395339}]],
+ [1733846400, [{"count": 191.45799652669942}]],
+ [1733848200, [{"count": 168.08625846646902}]],
+ [1733850000, [{"count": 171.19012303216394}]],
+ [1733851800, [{"count": 165.4369895365753}]],
+ [1733853600, [{"count": 179.14458584719685}]],
+ [1733855400, [{"count": 162.45116280726685}]],
+ [1733857200, [{"count": 173.7653410018822}]],
+ [1733859000, [{"count": 158.8766740686512}]],
+ [1733860800, [{"count": 165.27304423302115}]],
+ [1733862600, [{"count": 155.74659761848613}]],
+ [1733864400, [{"count": 164.0378545765358}]],
+ [1733866200, [{"count": 160.07243369487554}]],
+ [1733868000, [{"count": 161.39470890181587}]],
+ [1733869800, [{"count": 0}]]
+ ]
+ },
+ {
+ "field": "avg(messaging.message.receive.latency)",
+ "data": [
+ [1733781600, [{"count": 15.797387473297285}]],
+ [1733783400, [{"count": 6.098785205112891}]],
+ [1733785200, [{"count": 10.023521061981072}]],
+ [1733787000, [{"count": 6.303244308149576}]],
+ [1733788800, [{"count": 11.352363343199878}]],
+ [1733790600, [{"count": 4.845476325747717}]],
+ [1733792400, [{"count": 11.251521855070287}]],
+ [1733794200, [{"count": 4.425860645618586}]],
+ [1733796000, [{"count": 9.989218687466126}]],
+ [1733797800, [{"count": 4.025772756170272}]],
+ [1733799600, [{"count": 9.17541579241641}]],
+ [1733801400, [{"count": 4.2380217489924865}]],
+ [1733803200, [{"count": 8.027566877217474}]],
+ [1733805000, [{"count": 5.295137636393576}]],
+ [1733806800, [{"count": 8.656494836655359}]],
+ [1733808600, [{"count": 5.475662654003712}]],
+ [1733810400, [{"count": 9.793632841698757}]],
+ [1733812200, [{"count": 5.591866790430932}]],
+ [1733814000, [{"count": 9.187478698931766}]],
+ [1733815800, [{"count": 3.779925204205954}]],
+ [1733817600, [{"count": 10.315176397597504}]],
+ [1733819400, [{"count": 5.101678176894567}]],
+ [1733821200, [{"count": 10.405819741300553}]],
+ [1733823000, [{"count": 5.157673142672812}]],
+ [1733824800, [{"count": 13.454678120116997}]],
+ [1733826600, [{"count": 5.633257152519796}]],
+ [1733828400, [{"count": 12.38331484233643}]],
+ [1733830200, [{"count": 6.7168414807525245}]],
+ [1733832000, [{"count": 10.82394699109634}]],
+ [1733833800, [{"count": 6.649313439563898}]],
+ [1733835600, [{"count": 10.957238674362912}]],
+ [1733837400, [{"count": 7.891612896606848}]],
+ [1733839200, [{"count": 9.83684972309017}]],
+ [1733841000, [{"count": 4.472633330313572}]],
+ [1733842800, [{"count": 13.886404361521333}]],
+ [1733844600, [{"count": 9.212753388080626}]],
+ [1733846400, [{"count": 12.213060522650725}]],
+ [1733848200, [{"count": 5.308362659314872}]],
+ [1733850000, [{"count": 12.167955277129803}]],
+ [1733851800, [{"count": 5.95843470061378}]],
+ [1733853600, [{"count": 11.623431372484815}]],
+ [1733855400, [{"count": 7.133246343117738}]],
+ [1733857200, [{"count": 9.91670999332601}]],
+ [1733859000, [{"count": 8.220551566043556}]],
+ [1733860800, [{"count": 10.644324473372116}]],
+ [1733862600, [{"count": 6.380262999155083}]],
+ [1733864400, [{"count": 11.611059631291011}]],
+ [1733866200, [{"count": 4.830427051517974}]],
+ [1733868000, [{"count": 14.018863061817425}]],
+ [1733869800, [{"count": 0}]]
+ ]
+ }
+]
diff --git a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.spec.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.spec.tsx
new file mode 100644
index 00000000000000..5975cb6041941b
--- /dev/null
+++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.spec.tsx
@@ -0,0 +1,87 @@
+import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+
+import {AreaChartWidget} from './areaChartWidget';
+import sampleDurationTimeSeries from './sampleDurationTimeSeries.json';
+
+describe('AreaChartWidget', () => {
+ describe('Layout', () => {
+ it('Renders', () => {
+ render(
+
+ );
+ });
+ });
+
+ describe('Visualization', () => {
+ it('Explains missing data', () => {
+ render();
+
+ expect(screen.getByText('No Data')).toBeInTheDocument();
+ });
+ });
+
+ describe('State', () => {
+ it('Shows a loading placeholder', () => {
+ render();
+
+ expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
+ });
+
+ it('Loading state takes precedence over error state', () => {
+ render(
+
+ );
+
+ expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
+ });
+
+ it('Shows an error message', () => {
+ render();
+
+ expect(screen.getByText('Error: Uh oh')).toBeInTheDocument();
+ });
+
+ it('Shows a retry button', async () => {
+ const onRetry = jest.fn();
+
+ render();
+
+ await userEvent.click(screen.getByRole('button', {name: 'Retry'}));
+ expect(onRetry).toHaveBeenCalledTimes(1);
+ });
+
+ it('Hides other actions if there is an error and a retry handler', () => {
+ const onRetry = jest.fn();
+
+ render(
+
+ );
+
+ expect(screen.getByRole('button', {name: 'Retry'})).toBeInTheDocument();
+ expect(
+ screen.queryByRole('link', {name: 'Open in Discover'})
+ ).not.toBeInTheDocument();
+ });
+ });
+});
diff --git a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx
new file mode 100644
index 00000000000000..eae96d30624839
--- /dev/null
+++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.stories.tsx
@@ -0,0 +1,234 @@
+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, TimeseriesData} from '../common/types';
+
+import {AreaChartWidget} from './areaChartWidget';
+import sampleLatencyTimeSeries from './sampleLatencyTimeSeries.json';
+import sampleSpanDurationTimeSeries from './sampleSpanDurationTimeSeries.json';
+
+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.
+
+
+ );
+ });
+
+ story('Visualization', () => {
+ const {selection} = usePageFilters();
+ const {datetime} = selection;
+ const {start, end} = datetime;
+
+ const latencyTimeSeries = toTimeSeriesSelection(
+ sampleLatencyTimeSeries as unknown as TimeseriesData,
+ start,
+ end
+ );
+
+ const spanDurationTimeSeries = toTimeSeriesSelection(
+ sampleSpanDurationTimeSeries as unknown as TimeseriesData,
+ 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.
+
+
+
+
+
+ );
+ });
+
+ 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: TimeseriesData,
+ start: DateString | null,
+ end: DateString | null
+): TimeseriesData {
+ 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/areaChartWidget/areaChartWidget.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.tsx
new file mode 100644
index 00000000000000..71d3b76c4f4826
--- /dev/null
+++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidget.tsx
@@ -0,0 +1,78 @@
+import styled from '@emotion/styled';
+
+import TransparentLoadingMask from 'sentry/components/charts/transparentLoadingMask';
+import LoadingIndicator from 'sentry/components/loadingIndicator';
+import {defined} from 'sentry/utils';
+import {
+ AreaChartWidgetVisualization,
+ type AreaChartWidgetVisualizationProps,
+} from 'sentry/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization';
+import {
+ WidgetFrame,
+ type WidgetFrameProps,
+} from 'sentry/views/dashboards/widgets/common/widgetFrame';
+
+import {MISSING_DATA_MESSAGE, X_GUTTER, Y_GUTTER} from '../common/settings';
+import type {StateProps} from '../common/types';
+
+export interface AreaChartWidgetProps
+ extends StateProps,
+ Omit,
+ Partial {}
+
+export function AreaChartWidget(props: AreaChartWidgetProps) {
+ const {timeseries} = props;
+
+ if (props.isLoading) {
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ let parsingError: string | undefined = undefined;
+
+ if (!defined(timeseries)) {
+ parsingError = MISSING_DATA_MESSAGE;
+ }
+
+ const error = props.error ?? parsingError;
+
+ return (
+
+ {defined(timeseries) && (
+
+
+
+ )}
+
+ );
+}
+
+const AreaChartWrapper = styled('div')`
+ flex-grow: 1;
+ padding: 0 ${X_GUTTER} ${Y_GUTTER} ${X_GUTTER};
+`;
+
+const LoadingPlaceholder = styled('div')`
+ position: absolute;
+ inset: 0;
+
+ display: flex;
+ justify-content: center;
+ align-items: center;
+`;
diff --git a/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx
new file mode 100644
index 00000000000000..1358e6de70291e
--- /dev/null
+++ b/static/app/views/dashboards/widgets/areaChartWidget/areaChartWidgetVisualization.tsx
@@ -0,0 +1,215 @@
+import {useRef} from 'react';
+import {useNavigate} from 'react-router-dom';
+import {useTheme} from '@emotion/react';
+import type {
+ TooltipFormatterCallback,
+ TopLevelFormatterParams,
+} from 'echarts/types/dist/shared';
+import type EChartsReactCore from 'echarts-for-react/lib/core';
+
+import BaseChart from 'sentry/components/charts/baseChart';
+import {getFormatter} from 'sentry/components/charts/components/tooltip';
+import AreaSeries from 'sentry/components/charts/series/areaSeries';
+import LineSeries from 'sentry/components/charts/series/lineSeries';
+import {useChartZoom} from 'sentry/components/charts/useChartZoom';
+import {isChartHovered} from 'sentry/components/charts/utils';
+import type {Series} from 'sentry/types/echarts';
+import {defined} from 'sentry/utils';
+import normalizeUrl from 'sentry/utils/url/normalizeUrl';
+import useOrganization from 'sentry/utils/useOrganization';
+import usePageFilters from 'sentry/utils/usePageFilters';
+
+import {useWidgetSyncContext} from '../../contexts/widgetSyncContext';
+import {formatChartValue} from '../common/formatChartValue';
+import {ReleaseSeries} from '../common/releaseSeries';
+import type {Meta, Release, TimeseriesData} from '../common/types';
+
+export interface AreaChartWidgetVisualizationProps {
+ timeseries: TimeseriesData[];
+ meta?: Meta;
+ releases?: Release[];
+}
+
+export function AreaChartWidgetVisualization(props: AreaChartWidgetVisualizationProps) {
+ const chartRef = useRef(null);
+ const {register: registerWithWidgetSyncContext} = useWidgetSyncContext();
+
+ const pageFilters = usePageFilters();
+ const {start, end, period, utc} = pageFilters.selection.datetime;
+ const {meta} = props;
+
+ const theme = useTheme();
+ const organization = useOrganization();
+ const navigate = useNavigate();
+
+ let releaseSeries: Series | undefined = undefined;
+ if (props.releases) {
+ const onClick = (release: Release) => {
+ navigate(
+ normalizeUrl({
+ pathname: `/organizations/${
+ organization.slug
+ }/releases/${encodeURIComponent(release.version)}/`,
+ })
+ );
+ };
+
+ releaseSeries = ReleaseSeries(theme, props.releases, onClick, utc ?? false);
+ }
+
+ const chartZoomProps = useChartZoom({
+ saveOnZoom: true,
+ });
+
+ // TODO: There's a TypeScript indexing error here. This _could_ in theory be
+ // `undefined`. We need to guard against this in the parent component, and
+ // show an error.
+ const firstSeries = props.timeseries[0];
+
+ // TODO: Raise error if attempting to plot series of different types or units
+ const firstSeriesField = firstSeries?.field;
+ const type = meta?.fields?.[firstSeriesField] ?? 'number';
+ const unit = meta?.units?.[firstSeriesField] ?? undefined;
+
+ const formatter: TooltipFormatterCallback = (
+ params,
+ asyncTicket
+ ) => {
+ // Only show the tooltip of the current chart. Otherwise, all tooltips
+ // in the chart group appear.
+ if (!isChartHovered(chartRef?.current)) {
+ return '';
+ }
+
+ let deDupedParams = params;
+
+ if (Array.isArray(params)) {
+ // We split each series into a complete and incomplete series, and they
+ // have the same name. The two series overlap at one point on the chart,
+ // to create a continuous line. This code prevents both series from
+ // showing up on the tooltip
+ const uniqueSeries = new Set();
+
+ deDupedParams = params.filter(param => {
+ // Filter null values from tooltip
+ if (param.value[1] === null) {
+ return false;
+ }
+
+ if (uniqueSeries.has(param.seriesName)) {
+ return false;
+ }
+
+ uniqueSeries.add(param.seriesName);
+ return true;
+ });
+ }
+
+ return getFormatter({
+ isGroupedByDate: true,
+ showTimeInTooltip: true,
+ valueFormatter: value => {
+ return formatChartValue(value, type, unit);
+ },
+ truncate: true,
+ utc: utc ?? false,
+ })(deDupedParams, asyncTicket);
+ };
+
+ let visibleSeriesCount = props.timeseries.length;
+ if (releaseSeries) {
+ visibleSeriesCount += 1;
+ }
+
+ const showLegend = visibleSeriesCount > 1;
+
+ return (
+ {
+ chartRef.current = e;
+
+ if (e?.getEchartsInstance) {
+ registerWithWidgetSyncContext(e.getEchartsInstance());
+ }
+ }}
+ autoHeightResize
+ series={[
+ ...props.timeseries.map(timeserie => {
+ return AreaSeries({
+ name: timeserie.field,
+ color: timeserie.color,
+ stack: 'area',
+ animation: false,
+ areaStyle: {
+ color: timeserie.color,
+ opacity: 1.0,
+ },
+ data: timeserie.data.map(datum => {
+ return [datum.timestamp, datum.value];
+ }),
+ });
+ }),
+ releaseSeries &&
+ LineSeries({
+ ...releaseSeries,
+ name: releaseSeries.seriesName,
+ data: [],
+ }),
+ ].filter(defined)}
+ grid={{
+ left: 0,
+ top: showLegend ? 25 : 10,
+ right: 4,
+ bottom: 0,
+ containLabel: true,
+ }}
+ legend={
+ showLegend
+ ? {
+ top: 0,
+ left: 0,
+ }
+ : undefined
+ }
+ tooltip={{
+ trigger: 'axis',
+ axisPointer: {
+ type: 'cross',
+ },
+ formatter,
+ }}
+ xAxis={{
+ axisLabel: {
+ padding: [0, 10, 0, 10],
+ width: 60,
+ },
+ splitNumber: 0,
+ }}
+ yAxis={{
+ axisLabel: {
+ formatter(value: number) {
+ return formatChartValue(value, type, unit);
+ },
+ },
+ axisPointer: {
+ type: 'line',
+ snap: false,
+ lineStyle: {
+ type: 'solid',
+ width: 0.5,
+ },
+ label: {
+ show: false,
+ },
+ },
+ }}
+ {...chartZoomProps}
+ isGroupedByDate
+ useMultilineDate
+ start={start ? new Date(start) : undefined}
+ end={end ? new Date(end) : undefined}
+ period={period}
+ utc={utc ?? undefined}
+ />
+ );
+}
diff --git a/static/app/views/dashboards/widgets/areaChartWidget/sampleLatencyTimeSeries.json b/static/app/views/dashboards/widgets/areaChartWidget/sampleLatencyTimeSeries.json
new file mode 100644
index 00000000000000..f862de4e76249e
--- /dev/null
+++ b/static/app/views/dashboards/widgets/areaChartWidget/sampleLatencyTimeSeries.json
@@ -0,0 +1,205 @@
+{
+ "field": "avg(messaging.message.receive.latency)",
+ "data": [
+ {
+ "timestamp": "2024-12-09T22:00:00Z",
+ "value": 15.797387473297285
+ },
+ {
+ "timestamp": "2024-12-09T22:30:00Z",
+ "value": 6.098785205112891
+ },
+ {
+ "timestamp": "2024-12-09T23:00:00Z",
+ "value": 10.023521061981072
+ },
+ {
+ "timestamp": "2024-12-09T23:30:00Z",
+ "value": 6.303244308149576
+ },
+ {
+ "timestamp": "2024-12-10T00:00:00Z",
+ "value": 11.352363343199878
+ },
+ {
+ "timestamp": "2024-12-10T00:30:00Z",
+ "value": 4.845476325747717
+ },
+ {
+ "timestamp": "2024-12-10T01:00:00Z",
+ "value": 11.251521855070287
+ },
+ {
+ "timestamp": "2024-12-10T01:30:00Z",
+ "value": 4.425860645618586
+ },
+ {
+ "timestamp": "2024-12-10T02:00:00Z",
+ "value": 9.989218687466126
+ },
+ {
+ "timestamp": "2024-12-10T02:30:00Z",
+ "value": 4.025772756170272
+ },
+ {
+ "timestamp": "2024-12-10T03:00:00Z",
+ "value": 9.17541579241641
+ },
+ {
+ "timestamp": "2024-12-10T03:30:00Z",
+ "value": 4.2380217489924865
+ },
+ {
+ "timestamp": "2024-12-10T04:00:00Z",
+ "value": 8.027566877217474
+ },
+ {
+ "timestamp": "2024-12-10T04:30:00Z",
+ "value": 5.295137636393576
+ },
+ {
+ "timestamp": "2024-12-10T05:00:00Z",
+ "value": 8.656494836655359
+ },
+ {
+ "timestamp": "2024-12-10T05:30:00Z",
+ "value": 5.475662654003712
+ },
+ {
+ "timestamp": "2024-12-10T06:00:00Z",
+ "value": 9.793632841698757
+ },
+ {
+ "timestamp": "2024-12-10T06:30:00Z",
+ "value": 5.591866790430932
+ },
+ {
+ "timestamp": "2024-12-10T07:00:00Z",
+ "value": 9.187478698931766
+ },
+ {
+ "timestamp": "2024-12-10T07:30:00Z",
+ "value": 3.779925204205954
+ },
+ {
+ "timestamp": "2024-12-10T08:00:00Z",
+ "value": 10.315176397597504
+ },
+ {
+ "timestamp": "2024-12-10T08:30:00Z",
+ "value": 5.101678176894567
+ },
+ {
+ "timestamp": "2024-12-10T09:00:00Z",
+ "value": 10.405819741300553
+ },
+ {
+ "timestamp": "2024-12-10T09:30:00Z",
+ "value": 5.157673142672812
+ },
+ {
+ "timestamp": "2024-12-10T10:00:00Z",
+ "value": 13.454678120116997
+ },
+ {
+ "timestamp": "2024-12-10T10:30:00Z",
+ "value": 5.633257152519796
+ },
+ {
+ "timestamp": "2024-12-10T11:00:00Z",
+ "value": 12.38331484233643
+ },
+ {
+ "timestamp": "2024-12-10T11:30:00Z",
+ "value": 6.7168414807525245
+ },
+ {
+ "timestamp": "2024-12-10T12:00:00Z",
+ "value": 10.82394699109634
+ },
+ {
+ "timestamp": "2024-12-10T12:30:00Z",
+ "value": 6.649313439563898
+ },
+ {
+ "timestamp": "2024-12-10T13:00:00Z",
+ "value": 10.957238674362912
+ },
+ {
+ "timestamp": "2024-12-10T13:30:00Z",
+ "value": 7.891612896606848
+ },
+ {
+ "timestamp": "2024-12-10T14:00:00Z",
+ "value": 9.83684972309017
+ },
+ {
+ "timestamp": "2024-12-10T14:30:00Z",
+ "value": 4.472633330313572
+ },
+ {
+ "timestamp": "2024-12-10T15:00:00Z",
+ "value": 13.886404361521333
+ },
+ {
+ "timestamp": "2024-12-10T15:30:00Z",
+ "value": 9.212753388080626
+ },
+ {
+ "timestamp": "2024-12-10T16:00:00Z",
+ "value": 12.213060522650725
+ },
+ {
+ "timestamp": "2024-12-10T16:30:00Z",
+ "value": 5.308362659314872
+ },
+ {
+ "timestamp": "2024-12-10T17:00:00Z",
+ "value": 12.167955277129803
+ },
+ {
+ "timestamp": "2024-12-10T17:30:00Z",
+ "value": 5.95843470061378
+ },
+ {
+ "timestamp": "2024-12-10T18:00:00Z",
+ "value": 11.623431372484815
+ },
+ {
+ "timestamp": "2024-12-10T18:30:00Z",
+ "value": 7.133246343117738
+ },
+ {
+ "timestamp": "2024-12-10T19:00:00Z",
+ "value": 9.91670999332601
+ },
+ {
+ "timestamp": "2024-12-10T19:30:00Z",
+ "value": 8.220551566043556
+ },
+ {
+ "timestamp": "2024-12-10T20:00:00Z",
+ "value": 10.644324473372116
+ },
+ {
+ "timestamp": "2024-12-10T20:30:00Z",
+ "value": 6.380262999155083
+ },
+ {
+ "timestamp": "2024-12-10T21:00:00Z",
+ "value": 11.611059631291011
+ },
+ {
+ "timestamp": "2024-12-10T21:30:00Z",
+ "value": 4.830427051517974
+ },
+ {
+ "timestamp": "2024-12-10T22:00:00Z",
+ "value": 14.018863061817425
+ },
+ {
+ "timestamp": "2024-12-10T22:30:00Z",
+ "value": 0
+ }
+ ]
+}
diff --git a/static/app/views/dashboards/widgets/areaChartWidget/sampleSpanDurationTimeSeries.json b/static/app/views/dashboards/widgets/areaChartWidget/sampleSpanDurationTimeSeries.json
new file mode 100644
index 00000000000000..815c94d3eeac81
--- /dev/null
+++ b/static/app/views/dashboards/widgets/areaChartWidget/sampleSpanDurationTimeSeries.json
@@ -0,0 +1,205 @@
+{
+ "field": "avg(span.duration)",
+ "data": [
+ {
+ "timestamp": "2024-12-09T22:00:00Z",
+ "value": 97.16045076146938
+ },
+ {
+ "timestamp": "2024-12-09T22:30:00Z",
+ "value": 73.81276498986948
+ },
+ {
+ "timestamp": "2024-12-09T23:00:00Z",
+ "value": 83.65629482432625
+ },
+ {
+ "timestamp": "2024-12-09T23:30:00Z",
+ "value": 77.4773209216508
+ },
+ {
+ "timestamp": "2024-12-10T00:00:00Z",
+ "value": 88.87920889091058
+ },
+ {
+ "timestamp": "2024-12-10T00:30:00Z",
+ "value": 74.81035769529551
+ },
+ {
+ "timestamp": "2024-12-10T01:00:00Z",
+ "value": 74.18713473976618
+ },
+ {
+ "timestamp": "2024-12-10T01:30:00Z",
+ "value": 75.51014337607224
+ },
+ {
+ "timestamp": "2024-12-10T02:00:00Z",
+ "value": 71.75338738667213
+ },
+ {
+ "timestamp": "2024-12-10T02:30:00Z",
+ "value": 71.44267608528102
+ },
+ {
+ "timestamp": "2024-12-10T03:00:00Z",
+ "value": 73.23258991739772
+ },
+ {
+ "timestamp": "2024-12-10T03:30:00Z",
+ "value": 73.62710321499654
+ },
+ {
+ "timestamp": "2024-12-10T04:00:00Z",
+ "value": 73.24679865212109
+ },
+ {
+ "timestamp": "2024-12-10T04:30:00Z",
+ "value": 71.00435116898733
+ },
+ {
+ "timestamp": "2024-12-10T05:00:00Z",
+ "value": 78.74461664047831
+ },
+ {
+ "timestamp": "2024-12-10T05:30:00Z",
+ "value": 72.30733243572487
+ },
+ {
+ "timestamp": "2024-12-10T06:00:00Z",
+ "value": 78.42600782679243
+ },
+ {
+ "timestamp": "2024-12-10T06:30:00Z",
+ "value": 74.29718208848486
+ },
+ {
+ "timestamp": "2024-12-10T07:00:00Z",
+ "value": 77.86389655145648
+ },
+ {
+ "timestamp": "2024-12-10T07:30:00Z",
+ "value": 74.11896494459268
+ },
+ {
+ "timestamp": "2024-12-10T08:00:00Z",
+ "value": 77.83106835851203
+ },
+ {
+ "timestamp": "2024-12-10T08:30:00Z",
+ "value": 80.87383050251273
+ },
+ {
+ "timestamp": "2024-12-10T09:00:00Z",
+ "value": 85.53912058349381
+ },
+ {
+ "timestamp": "2024-12-10T09:30:00Z",
+ "value": 82.16649885587427
+ },
+ {
+ "timestamp": "2024-12-10T10:00:00Z",
+ "value": 96.46710756423539
+ },
+ {
+ "timestamp": "2024-12-10T10:30:00Z",
+ "value": 90.2296730934056
+ },
+ {
+ "timestamp": "2024-12-10T11:00:00Z",
+ "value": 86.49672620288618
+ },
+ {
+ "timestamp": "2024-12-10T11:30:00Z",
+ "value": 81.76636615572134
+ },
+ {
+ "timestamp": "2024-12-10T12:00:00Z",
+ "value": 91.30479930843254
+ },
+ {
+ "timestamp": "2024-12-10T12:30:00Z",
+ "value": 86.5261930482867
+ },
+ {
+ "timestamp": "2024-12-10T13:00:00Z",
+ "value": 88.58528428072619
+ },
+ {
+ "timestamp": "2024-12-10T13:30:00Z",
+ "value": 84.57326040990161
+ },
+ {
+ "timestamp": "2024-12-10T14:00:00Z",
+ "value": 82.43636182893853
+ },
+ {
+ "timestamp": "2024-12-10T14:30:00Z",
+ "value": 90.33512756892627
+ },
+ {
+ "timestamp": "2024-12-10T15:00:00Z",
+ "value": 103.27654137156406
+ },
+ {
+ "timestamp": "2024-12-10T15:30:00Z",
+ "value": 127.10951181976695
+ },
+ {
+ "timestamp": "2024-12-10T16:00:00Z",
+ "value": 95.72899826334971
+ },
+ {
+ "timestamp": "2024-12-10T16:30:00Z",
+ "value": 84.04312923323451
+ },
+ {
+ "timestamp": "2024-12-10T17:00:00Z",
+ "value": 85.59506151608197
+ },
+ {
+ "timestamp": "2024-12-10T17:30:00Z",
+ "value": 82.71849476828766
+ },
+ {
+ "timestamp": "2024-12-10T18:00:00Z",
+ "value": 89.57229292359843
+ },
+ {
+ "timestamp": "2024-12-10T18:30:00Z",
+ "value": 81.22558140363343
+ },
+ {
+ "timestamp": "2024-12-10T19:00:00Z",
+ "value": 86.8826705009411
+ },
+ {
+ "timestamp": "2024-12-10T19:30:00Z",
+ "value": 79.4383370343256
+ },
+ {
+ "timestamp": "2024-12-10T20:00:00Z",
+ "value": 82.63652211651058
+ },
+ {
+ "timestamp": "2024-12-10T20:30:00Z",
+ "value": 77.87329880924307
+ },
+ {
+ "timestamp": "2024-12-10T21:00:00Z",
+ "value": 82.0189272882679
+ },
+ {
+ "timestamp": "2024-12-10T21:30:00Z",
+ "value": 80.03621684743777
+ },
+ {
+ "timestamp": "2024-12-10T22:00:00Z",
+ "value": 80.69735445090794
+ },
+ {
+ "timestamp": "2024-12-10T22:30:00Z",
+ "value": 0
+ }
+ ]
+}