Skip to content

feat(service-map): dotted canvas, metric color toggle, and popover polish#2601

Draft
elizabetdev wants to merge 3 commits into
mainfrom
elizabet/polish-service-map-page
Draft

feat(service-map): dotted canvas, metric color toggle, and popover polish#2601
elizabetdev wants to merge 3 commits into
mainfrom
elizabet/polish-service-map-page

Conversation

@elizabetdev

Copy link
Copy Markdown
Contributor

Summary

Polishes the Service Map page (@xyflow/react graph) with a set of related UX improvements:

  • Dotted canvas background via xyflow's native <Background variant={Dots} />, so the dots pan/zoom with the graph (the standard infinite-canvas feel) rather than a static CSS backdrop.
  • Metric color toggle — a Latency / Error rate / Throughput segmented control that recolors nodes by the selected dimension. The active metric + graph-wide maxes flow through a small React context (ServiceMapMetricContext) so switching metrics recolors instantly without rebuilding nodes or re-running the dagre layout (preserving zoom/positions).
  • Legend explaining the color scale (with the real max value formatted per metric) and documenting that node size encodes throughput. The legend swatch and node fills are generated from the same ramp endpoints (getMetricGradientCss), so they can never drift.
  • Sequential color ramp — node color now goes light→dark within each metric's hue (red/amber/blue) instead of the previous grey→color saturation ramp.
  • Light/dark support — the canvas colorMode and zoom controls now follow the app's resolved color scheme instead of being hardcoded to dark.
  • Popover polish — the node tooltip is now a raised surface (lighter than the canvas, with elevation) carrying a service-name header, grouped sections (identity → stats → action), an "incoming requests" label, and severity-aware error coloring (amber < 5%, red ≥ 5%).

Why

The service map lacked the spatial affordances and single-metric focus common to node-graph tools (Splunk APM, Datadog, ARMS), the node coloring only conveyed error rate and read as washed-out (grey→color), and the popover blended into the canvas with no title or hierarchy.

Test plan

  • yarn ci:unitutils.test.ts covers the sequential ramp (light→dark, monotonic, capping, per-metric hue), getServiceMetricValue, and getMetricGradientCss (54 tests, 100% coverage of utils.ts).
  • Visual check on /service-map in light and dark mode:
    • Dots render subtly and pan/zoom with the graph.
    • Toggle recolors nodes; Latency disabled when the source has no duration data.
    • Legend max value + gradient match the active metric.
    • Node popover reads as a raised card with a service-name header.

Notes

  • Includes a changeset (@hyperdx/app patch).

Made with Cursor

…lish

Add a dotted background to the service map canvas and a Latency / Error rate /
Throughput segmented control that recolors nodes by the selected metric, with a
legend (sequential light-to-dark ramp, shared by node fills and the legend
swatch) that also documents node size = throughput. The canvas and its controls
now follow the app's light/dark color scheme instead of being hardcoded to dark.

Polish the node popover into a raised surface with a service-name header,
grouped sections, and severity-aware error coloring.

Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8ab8a38

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/app Patch
@hyperdx/api Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jul 7, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Jul 8, 2026 11:35am
hyperdx-storybook Ready Ready Preview, Comment Jul 8, 2026 11:35am

Request Review

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR polishes the Service Map page with a dotted canvas background, a metric color toggle (Latency / Error rate / Throughput) backed by a React context so metric switching recolors nodes instantly without re-running the dagre layout, a legend that shares ramp endpoints with node fills, light/dark color-scheme support, and an overhauled node popover with a service-name header and severity-aware error coloring.

  • Color ramp: nodes now use a sequential light→dark HSL ramp per metric hue (red/amber/blue) via shared rampFill helpers; legend and nodes are derived from the same endpoints so they can never drift.
  • Context architecture: ServiceMapMetricContext carries metric and metricMax to node components, keeping metric-switch re-renders cheap and layout-stable.
  • Tooltip / UX: the popover gains a service-name header, dividers, and a "Focus / Clear focus" CTA whose toggle semantics are correctly handled in DBServiceMapPage.onFocusService.

Confidence Score: 5/5

Safe to merge — purely additive UX changes to the service map with no data-layer or API impact.

All changed files are scoped to the ServiceMap component subtree. The new metric context, color ramp, and legend are well-tested (54 unit tests with property-based ramp assertions). The 'Clear focus' toggle is correctly wired: the parent's onFocusService in DBServiceMapPage already implements the toggle-to-clear pattern. No data mutations, auth logic, or shared infrastructure is touched.

No files require special attention.

Important Files Changed

