Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,86 @@ export default Storybook.story('HeatMapWidgetVisualization', story => {
</Fragment>
);
});

story('Tooltip Options', () => {
return (
<Fragment>
<p>
<Storybook.JSXNode name="HeatMapWidgetVisualization" /> supports two optional
tooltip action props. Click a cell to open the tooltip and see the links.
</p>
<p>
With no extra props, the tooltip shows the Y-axis bucket range and the Z-axis
count for the clicked cell.
</p>
<p>
Pass <code>makeExploreUrl</code> to add a <em>View connected spans</em> link in
the tooltip. The callback receives the Y-axis filter query (e.g.{' '}
<code>value:&gt;=200 value:&lt;250</code>) and a <code>PageFilters</code> object
whose datetime is narrowed to the clicked X-axis bucket. Use these to build a
cross-event query link into Explore.
</p>
<p>
<CodeBlock language="jsx">
{`<HeatMapWidgetVisualization
plottables={[new HeatMap(heatMapData)]}
makeExploreUrl={(query, filteredSelection) =>
getExploreUrl({
organization,
selection: filteredSelection,
crossEvents: [{
type: 'metrics',
metric,
query,
}]
})
}
/>`}
</CodeBlock>
</p>

<p>
Pass <code>makeLocalQueryUpdateUrl</code> to add an <em>Add to filter</em> link
in the tooltip. The callback receives the same Y-axis and X-axis filter query
and should return a URL that applies that filter to the current view. Navigation
is handled client-side via React Router.
</p>
<p>
<CodeBlock language="jsx">
{`<HeatMapWidgetVisualization
plottables={[new HeatMap(heatMapData)]}
makeLocalQueryUpdateUrl={(query, filteredSelection) =>
getMetricsUrl({
organization,
selection: filteredSelection,
metricQueries: [{
metric,
queryParams: {...queryParams, query: newQuery},
}],
})
}
/>`}
</CodeBlock>
</p>

<p>
Both props can be used together. The tooltip shows <em>View related traces</em>{' '}
and <em>Add to filter</em> as separate actions.
</p>
<LargeWidget>
<HeatMapWidgetVisualization
plottables={[new HeatMap(sampleLatencyHeatMap)]}
makeExploreUrl={(query, filteredSelection) =>
`/explore/traces/?query=${encodeURIComponent(query)}&start=${filteredSelection.datetime.start}&end=${filteredSelection.datetime.end}`
}
makeLocalQueryUpdateUrl={query =>
`/explore/metrics/?query=${encodeURIComponent(query)}`
}
/>
</LargeWidget>
</Fragment>
);
});
});

