Repository navigation
feat(charts): name the service on release marker hover #2895
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| --- | ||
| '@hyperdx/app': minor | ||
| --- | ||
|
|
||
| Hovering a release marker now lists every release in its cluster with the | ||
| service that shipped it, its version, and the time. Colour alone could not | ||
| identify a service once a chart had more series than the legend shows, and a | ||
| collapsed "N releases" cluster named none of them. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
packages/app/src/components/charts/AnnotationHitLayer.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| import { | ||
| DefaultZIndexes, | ||
| usePlotArea, | ||
| useXAxisScale, | ||
| ZIndexLayer, | ||
| } from 'recharts'; | ||
|
|
||
| import type { PositionedAnnotation } from './chartAnnotations'; | ||
|
|
||
| /** | ||
| * Minimum width of a marker's hover target. A dashed 1px line is unhittable, so | ||
| * each cluster gets a band at least this wide centred on it. | ||
| */ | ||
| const MIN_HIT_WIDTH_PX = 14; | ||
|
|
||
| export type HoveredAnnotation = { | ||
| /** The cluster's labelled anchor. `members` names everything inside it. */ | ||
| annotation: PositionedAnnotation; | ||
| /** Pointer position in chart-container pixels. */ | ||
| point: { x: number; y: number }; | ||
| }; | ||
|
|
||
| /** | ||
| * Transparent hover targets over the annotation markers, so a marker can name | ||
| * the service it belongs to rather than relying on its colour — which stops | ||
| * being resolvable once the legend overflows past `MAX_LEGEND_ITEMS`. | ||
| * | ||
| * Rendered inside the chart through Recharts' `<Customized>` so it can read the | ||
| * real plot geometry (`usePlotArea`, `useXAxisScale`) instead of re-deriving it | ||
| * from the container width. Handlers deliberately do not stop propagation: | ||
| * events still bubble to the chart wrapper, which is what keeps drag-to-zoom | ||
| * and the series tooltip working underneath the bands. | ||
| */ | ||
| export function AnnotationHitLayer({ | ||
| annotations, | ||
| onHover, | ||
| }: { | ||
| annotations: PositionedAnnotation[]; | ||
| onHover: (hovered: HoveredAnnotation | null) => void; | ||
| }) { | ||
| const plot = usePlotArea(); | ||
| const scale = useXAxisScale(); | ||
|
|
||
| if (plot == null || scale == null) { | ||
| return null; | ||
| } | ||
|
|
||
| // One target per cluster, not per line. Anchors carry `members`; the muted | ||
| // lines inside a cluster are already covered by their anchor's band, and | ||
| // giving them their own rects would just have them intercept each other. | ||
| const anchors = annotations.filter(annotation => annotation.members != null); | ||
|
|
||
| return ( | ||
| // Above every other chart layer, otherwise the series areas (zIndex 100) | ||
| // and the marker lines (400) receive the pointer instead of these bands. | ||
| <ZIndexLayer zIndex={DefaultZIndexes.label + 1}> | ||
| <g className="recharts-annotation-hit-layer"> | ||
| {anchors.map((annotation, i) => { | ||
| const members = annotation.members ?? [annotation]; | ||
| // Span the whole cluster so hovering any of its lines — including the | ||
| // muted ones, which carry no label — surfaces the same tooltip. | ||
| const xs = members | ||
| .map(member => scale(member.x)) | ||
| .filter((px): px is number => px != null && Number.isFinite(px)); | ||
| if (xs.length === 0) { | ||
| return null; | ||
| } | ||
| const left = Math.min(...xs); | ||
| const right = Math.max(...xs); | ||
|
|
||
| const width = Math.max(right - left, MIN_HIT_WIDTH_PX); | ||
| const x = left - (width - (right - left)) / 2; | ||
|
|
||
| return ( | ||
| <rect | ||
| key={annotation.key ?? `annotation-hit-${annotation.x}-${i}`} | ||
| x={x} | ||
| // Confined to the label headroom above the plot. Covering the plot | ||
| // too would make the series tooltip fire alongside this one, and | ||
| // the label is the natural thing to aim at anyway. | ||
| y={0} | ||
| width={width} | ||
| height={plot.y} | ||
| fill="transparent" | ||
| pointerEvents="all" | ||
| style={{ cursor: 'default' }} | ||
| onMouseEnter={event => { | ||
| // Measured here rather than passed down from the chart | ||
| // container: reading the DOM in an event handler is fine, | ||
| // whereas reading a ref during render is not. | ||
| const band = event.currentTarget.getBoundingClientRect(); | ||
| onHover({ | ||
| annotation, | ||
| point: { x: band.left + band.width / 2, y: band.bottom }, | ||
| }); | ||
| }} | ||
| onMouseLeave={() => onHover(null)} | ||
| /> | ||
| ); | ||
| })} | ||
| </g> | ||
| </ZIndexLayer> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| import { createPortal } from 'react-dom'; | ||
| import { Group, Stack, Text } from '@mantine/core'; | ||
|
|
||
| import type { HoveredAnnotation } from './AnnotationHitLayer'; | ||
| import { | ||
| ChartTooltipContainer, | ||
| ChartTooltipHeader, | ||
| useChartTooltipZIndex, | ||
| } from './ChartTooltip'; | ||
|
|
||
| /** Gap below the marker so the tooltip does not sit on top of its own label. */ | ||
| const TOOLTIP_OFFSET_PX = 6; | ||
|
|
||
| /** | ||
| * Hover tooltip for a release marker: one row per release in the hovered | ||
| * cluster, each naming the service it belongs to. | ||
| * | ||
| * The service name is the whole point. On the chart the only carrier of that is | ||
| * the marker's colour, which stops being resolvable once the legend overflows | ||
| * past `MAX_LEGEND_ITEMS` and the matching entry hides behind "+N more". | ||
| * | ||
| * Portaled to the body and positioned fixed, matching the series tooltip: a | ||
| * dashboard tile clips its overflow, so an absolutely-positioned tooltip gets | ||
| * cut off at the tile edge. `hovered.point` is already in viewport pixels, | ||
| * measured from the hit band when the pointer entered it. | ||
| */ | ||
| export function AnnotationTooltip({ hovered }: { hovered: HoveredAnnotation }) { | ||
| const zIndex = useChartTooltipZIndex(); | ||
| const members = hovered.annotation.members ?? [hovered.annotation]; | ||
|
|
||
| if (typeof document === 'undefined') { | ||
| return null; | ||
| } | ||
|
|
||
| return createPortal( | ||
| <div | ||
| style={{ | ||
| position: 'fixed', | ||
| left: hovered.point.x, | ||
| top: hovered.point.y + TOOLTIP_OFFSET_PX, | ||
| transform: 'translateX(-50%)', | ||
| pointerEvents: 'none', | ||
| zIndex, | ||
| }} | ||
| > | ||
| <ChartTooltipContainer | ||
| header={<ChartTooltipHeader labelSeconds={hovered.annotation.x} />} | ||
| > | ||
| <Stack gap={4}> | ||
| {members.map((member, i) => ( | ||
| <Group | ||
| key={member.key ?? `annotation-row-${member.x}-${i}`} | ||
| gap={8} | ||
| wrap="nowrap" | ||
| > | ||
| <div | ||
| style={{ | ||
| width: 8, | ||
| height: 8, | ||
| borderRadius: 2, | ||
| flexShrink: 0, | ||
| background: member.color ?? 'var(--color-border)', | ||
| }} | ||
| /> | ||
| {member.group != null && ( | ||
| <Text size="xs" c="dimmed" style={{ whiteSpace: 'nowrap' }}> | ||
| {member.group} | ||
| </Text> | ||
| )} | ||
| <Text size="xs" fw={500} style={{ whiteSpace: 'nowrap' }}> | ||
| {member.label} | ||
| </Text> | ||
| </Group> | ||
| ))} | ||
| </Stack> | ||
| </ChartTooltipContainer> | ||
| </div>, | ||
| document.body, | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.