Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions static/app/components/charts/baseChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,11 @@ const getTooltipStyles = (p: {theme: Theme}) => css`
justify-content: flex-start;
align-items: baseline;
}
.tooltip-label-centered {
display: flex;
justify-content: center;
align-items: center;
}
.tooltip-code-no-margin {
padding-left: 0;
margin-left: 0;
Expand Down
15 changes: 0 additions & 15 deletions static/app/components/charts/components/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,21 +243,6 @@ export function getFormatter({
serie
);

if (serie.seriesType === 'heatmap') {
Comment thread
nikkikapadia marked this conversation as resolved.
const zAxisCountValue = (getSeriesValue(serie, 2) ?? 0).toString();
const yAxisValue = valueFormatter(
getSeriesValue(serie, 1),
serie.seriesName,
serie
);

acc.series.push(
`<div><span class="tooltip-label"><strong>${yAxisValue}</strong></span> ${zAxisCountValue}</div>`
);

return acc;
}

const value = valueFormatter(getSeriesValue(serie, 1), serie.seriesName, serie);

const marker = markerFormatter(serie.marker ?? '', serie.seriesName);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,32 @@
import 'echarts/lib/chart/heatmap';

import {useRef} from 'react';
import {Fragment, useRef, type ReactNode} from 'react';
import {useTheme} from '@emotion/react';
import type {
TooltipFormatterCallback,
TopLevelFormatterParams,
} from 'echarts/types/dist/shared';

import {Flex} from '@sentry/scraps/layout';
import {useRenderToString} from '@sentry/scraps/renderToString';

import {BaseChart} from 'sentry/components/charts/baseChart';
import {getFormatter} from 'sentry/components/charts/components/tooltip';
import {isChartHovered, truncationFormatter} from 'sentry/components/charts/utils';
import {defaultFormatAxisLabel} from 'sentry/components/charts/components/tooltip';
import {isChartHovered} from 'sentry/components/charts/utils';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {t} from 'sentry/locale';
import type {ReactEchartsRef} from 'sentry/types/echarts';
import {defined} from 'sentry/utils';
import {formatAbbreviatedNumber} from 'sentry/utils/formatters';
import {ECHARTS_MISSING_DATA_VALUE} from 'sentry/utils/timeSeries/timeSeriesItemToEChartsDataPoint';
import {useOrganization} from 'sentry/utils/useOrganization';
import {NO_PLOTTABLE_VALUES} from 'sentry/views/dashboards/widgets/common/settings';
import {plottablesCanBeVisualized} from 'sentry/views/dashboards/widgets/plottablesCanBeVisualized';
import {formatTooltipValue} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatTooltipValue';
import {formatXAxisTimestamp} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatXAxisTimestamp';
import {formatYAxisValue} from 'sentry/views/dashboards/widgets/timeSeriesWidget/formatters/formatYAxisValue';
import {FALLBACK_TYPE} from 'sentry/views/dashboards/widgets/timeSeriesWidget/settings';
import {getExploreUrl, type GetExploreUrlArgs} from 'sentry/views/explore/utils';

import {HeatMap} from './plottables/heatMap';
import type {HeatMapPlottable} from './plottables/heatMapPlottable';
Expand All @@ -34,11 +41,17 @@ interface HeatMapWidgetVisualizationProps {
* Experimental! Specify the Z-axis scale type. Logarithmic scales can be much more useful for values with a high range.
*/
scale?: 'linear' | 'log';
/**
* getExploreUrl props that will be used to generate an explore link for the tooltip. Omitting this will not generate an explore link.
*/
tooltipExploreUrlArgs?: Omit<GetExploreUrlArgs, 'organization'>;
}

export function HeatMapWidgetVisualization(props: HeatMapWidgetVisualizationProps) {
const {plottables} = props;
const theme = useTheme();
const organization = useOrganization();
const renderToString = useRenderToString();

const pageFilters = usePageFilters();
const {start, end, period, utc} = pageFilters.selection.datetime;
Expand Down Expand Up @@ -68,43 +81,155 @@ export function HeatMapWidgetVisualization(props: HeatMapWidgetVisualizationProp
const Zmax =
scale === 'log' ? Math.log1p(heatMapPlottable.Zend) : heatMapPlottable.Zend;

/** Extract the numeric value from ECharts tooltip param.value. */
function extractValue(data: unknown): number | null {
// param.value can be either:
// 1. The numeric value directly (for heatmap charts with axis trigger)
// 2. An object {name, value} (depends on series config)
if (typeof data === 'number') {
return data;
}

const value = (data as {value?: unknown} | null | undefined)?.value;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Any way we can get rid of this type cast 👀

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unfortunately not, the original typing for this value is from e-charts and this type in particular is not exported 😢

return typeof value === 'number' ? value : null;
}

// Create tooltip formatter
const formatTooltip: TooltipFormatterCallback<TopLevelFormatterParams> = (
params,
asyncTicket
) => {
const formatTooltip: TooltipFormatterCallback<TopLevelFormatterParams> = params => {
// Only show the tooltip of the current chart. Otherwise, all tooltips
// in the chart group appear.
if (!isChartHovered(chartRef?.current)) {
return '';
}

return getFormatter({
isGroupedByDate: true,
showTimeInTooltip: true,
nameFormatter: function (seriesName, _nameFormatterParams) {
return truncationFormatter(seriesName, true, false);
},
valueFormatter: function (value, _field, _valueFormatterParams) {
const bucketSize = heatMapPlottable.heatMapSeries.meta.yAxis.bucketSize;
const fieldType = heatMapPlottable?.yAxisValueType ?? FALLBACK_TYPE;

const yAxisMinValueFormatted = formatTooltipValue(
value,
fieldType,
heatMapPlottable.yAxisValueUnit ?? undefined
);
const yAxisMaxValueFormatted = formatTooltipValue(
value + bucketSize,
fieldType,
heatMapPlottable.yAxisValueUnit ?? undefined
);

return `${yAxisMinValueFormatted} – ${yAxisMaxValueFormatted}`;
},
truncate: false,
utc: utc ?? false,
})(params, asyncTicket);
const seriesParams = Array.isArray(params) ? params : [params];

// Filter null values from tooltip
const filteredParams = seriesParams.filter(param => {
// @ts-expect-error ECharts types param.value as unknown, but we know it's [xAxis, yAxis, zAxis] from our HeatMap plottable
const value = extractValue(param.value[2]);
return value !== null;
});
Comment thread
cursor[bot] marked this conversation as resolved.

let formattedXValue = ECHARTS_MISSING_DATA_VALUE;

return renderToString(
<Fragment>
<div className="tooltip-series">
{filteredParams.map(param => {
const xAxisBucketSize = heatMapPlottable.heatMapSeries.meta.xAxis.bucketSize;
const yAxisBucketSize = heatMapPlottable.heatMapSeries.meta.yAxis.bucketSize;
const yAxisUnit = heatMapPlottable?.yAxisValueUnit;
const yAxisValueType = heatMapPlottable?.yAxisValueType ?? FALLBACK_TYPE;
Comment thread
nikkikapadia marked this conversation as resolved.
Outdated

let rawXValue: number | undefined;
let rawYValue: number | undefined;

let formattedYValue = ECHARTS_MISSING_DATA_VALUE;
let formattedZValue = ECHARTS_MISSING_DATA_VALUE;
if (Array.isArray(param.value) && param.value.length === 3) {
const [xValue, yValue, zValue] = param.value;

if (defined(xValue) && typeof xValue === 'number') {
rawXValue = xValue;
// bucket size seems to be in seconds but we need to convert to milliseconds
formattedXValue = defaultFormatAxisLabel(
xValue,
true,
utc ?? false,
true,
false,
xAxisBucketSize * 1000
).toString();
}

if (defined(yValue) && typeof yValue === 'number') {
rawYValue = yValue;
const yAxisMinValueFormatted = formatTooltipValue(
yValue,
yAxisValueType,
yAxisUnit ?? undefined
);

if (yAxisBucketSize === 0) {
formattedYValue = yAxisMinValueFormatted;
} else {
const yAxisMaxValueFormatted = formatTooltipValue(
yValue + yAxisBucketSize,
yAxisValueType,
yAxisUnit ?? undefined
);

formattedYValue = `${yAxisMinValueFormatted} – ${yAxisMaxValueFormatted}`;
}
}

if (defined(zValue) && typeof zValue === 'number') {
formattedZValue = formatAbbreviatedNumber(zValue, 4, false);
}
Comment thread
nikkikapadia marked this conversation as resolved.
}

let exploreLink: ReactNode;

if (defined(rawXValue) && defined(rawYValue) && props.tooltipExploreUrlArgs) {
const xAxisMaxValue = rawXValue + xAxisBucketSize * 1000;
const yAxisMaxValue = rawYValue + yAxisBucketSize;

const exploreUrlProps: GetExploreUrlArgs = {
organization,
...props.tooltipExploreUrlArgs,
selection: {
...pageFilters.selection,
datetime: {
...pageFilters.selection.datetime,
start: new Date(rawXValue),
end: new Date(xAxisMaxValue),
period: null,
},
},
// TODO(nikki): we're only handling metrics for now but if we're looking to support other explore
// surfaces then we'll need to add more logic here
crossEvents: props.tooltipExploreUrlArgs?.crossEvents?.map(crossEvent => {
if (crossEvent.type === 'metrics') {
return {
...crossEvent,
query:
yAxisBucketSize === 0
? `value:<=${rawYValue}`
: `value:>=${rawYValue} value:<${yAxisMaxValue}`,
};
}
return crossEvent;
}),
Comment thread
cursor[bot] marked this conversation as resolved.
};

const tracesLink = getExploreUrl(exploreUrlProps);
exploreLink = <a href={tracesLink}>{t('View related traces')}</a>;
Comment thread
cursor[bot] marked this conversation as resolved.
}

return (
<Fragment key={param.seriesIndex}>
<div>
<span className="tooltip-label">
<strong>{formattedYValue}</strong>
</span>{' '}
{formattedZValue}
</div>
{exploreLink && (
<div>
<span className="tooltip-label tooltip-label-centered">
{exploreLink}
</span>
</div>
)}
</Fragment>
);
})}
</div>
<div className="tooltip-footer tooltip-footer-centered">{formattedXValue}</div>
<div className="tooltip-arrow" />
</Fragment>
);
};

return (
Expand All @@ -118,6 +243,8 @@ export function HeatMapWidgetVisualization(props: HeatMapWidgetVisualizationProp
ref={chartRef}
tooltip={{
show: true,
enterable: true,
extraCssText: `box-shadow: 0 0 0 1px ${theme.tokens.border.transparent.neutral.muted}, ${theme.shadow.high}; z-index: ${theme.zIndex.tooltip} !important; pointer-events: auto !important;`,
axisPointer: {
show: false,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class HeatMap implements HeatMapPlottable {

toSeries(plottingOptions: HeatMapPlottingOptions): SeriesOption[] {
const {heatMapSeries} = this;
const {scale = 'linear'} = plottingOptions;
const {scale = 'linear', theme} = plottingOptions;

return [
{
Expand All @@ -64,7 +64,10 @@ export class HeatMap implements HeatMapPlottable {
];
}),
emphasis: {
disabled: true,
itemStyle: {
borderColor: theme.tokens.content.primary,
borderWidth: 2,
Comment thread
nikkikapadia marked this conversation as resolved.
Outdated
},
},
},
];
Expand Down
18 changes: 17 additions & 1 deletion static/app/views/explore/metrics/metricsHeatMap.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {useMemo} from 'react';
import type {UseQueryResult} from '@tanstack/react-query';

import {t} from 'sentry/locale';
Expand All @@ -12,9 +13,10 @@ import {
useMetricName,
useMetricVisualize,
useMetricVisualizes,
useTraceMetric,
} from 'sentry/views/explore/metrics/metricsQueryParams';
import {STACKED_GRAPH_HEIGHT} from 'sentry/views/explore/metrics/settings';
import {prettifyAggregation} from 'sentry/views/explore/utils';
import {prettifyAggregation, type GetExploreUrlArgs} from 'sentry/views/explore/utils';

interface MetricsHeatMapProps {
actions: React.ReactNode;
Expand All @@ -27,6 +29,7 @@ export function MetricsHeatMap({heatmapResult, actions, title}: MetricsHeatMapPr
const visualizes = useMetricVisualizes();
const metricLabel = useMetricLabel();
const metricName = useMetricName();
const metric = useTraceMetric();

const {data: heatMapSeries, isPending, error} = heatmapResult;

Expand All @@ -36,6 +39,18 @@ export function MetricsHeatMap({heatmapResult, actions, title}: MetricsHeatMapPr
? metricName
: (title ?? metricLabel ?? prettifyAggregation(aggregate) ?? aggregate);

const tooltipExploreUrlArgs: Omit<GetExploreUrlArgs, 'organization'> = useMemo(() => {
return {
crossEvents: [
{
type: 'metrics',
query: '',
metric,
},
],
};
}, [metric]);

return (
<WidgetWrapper>
<Widget
Expand All @@ -52,6 +67,7 @@ export function MetricsHeatMap({heatmapResult, actions, title}: MetricsHeatMapPr
<HeatMapWidgetVisualization
plottables={[new HeatMap(heatMapSeries)]}
scale="log"
tooltipExploreUrlArgs={tooltipExploreUrlArgs}
/>
)
}
Expand Down
Loading