const LargeWidget = styled('div')`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'echarts/lib/chart/heatmap';

import {Fragment, useRef, type ReactNode} from 'react';
import {Fragment, useEffect, useRef} from 'react';
import {useTheme} from '@emotion/react';
import type {
TooltipFormatterCallback,
Expand All @@ -15,18 +15,18 @@ import {defaultFormatAxisLabel} from 'sentry/components/charts/components/toolti
import {isChartHovered} from 'sentry/components/charts/utils';
import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters';
import {t} from 'sentry/locale';
import type {PageFilters} from 'sentry/types/core';
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 {useNavigate} from 'sentry/utils/useNavigate';
import {NO_PLOTTABLE_VALUES} from 'sentry/views/dashboards/widgets/common/settings';
import {formatYAxisValue} from 'sentry/views/dashboards/widgets/heatMapWidget/formatters/formatYAxisValue';
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 {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 @@ -42,25 +42,73 @@ interface HeatMapWidgetVisualizationProps {
*/
plottables: [HeatMap, ...HeatMapPlottable[]];
/**
* Experimental! Specify the Z-axis scale type. Logarithmic scales can be much more useful for values with a high range.
* Callback that returns an explore URL for a given query and filtered datetime selection
*/
scale?: 'linear' | 'log';
makeExploreUrl?: (query: string, filteredSelection: PageFilters) => string;
/**
* getExploreUrl props that will be used to generate an explore link for the tooltip. Omitting this will not generate an explore link.
* Callback that returns an updated query string.
*/
makeLocalQueryUpdateUrl?: (query: string) => string;
/**
* Experimental! Specify the Z-axis scale type. Logarithmic scales can be much more useful for values with a high range.
*/
tooltipExploreUrlArgs?: Omit<GetExploreUrlArgs, 'organization'>;
scale?: 'linear' | 'log';
}

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

const navigate = useNavigate();
const pageFilters = usePageFilters();
const {start, end, period, utc} = pageFilters.selection.datetime;
const chartRef = useRef<ReactEchartsRef | null>(null);

// yes i am aware that this is UGLY but it's a hack so that we can use proper react routing.
// Basically the way ECharts renders the tooltip is by creating a string out of the dom tree.
// This means that we can't use any of the normal linking/routing tools that we use in React trees
// because they require contexts that won't be available properly in this string tree.
// Using the `<a>` tag will make the page reload and navigate to the url because it doesn't have
// link history context. Doing the navigation here preserves the link history context and makes the
// page navigation smoother instead of reloading the page every time a link is clicked.
Comment on lines +67 to +73
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.

😭

useEffect(() => {
const handleClick = (e: MouseEvent) => {
const localQueryUpdateTarget = (e.target as Element).closest(
'#local-query-update-link'
);
const tracesLinkTarget = (e.target as Element).closest('#traces-link');
if (!localQueryUpdateTarget && !tracesLinkTarget) {
return;
}
e.preventDefault();
const openInNewTab = e.metaKey || e.ctrlKey;
if (localQueryUpdateTarget) {
const localUrl = localQueryUpdateTarget.getAttribute(
'data-local-query-update-url'
);
if (localUrl) {
if (openInNewTab) {
window.open(localUrl, '_blank');
} else {
navigate(localUrl);
}
}
}
if (tracesLinkTarget) {
const tracesUrl = tracesLinkTarget.getAttribute('data-traces-link');
if (tracesUrl) {
if (openInNewTab) {
window.open(tracesUrl, '_blank');
} else {
navigate(tracesUrl);
}
}
}
};
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, [navigate]);
Comment thread
nikkikapadia marked this conversation as resolved.
Outdated

if (!plottablesCanBeVisualized(plottables)) {
throw new Error(NO_PLOTTABLE_VALUES);
}
Expand Down Expand Up @@ -126,7 +174,7 @@ export function HeatMapWidgetVisualization(props: HeatMapWidgetVisualizationProp

return renderToString(
<Fragment>
<div className="tooltip-series">
<div className="tooltip-series" style={{cursor: 'default'}}>
{filteredParams.map(param => {
let rawXValue: number | undefined;
let rawYValue: number | undefined;
Expand Down Expand Up @@ -175,42 +223,34 @@ export function HeatMapWidgetVisualization(props: HeatMapWidgetVisualizationProp
}
}

let exploreLink: ReactNode;
let tracesLink: string | undefined;
let localQueryUpdateUrl: string | undefined;
const metricsQuery = defined(rawYValue)
? yAxisBucketSize === 0
? `value:<=${rawYValue}`
: `value:>=${rawYValue} value:<${rawYValue + yAxisBucketSize}`
: undefined;

if (defined(rawXValue) && defined(rawYValue) && props.tooltipExploreUrlArgs) {
if (defined(rawXValue) && defined(rawYValue)) {
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,
},

const filteredSelection = {
...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;
}),
};

const tracesLink = getExploreUrl(exploreUrlProps);
exploreLink = <a href={tracesLink}>{t('View related traces')}</a>;
if (makeExploreUrl) {
tracesLink = makeExploreUrl(metricsQuery ?? '', filteredSelection);
}
}

if (makeLocalQueryUpdateUrl) {
localQueryUpdateUrl = makeLocalQueryUpdateUrl(metricsQuery ?? '');
Comment thread
nikkikapadia marked this conversation as resolved.
Outdated
}

return (
Expand All @@ -221,18 +261,38 @@ export function HeatMapWidgetVisualization(props: HeatMapWidgetVisualizationProp
</span>{' '}
{formattedZValue}
</div>
{exploreLink && (
{defined(tracesLink) && (
<div>
<span className="tooltip-label tooltip-label-centered">
{exploreLink}
<a id="traces-link" data-traces-link={tracesLink} href={tracesLink}>
{t('View connected spans')}
</a>
</span>
</div>
)}
{defined(localQueryUpdateUrl) && (
<div>
<span className="tooltip-label tooltip-label-centered">
<a
id="local-query-update-link"
data-local-query-update-url={localQueryUpdateUrl}
href={localQueryUpdateUrl}
>
{t('Add to filter')}
</a>
</span>
</div>
)}
</Fragment>
);
})}
</div>
<div className="tooltip-footer tooltip-footer-centered">{formattedXValue}</div>
<div
className="tooltip-footer tooltip-footer-centered"
style={{cursor: 'default'}}
>
{formattedXValue}
</div>
<div className="tooltip-arrow" />
</Fragment>
);
Expand Down
Loading
Loading