|
@@ -18,10 +18,39 @@ import {
|
|
|
getSummaryValueForOp,
|
|
|
type MetricsSamplesResults,
|
|
|
} from 'sentry/utils/metrics/useMetricsSamples';
|
|
|
-import {fitToValueRect, getValueRect} from 'sentry/views/ddm/chart/chartUtils';
|
|
|
-import type {CombinedMetricChartProps, Series} from 'sentry/views/ddm/chart/types';
|
|
|
+import {fitToValueRect} from 'sentry/views/ddm/chart/chartUtils';
|
|
|
+import type {
|
|
|
+ CombinedMetricChartProps,
|
|
|
+ ScatterSeries,
|
|
|
+ Series,
|
|
|
+} from 'sentry/views/ddm/chart/types';
|
|
|
import type {Sample} from 'sentry/views/ddm/widget';
|
|
|
|
|
|
+export const SAMPLES_X_AXIS_ID = 'xAxisSamples';
|
|
|
+export const SAMPLES_Y_AXIS_ID = 'yAxisSamples';
|
|
|
+
|
|
|
+function getValueRectFromSeries(series: Series[]) {
|
|
|
+ const referenceSeries = series[0];
|
|
|
+ if (!referenceSeries) {
|
|
|
+ return {xMin: -Infinity, xMax: Infinity, yMin: -Infinity, yMax: Infinity};
|
|
|
+ }
|
|
|
+ const seriesWithSameUnit = series.filter(
|
|
|
+ s => s.unit === referenceSeries.unit && !s.hidden
|
|
|
+ );
|
|
|
+ const scalingFactor = referenceSeries.scalingFactor ?? 1;
|
|
|
+ const xValues = referenceSeries.data.map(entry => entry.name);
|
|
|
+ const yValues = [referenceSeries, ...seriesWithSameUnit].flatMap(s =>
|
|
|
+ s.data.map(entry => entry.value)
|
|
|
+ );
|
|
|
+
|
|
|
+ return {
|
|
|
+ xMin: Math.min(...xValues),
|
|
|
+ xMax: Math.max(...xValues),
|
|
|
+ yMin: Math.min(0, ...yValues) / scalingFactor,
|
|
|
+ yMax: Math.max(0, ...yValues) / scalingFactor,
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
type UseChartSamplesProps = {
|
|
|
timeseries: Series[];
|
|
|
chartRef?: RefObject<ReactEchartsRef>;
|
|
@@ -36,17 +65,6 @@ type UseChartSamplesProps = {
|
|
|
|
|
|
// TODO: remove this once we have a stabilized type for this
|
|
|
type ChartSample = MetricCorrelation & MetricSummary;
|
|
|
-
|
|
|
-function getDateRange(timeseries: Series[]) {
|
|
|
- if (!timeseries?.length) {
|
|
|
- return {min: -Infinity, max: Infinity};
|
|
|
- }
|
|
|
- const min = timeseries[0].data[0].name as number;
|
|
|
- const max = timeseries[0].data[timeseries[0].data.length - 1].name as number;
|
|
|
-
|
|
|
- return {min, max};
|
|
|
-}
|
|
|
-
|
|
|
type EChartMouseEventParam = Parameters<EChartClickHandler>[0];
|
|
|
|
|
|
export function useMetricChartSamples({
|
|
@@ -59,9 +77,8 @@ export function useMetricChartSamples({
|
|
|
}: UseChartSamplesProps) {
|
|
|
const theme = useTheme();
|
|
|
const chartRef = useRef<ReactEchartsRef>(null);
|
|
|
- const scalingFactor = timeseries?.[0]?.scalingFactor ?? 1;
|
|
|
|
|
|
- const [valueRect, setValueRect] = useState(getValueRect(chartRef));
|
|
|
+ const [valueRect, setValueRect] = useState(() => getValueRectFromSeries(timeseries));
|
|
|
|
|
|
const samples: Record<string, ChartSample> = useMemo(() => {
|
|
|
return (correlations ?? [])
|
|
@@ -77,40 +94,31 @@ export function useMetricChartSamples({
|
|
|
useEffect(() => {
|
|
|
// Changes in timeseries change the valueRect since the timeseries yAxis auto scales
|
|
|
// and scatter yAxis needs to match the scale
|
|
|
- setValueRect(getValueRect(chartRef));
|
|
|
- }, [chartRef, timeseries]);
|
|
|
+ setValueRect(getValueRectFromSeries(timeseries));
|
|
|
+ }, [timeseries]);
|
|
|
|
|
|
const xAxis: XAXisOption = useMemo(() => {
|
|
|
- const {min, max} = getDateRange(timeseries);
|
|
|
-
|
|
|
return {
|
|
|
- id: 'xAxisScatter',
|
|
|
- scale: false,
|
|
|
+ id: SAMPLES_X_AXIS_ID,
|
|
|
show: false,
|
|
|
axisLabel: {
|
|
|
- formatter: () => {
|
|
|
- return '';
|
|
|
- },
|
|
|
+ show: false,
|
|
|
},
|
|
|
axisPointer: {
|
|
|
type: 'none',
|
|
|
},
|
|
|
- min: Math.max(valueRect.xMin, min),
|
|
|
- max: Math.min(valueRect.xMax, max),
|
|
|
+ min: valueRect.xMin,
|
|
|
+ max: valueRect.xMax,
|
|
|
};
|
|
|
- }, [valueRect.xMin, valueRect.xMax, timeseries]);
|
|
|
+ }, [valueRect.xMin, valueRect.xMax]);
|
|
|
|
|
|
const yAxis: YAXisOption = useMemo(() => {
|
|
|
return {
|
|
|
- id: 'yAxisScatter',
|
|
|
- scale: false,
|
|
|
+ id: SAMPLES_Y_AXIS_ID,
|
|
|
show: false,
|
|
|
axisLabel: {
|
|
|
- formatter: () => {
|
|
|
- return '';
|
|
|
- },
|
|
|
+ show: false,
|
|
|
},
|
|
|
-
|
|
|
min: valueRect.yMin,
|
|
|
max: valueRect.yMax,
|
|
|
};
|
|
@@ -137,67 +145,6 @@ export function useMetricChartSamples({
|
|
|
[getSample, onClick]
|
|
|
);
|
|
|
|
|
|
- const series = useMemo(() => {
|
|
|
- if (isCumulativeOp(operation)) {
|
|
|
- // TODO: for now we do not show samples for cumulative operations,
|
|
|
- // we will implement them as marklines
|
|
|
- return [];
|
|
|
- }
|
|
|
-
|
|
|
- return Object.values(samples).map(sample => {
|
|
|
- const isHighlighted = highlightedSampleId === sample.transactionId;
|
|
|
-
|
|
|
- const xValue = moment(sample.timestamp).valueOf();
|
|
|
- const yValue = (((sample.min ?? 0) + (sample.max ?? 0)) / 2) * scalingFactor;
|
|
|
-
|
|
|
- const [xPosition, yPosition] = fitToValueRect(xValue, yValue, valueRect);
|
|
|
-
|
|
|
- const symbol = yPosition === yValue ? 'circle' : 'arrow';
|
|
|
- const symbolRotate = yPosition > yValue ? 180 : 0;
|
|
|
-
|
|
|
- return {
|
|
|
- seriesName: sample.transactionId,
|
|
|
- id: sample.spanId,
|
|
|
- operation: '',
|
|
|
- unit: '',
|
|
|
- symbolSize: isHighlighted ? 20 : 10,
|
|
|
- animation: false,
|
|
|
- symbol,
|
|
|
- symbolRotate,
|
|
|
- color: theme.purple400,
|
|
|
- // TODO: for now we just pass these ids through, but we should probably index
|
|
|
- // samples by an id and then just pass that reference
|
|
|
- itemStyle: {
|
|
|
- color: theme.purple400,
|
|
|
- opacity: 1,
|
|
|
- },
|
|
|
- yAxisIndex: 1,
|
|
|
- xAxisIndex: 1,
|
|
|
- xValue,
|
|
|
- yValue,
|
|
|
- tooltip: {
|
|
|
- axisPointer: {
|
|
|
- type: 'none',
|
|
|
- },
|
|
|
- },
|
|
|
- data: [
|
|
|
- {
|
|
|
- name: xPosition,
|
|
|
- value: yPosition,
|
|
|
- },
|
|
|
- ],
|
|
|
- z: 10,
|
|
|
- };
|
|
|
- });
|
|
|
- }, [
|
|
|
- operation,
|
|
|
- samples,
|
|
|
- highlightedSampleId,
|
|
|
- scalingFactor,
|
|
|
- valueRect,
|
|
|
- theme.purple400,
|
|
|
- ]);
|
|
|
-
|
|
|
const formatterOptions = useMemo(() => {
|
|
|
return {
|
|
|
isGroupedByDate: true,
|
|
@@ -218,6 +165,58 @@ export function useMetricChartSamples({
|
|
|
|
|
|
const applyChartProps = useCallback(
|
|
|
(baseProps: CombinedMetricChartProps): CombinedMetricChartProps => {
|
|
|
+ let series: ScatterSeries[] = [];
|
|
|
+ // TODO: for now we do not show samples for cumulative operations,
|
|
|
+ // we will implement them as marklines
|
|
|
+ if (!isCumulativeOp(operation)) {
|
|
|
+ const newYAxisIndex = Array.isArray(baseProps.yAxes) ? baseProps.yAxes.length : 1;
|
|
|
+ const newXAxisIndex = Array.isArray(baseProps.xAxes) ? baseProps.xAxes.length : 1;
|
|
|
+
|
|
|
+ series = Object.values(samples).map(sample => {
|
|
|
+ const isHighlighted = highlightedSampleId === sample.transactionId;
|
|
|
+
|
|
|
+ const xValue = moment(sample.timestamp).valueOf();
|
|
|
+ const yValue = ((sample.min ?? 0) + (sample.max ?? 0)) / 2;
|
|
|
+
|
|
|
+ const [xPosition, yPosition] = fitToValueRect(xValue, yValue, valueRect);
|
|
|
+
|
|
|
+ const symbol = yPosition === yValue ? 'circle' : 'arrow';
|
|
|
+ const symbolRotate = yPosition > yValue ? 180 : 0;
|
|
|
+
|
|
|
+ return {
|
|
|
+ seriesName: sample.transactionId,
|
|
|
+ id: sample.spanId,
|
|
|
+ operation: '',
|
|
|
+ unit: '',
|
|
|
+ symbolSize: isHighlighted ? 20 : 10,
|
|
|
+ animation: false,
|
|
|
+ symbol,
|
|
|
+ symbolRotate,
|
|
|
+ color: theme.purple400,
|
|
|
+ itemStyle: {
|
|
|
+ color: theme.purple400,
|
|
|
+ opacity: 1,
|
|
|
+ },
|
|
|
+ yAxisIndex: newYAxisIndex,
|
|
|
+ xAxisIndex: newXAxisIndex,
|
|
|
+ xValue,
|
|
|
+ yValue,
|
|
|
+ tooltip: {
|
|
|
+ axisPointer: {
|
|
|
+ type: 'none',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ data: [
|
|
|
+ {
|
|
|
+ name: xPosition,
|
|
|
+ value: yPosition,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ z: 10,
|
|
|
+ };
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
return {
|
|
|
...baseProps,
|
|
|
forwardedRef: mergeRefs([baseProps.forwardedRef, chartRef]),
|
|
@@ -257,7 +256,17 @@ export function useMetricChartSamples({
|
|
|
},
|
|
|
};
|
|
|
},
|
|
|
- [formatterOptions, handleClick, series, xAxis, yAxis]
|
|
|
+ [
|
|
|
+ formatterOptions,
|
|
|
+ handleClick,
|
|
|
+ highlightedSampleId,
|
|
|
+ operation,
|
|
|
+ samples,
|
|
|
+ theme.purple400,
|
|
|
+ valueRect,
|
|
|
+ xAxis,
|
|
|
+ yAxis,
|
|
|
+ ]
|
|
|
);
|
|
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -288,9 +297,8 @@ export function useMetricChartSamplesV2({
|
|
|
}: UseMetricChartSamplesV2Options) {
|
|
|
const theme = useTheme();
|
|
|
const chartRef = useRef<ReactEchartsRef>(null);
|
|
|
- const timeseriesScalingFactor = timeseries?.[0]?.scalingFactor ?? 1;
|
|
|
|
|
|
- const [valueRect, setValueRect] = useState(getValueRect(chartRef));
|
|
|
+ const [valueRect, setValueRect] = useState(() => getValueRectFromSeries(timeseries));
|
|
|
|
|
|
const samplesById = useMemo(() => {
|
|
|
return (samples ?? []).reduce((acc, sample) => {
|
|
@@ -302,102 +310,36 @@ export function useMetricChartSamplesV2({
|
|
|
useEffect(() => {
|
|
|
// Changes in timeseries change the valueRect since the timeseries yAxis auto scales
|
|
|
// and scatter yAxis needs to match the scale
|
|
|
- setValueRect(getValueRect(chartRef));
|
|
|
- }, [chartRef, timeseries]);
|
|
|
+ setValueRect(getValueRectFromSeries(timeseries));
|
|
|
+ }, [timeseries]);
|
|
|
|
|
|
const xAxis: XAXisOption = useMemo(() => {
|
|
|
- const {min, max} = getDateRange(timeseries);
|
|
|
-
|
|
|
return {
|
|
|
- id: 'xAxisScatter',
|
|
|
- scale: false,
|
|
|
+ id: SAMPLES_X_AXIS_ID,
|
|
|
show: false,
|
|
|
axisLabel: {
|
|
|
- formatter: () => {
|
|
|
- return '';
|
|
|
- },
|
|
|
+ show: false,
|
|
|
},
|
|
|
axisPointer: {
|
|
|
type: 'none',
|
|
|
},
|
|
|
- min: Math.max(valueRect.xMin, min),
|
|
|
- max: Math.min(valueRect.xMax, max),
|
|
|
+ min: valueRect.xMin,
|
|
|
+ max: valueRect.xMax,
|
|
|
};
|
|
|
- }, [valueRect.xMin, valueRect.xMax, timeseries]);
|
|
|
+ }, [valueRect.xMin, valueRect.xMax]);
|
|
|
|
|
|
const yAxis: YAXisOption = useMemo(() => {
|
|
|
return {
|
|
|
- id: 'yAxisScatter',
|
|
|
- scale: false,
|
|
|
+ id: SAMPLES_Y_AXIS_ID,
|
|
|
show: false,
|
|
|
axisLabel: {
|
|
|
- formatter: () => {
|
|
|
- return '';
|
|
|
- },
|
|
|
+ show: false,
|
|
|
},
|
|
|
-
|
|
|
min: valueRect.yMin,
|
|
|
max: valueRect.yMax,
|
|
|
};
|
|
|
}, [valueRect.yMin, valueRect.yMax]);
|
|
|
|
|
|
- const series = useMemo(() => {
|
|
|
- if (isCumulativeOp(operation)) {
|
|
|
- // TODO: for now we do not show samples for cumulative operations
|
|
|
- // figure out how should this be shown
|
|
|
- return [];
|
|
|
- }
|
|
|
-
|
|
|
- return (samples ?? []).map(sample => {
|
|
|
- const isHighlighted = highlightedSampleId === sample.id;
|
|
|
-
|
|
|
- const xValue = moment(sample.timestamp).valueOf();
|
|
|
- const value = getSummaryValueForOp(sample.summary, operation);
|
|
|
- const yValue = value * timeseriesScalingFactor;
|
|
|
-
|
|
|
- const [xPosition, yPosition] = fitToValueRect(xValue, yValue, valueRect);
|
|
|
-
|
|
|
- return {
|
|
|
- seriesName: sample.id,
|
|
|
- id: sample.id,
|
|
|
- operation: '',
|
|
|
- unit: '',
|
|
|
- symbolSize: isHighlighted ? 20 : 10,
|
|
|
- animation: false,
|
|
|
- symbol: yPosition === yValue ? 'circle' : 'arrow',
|
|
|
- symbolRotate: yPosition > yValue ? 180 : 0,
|
|
|
- color: theme.purple400,
|
|
|
- itemStyle: {
|
|
|
- color: theme.purple400,
|
|
|
- opacity: 1,
|
|
|
- },
|
|
|
- yAxisIndex: 1,
|
|
|
- xAxisIndex: 1,
|
|
|
- xValue,
|
|
|
- yValue,
|
|
|
- tooltip: {
|
|
|
- axisPointer: {
|
|
|
- type: 'none',
|
|
|
- },
|
|
|
- },
|
|
|
- data: [
|
|
|
- {
|
|
|
- name: xPosition,
|
|
|
- value: yPosition,
|
|
|
- },
|
|
|
- ],
|
|
|
- z: 10,
|
|
|
- };
|
|
|
- });
|
|
|
- }, [
|
|
|
- highlightedSampleId,
|
|
|
- operation,
|
|
|
- samples,
|
|
|
- theme.purple400,
|
|
|
- timeseriesScalingFactor,
|
|
|
- valueRect,
|
|
|
- ]);
|
|
|
-
|
|
|
const formatterOptions = useMemo(() => {
|
|
|
return {
|
|
|
isGroupedByDate: true,
|
|
@@ -428,6 +370,55 @@ export function useMetricChartSamplesV2({
|
|
|
|
|
|
const applyChartProps = useCallback(
|
|
|
(baseProps: CombinedMetricChartProps): CombinedMetricChartProps => {
|
|
|
+ let series: ScatterSeries[] = [];
|
|
|
+
|
|
|
+ const newYAxisIndex = Array.isArray(baseProps.yAxes) ? baseProps.yAxes.length : 1;
|
|
|
+ const newXAxisIndex = Array.isArray(baseProps.xAxes) ? baseProps.xAxes.length : 1;
|
|
|
+
|
|
|
+ if (!isCumulativeOp(operation)) {
|
|
|
+ series = (samples ?? []).map(sample => {
|
|
|
+ const isHighlighted = highlightedSampleId === sample.id;
|
|
|
+
|
|
|
+ const xValue = moment(sample.timestamp).valueOf();
|
|
|
+ const value = getSummaryValueForOp(sample.summary, operation);
|
|
|
+ const yValue = value;
|
|
|
+
|
|
|
+ const [xPosition, yPosition] = fitToValueRect(xValue, yValue, valueRect);
|
|
|
+
|
|
|
+ return {
|
|
|
+ seriesName: sample.id,
|
|
|
+ id: sample.id,
|
|
|
+ operation: '',
|
|
|
+ unit: '',
|
|
|
+ symbolSize: isHighlighted ? 20 : 10,
|
|
|
+ animation: false,
|
|
|
+ symbol: yPosition === yValue ? 'circle' : 'arrow',
|
|
|
+ symbolRotate: yPosition > yValue ? 180 : 0,
|
|
|
+ color: theme.purple400,
|
|
|
+ itemStyle: {
|
|
|
+ color: theme.purple400,
|
|
|
+ opacity: 1,
|
|
|
+ },
|
|
|
+ yAxisIndex: newYAxisIndex,
|
|
|
+ xAxisIndex: newXAxisIndex,
|
|
|
+ xValue,
|
|
|
+ yValue,
|
|
|
+ tooltip: {
|
|
|
+ axisPointer: {
|
|
|
+ type: 'none',
|
|
|
+ },
|
|
|
+ },
|
|
|
+ data: [
|
|
|
+ {
|
|
|
+ name: xPosition,
|
|
|
+ value: yPosition,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ z: 10,
|
|
|
+ };
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
return {
|
|
|
...baseProps,
|
|
|
forwardedRef: mergeRefs([baseProps.forwardedRef, chartRef]),
|
|
@@ -467,7 +458,17 @@ export function useMetricChartSamplesV2({
|
|
|
},
|
|
|
};
|
|
|
},
|
|
|
- [formatterOptions, handleClick, series, xAxis, yAxis]
|
|
|
+ [
|
|
|
+ formatterOptions,
|
|
|
+ handleClick,
|
|
|
+ highlightedSampleId,
|
|
|
+ operation,
|
|
|
+ samples,
|
|
|
+ theme.purple400,
|
|
|
+ valueRect,
|
|
|
+ xAxis,
|
|
|
+ yAxis,
|
|
|
+ ]
|
|
|
);
|
|
|
|
|
|
return useMemo(() => {
|