Filename Overview
packages/app/src/components/ServiceMap/utils.ts Adds ServiceMapMetric type, per-metric hue constants, sequential ramp helpers (rampFill, lerp, clamp01), and refactored getNodeColors. Logic is clean; ramp endpoints, border step, and color math are well-tested.
packages/app/src/components/ServiceMap/ServiceMap.tsx Adds metric toggle state, metricMax memo, ServiceMapMetricContext provider, Background/Panel elements, and focusedService derivation. Context-based coloring correctly avoids triggering node rebuild on metric switch.
packages/app/src/components/ServiceMap/ServiceMapTooltip.tsx Adds service-name header, dividers, isFocused toggle CTA ('Focus'/'Clear focus'), and severity-aware error coloring. 'Clear focus' callback correctness confirmed — the parent (DBServiceMapPage) implements toggle semantics.
packages/app/src/components/ServiceMap/ServiceMapNode.tsx Reads metric/metricMax from context and calls getNodeColors with them instead of baking color into node data; adds isFocused and focusedService prop plumbing.
packages/app/src/components/ServiceMap/ServiceMapLegend.tsx New component: renders gradient swatch and max-value label for the active metric. formatMax correctly delegates to the same util functions used by the tooltip, keeping legend and tooltip in sync.
packages/app/src/components/ServiceMap/ServiceMapMetricContext.ts Minimal context file: exports ServiceMapMetricMax type and ServiceMapMetricContext with safe defaults, enabling metric-driven recoloring without rebuilding node objects.
packages/app/src/components/ServiceMap/ServiceMap.module.scss Adds .panel and .legendGradient classes, updates .toolbar shadow/border for the raised-surface look. Uses CSS custom properties for both colors, so light/dark switching works automatically.
packages/app/src/components/ServiceMap/tests/utils.test.ts Updated tests use property-based assertions via parseHsl instead of hardcoding tuned color values; adds coverage for sequential ramp monotonicity, metric hues, getServiceMetricValue, and getMetricGradientCss.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[ServiceMap outer component] -->|derives focusedService\nfrom serviceNames| B[ServiceMapPresentation]
    B -->|useState: metric| C[SegmentedControl]
    B -->|useMemo: metricMax| D[ServiceMapMetricContext.Provider]
    D --> E[ReactFlow]
    E --> F[ServiceMapNode]
    F -->|useContext| D
    F -->|getNodeColors\ngetServiceMetricValue| G[utils.ts\nrampFill / getNodeColors]
    F --> H[ServiceMapTooltip\nisFocused / onFocus]
    B --> I[ServiceMapLegend\ngetMetricGradientCss]
    I -->|same ramp endpoints| G
    C -->|setMetric| B
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[ServiceMap outer component] -->|derives focusedService\nfrom serviceNames| B[ServiceMapPresentation]
    B -->|useState: metric| C[SegmentedControl]
    B -->|useMemo: metricMax| D[ServiceMapMetricContext.Provider]
    D --> E[ReactFlow]
    E --> F[ServiceMapNode]
    F -->|useContext| D
    F -->|getNodeColors\ngetServiceMetricValue| G[utils.ts\nrampFill / getNodeColors]
    F --> H[ServiceMapTooltip\nisFocused / onFocus]
    B --> I[ServiceMapLegend\ngetMetricGradientCss]
    I -->|same ramp endpoints| G
    C -->|setMetric| B
Loading

Reviews (3): Last reviewed commit: "feat(service-map): state-aware focus CTA..." | Re-trigger Greptile

Comment on lines +153 to +155
// Latency coloring is only meaningful when the source exposes duration data;
// otherwise every node reports 0 and the option is disabled.
const hasLatencyData = metricMax.latency > 0;

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.

P2 When hasLatencyData flips to false (e.g. the data window is changed and the refreshed dataset has no duration data), the metric state stays 'latency'. The toggle correctly disables the option, but all nodes remain colored on the latency ramp with metricMax.latency === 0, giving every node the same minimum-intensity fill until the user manually switches. A short effect that resets metric whenever the active metric loses its data prevents the stuck-state.

Suggested change
// Latency coloring is only meaningful when the source exposes duration data;
// otherwise every node reports 0 and the option is disabled.
const hasLatencyData = metricMax.latency > 0;
// Latency coloring is only meaningful when the source exposes duration data;
// otherwise every node reports 0 and the option is disabled.
const hasLatencyData = metricMax.latency > 0;
// If the active metric loses its data (e.g. time window changes and the new
// dataset has no latency), fall back to errorRate so nodes don't all render
// at the same zero-intensity color.
useEffect(() => {
if (metric === 'latency' && !hasLatencyData) {
setMetric('errorRate');
}
}, [metric, hasLatencyData]);

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

Comment on lines +39 to +42
case 'throughput':
return isSingleTrace
? `${formatApproximateNumber(max)} reqs`
: formatRate(getRequestsPerSecond(max, dateRange));

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.

P2 formatApproximateNumber always prepends ~ (e.g. ~5 reqs), but for single-trace maps the tooltip shows the raw integer without any ~ prefix (e.g. 5 incoming requests). Using the raw number directly here would be consistent with the tooltip.

Suggested change
case 'throughput':
return isSingleTrace
? `${formatApproximateNumber(max)} reqs`
: formatRate(getRequestsPerSecond(max, dateRange));
case 'throughput':
return isSingleTrace
? `${max} reqs`
: formatRate(getRequestsPerSecond(max, dateRange));

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Conductor Fix in Cursor Fix in Codex

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

E2E Test Results

1 test failed • 222 passed • 3 skipped • 1482s

Status Count
✅ Passed 222
❌ Failed 1
⚠️ Flaky 5
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

…over

Focus used space-between with a short label, so the target icon floated to the
far edge and read as broken. Switch to a centered light button with the icon as
a leftSection so it looks like a distinct action, separate from the drill-in
stat rows above it.

Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the vague "Focus" button with a clearer, state-aware call to action:
"Focus on this service" when unfocused and "Clear focus" when the map is already
scoped to that service, with matching target / target-off icons. Threads the
currently focused service down to each node so the tooltip reflects the toggle
state.

Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant