Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
231 changes: 231 additions & 0 deletions docs/superpowers/plans/2026-05-28-spike-lines-styling.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# Spike Lines Styling Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Make Plotly spike lines visible on hover and show precise x/y values in the existing hover popup, all styled to match the app's dark theme.

**Architecture:** Fix `plot_bgcolor` so Plotly renders spike lines against a dark surface; add x/y values to `SelectedPointData` and surface them in `PopoverCard`. No new components.

**Tech Stack:** React, Plotly / react-plotly.js, TypeScript, Redux

---

## File Map

| File | Change |
| -------------------------------------------- | ----------------------------------------------------------------------- |
| `src/constants/index.ts` | `spikeColor` updated (DONE) |
| `src/components/MainPlot/index.tsx` | Fix `plot_bgcolor`; spike/hoverlabel styling (DONE except plot_bgcolor) |
| `src/state/selection/types.ts` | Add `xValue`, `yValue` to `SelectedPointData` |
| `src/containers/MainPlotContainer/index.tsx` | Extract x/y in hover handler; format and pass to PopoverCard |
| `src/components/PopoverCard/index.tsx` | Add x/y display |

---

### Task 1: Fix spike line color constant ✅ DONE

---

### Task 2: Wire up spike color constant and add hover label styling in MainPlot ✅ DONE

---

### Task 3: Fix `plot_bgcolor` to eliminate white spike background

**Files:**
- Modify: `src/components/MainPlot/index.tsx`

Plotly renders a white mask behind spike lines when `plot_bgcolor` is transparent. Fix by setting it to `PALETTE.backgroundColor` (`#000`). The page background behind the plot is already this color, so visually nothing changes — Plotly just gets a dark surface to render against.

`PALETTE` is already imported in this file (from Task 2).

- [ ] **Step 1: Change `plot_bgcolor` in the layout `useMemo`**

Find the layout return object inside the `useMemo` and change:

- [ ] **Step 2: Run TypeScript check**

```bash
npx tsc --noEmit
```

Expected: same two pre-existing errors only.

- [ ] **Step 3: Commit**

```bash
git add src/components/MainPlot/index.tsx
git commit -m "fix: use dark plot background to prevent white spike line halo"
```

---

### Task 4: Add x/y values to hover popup

**Files:**
- Modify: `src/state/selection/types.ts`
- Modify: `src/containers/MainPlotContainer/index.tsx`
- Modify: `src/components/PopoverCard/index.tsx`

Three changes to show the hovered point's x and y values in the existing `PopoverCard`.

- [ ] **Step 1: Add `xValue` and `yValue` to `SelectedPointData` in `src/state/selection/types.ts`**

```ts
export interface SelectedPointData {
[CELL_ID_KEY]: string;
index: number;
thumbnailPath: string;
srcPath: string;
groupBy?: string;
xValue?: number | string;
yValue?: number | string;
}
```

- [ ] **Step 2: Extract x/y in `onPointHovered` in `src/containers/MainPlotContainer/index.tsx`**

In the `onPointHovered` method, add `xValue` and `yValue` to the `changeHoveredPoint` call:

```ts
changeHoveredPoint({
[CELL_ID_KEY]: point.id,
index: point.customdata.index,
thumbnailPath: point.customdata.thumbnailPath,
srcPath: point.customdata.srcPath,
xValue: point.x,
yValue: point.y,
});
```

- [ ] **Step 3: Format and pass x/y values in `renderPopover` in `src/containers/MainPlotContainer/index.tsx`**

Add a helper at the top of `renderPopover` to format a value:

```ts
const formatAxisValue = (value: number | string | undefined): string => {
if (value === undefined) return "";
if (typeof value === "string") return value;
return Number(value).toPrecision(4);
};
```

Then pass x/y to `PopoverCard`:

```ts
return (
hoveredPointData &&
galleryCollapsed && (
<PopoverCard
title={hoveredPointData[GROUP_BY_KEY] || ""}
description={hoveredPointData[CELL_ID_KEY].toString()}
src={thumbnailSrc}
xLabel={xDropDownValue}
xValue={formatAxisValue(hoveredPointData.xValue)}
yLabel={yDropDownValue}
yValue={formatAxisValue(hoveredPointData.yValue)}
/>
)
);
```

Note: `xDropDownValue` and `yDropDownValue` are already in the component's props (destructured from `this.props` in the `render` method). Destructure them in `renderPopover` as well:

```ts
public renderPopover() {
const { hoveredPointData, galleryCollapsed, thumbnailRoot, xDropDownValue, yDropDownValue } = this.props;
// ... rest of method
```

- [ ] **Step 4: Add x/y props and display to `PopoverCard` in `src/components/PopoverCard/index.tsx`**

Update the interface:

```ts
export interface PopoverCardProps {
description: string;
title: string;
src?: string;
xLabel?: string;
xValue?: string;
yLabel?: string;
yValue?: string;
}
```

Add the x/y display below `<Meta>` when values are present:

```tsx
return (
<Card className={styles.container} cover={cover} variant="borderless">
<Meta description={props.description} title={props.title} />
{(props.xValue || props.yValue) && (
<div className={styles.axisValues}>
{props.xValue && (
<div className={styles.axisRow}>
<span className={styles.axisLabel}>{props.xLabel}</span>
<span className={styles.axisValue}>{props.xValue}</span>
</div>
)}
{props.yValue && (
<div className={styles.axisRow}>
<span className={styles.axisLabel}>{props.yLabel}</span>
<span className={styles.axisValue}>{props.yValue}</span>
</div>
)}
</div>
)}
</Card>
);
```

- [ ] **Step 5: Add CSS for x/y display in `src/components/PopoverCard/style.css`**

Inside the `#thumbnail-popover` block, add:

```css
.axisValues {
padding: 4px 2px 2px;
display: flex;
flex-direction: column;
gap: 2px;
}

.axisRow {
display: flex;
justify-content: space-between;
gap: 6px;
font-size: 9px;
line-height: 1.3;
color: var(--text-gray);
overflow: hidden;
}

.axisLabel {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 54px;
}

.axisValue {
white-space: nowrap;
color: var(--off-white);
flex-shrink: 0;
}
```

- [ ] **Step 6: Run TypeScript check**

```bash
npx tsc --noEmit
```

Expected: same two pre-existing errors only.

- [ ] **Step 7: Commit**

```bash
git add src/state/selection/types.ts src/containers/MainPlotContainer/index.tsx src/components/PopoverCard/index.tsx src/components/PopoverCard/style.css
git commit -m "feat: show x/y axis values in hover popup"
```
106 changes: 106 additions & 0 deletions docs/superpowers/specs/2026-05-28-spike-lines-styling-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Spike Lines Styling Design

**Date:** 2026-05-28 (revised after visual verification)
**Branch:** copilot/display-x-and-y-values

## Problem

When hovering over a data point on the scatter plot, it is hard to read the precise x and y values. Plotly spike lines were added to address this, but two issues were discovered during visual verification:

1. **White background on spike lines**: Plotly renders a white mask/halo behind spike lines when `plot_bgcolor` is transparent (`rgba(0,0,0,0)`). This makes the dotted lines look like a solid white band on the dark background.
2. **No axis value labels**: Plotly only shows value labels at the spike/axis intersection in `hovermode: "x"`, `"y"`, `"x unified"`, or `"y unified"`. This app uses `hovermode: "closest"`, so those labels never appear.

## Scope

- Only activates when hovering over a data point (not on free cursor movement)
- Affects the main scatter plot axes only (`xaxis`, `yaxis` in `makeAxis()`)
- The histogram sub-axes (`histogramAxis`) are unaffected

## Design

### 1. Fix spike line white background (`MainPlot/index.tsx`)

Change `plot_bgcolor` to black.

### 2. Show x/y values in the hover popup

Since Plotly's axis spike labels don't appear in `hovermode: "closest"`, add the x/y values to the existing `PopoverCard` hover popup instead.

#### `src/state/selection/types.ts`

Add optional fields to `SelectedPointData`:

```ts
export interface SelectedPointData {
[CELL_ID_KEY]: string;
index: number;
thumbnailPath: string;
srcPath: string;
groupBy?: string;
xValue?: number;
yValue?: number;
}
```

#### `src/containers/MainPlotContainer/index.tsx`

In `onPointHovered`, extract `point.x` and `point.y` from the Plotly event and include them in the `changeHoveredPoint` dispatch:

```ts
changeHoveredPoint({
[CELL_ID_KEY]: point.id,
index: point.customdata.index,
thumbnailPath: point.customdata.thumbnailPath,
srcPath: point.customdata.srcPath,
xValue: point.x as number,
yValue: point.y as number,
});
```

In `renderPopover`, format the values and pass them to `PopoverCard` with axis feature name labels:

- For numerical axes: format to 4 significant figures using `Number(value).toPrecision(4)`
- For categorical axes: look up the display label from `xTickConversion`/`yTickConversion`
- Labels come from `xDropDownValue` / `yDropDownValue` (already in props)

#### `src/components/PopoverCard/index.tsx`

Add optional x/y props and display them below the existing cell info:

```ts
export interface PopoverCardProps {
description: string;
title: string;
src?: string;
xLabel?: string;
xValue?: string;
yLabel?: string;
yValue?: string;
}
```

Display as a small table or label/value pairs beneath the `Meta` section when values are present.

### 3. Spike line color (already done)

`GENERAL_PLOT_SETTINGS.spikeColor` was updated to `"rgb(0, 0, 0)"` in Task 1. This remains correct.
Comment on lines +84 to +86

### 4. Hover label styling (already done)

`hoverlabel` was added to the layout in Task 2 using `PALETTE.darkGray`, `PALETTE.headerGray`, and `GENERAL_PLOT_SETTINGS.textColor`. This styles the Plotly point tooltip and remains correct.

## Files Changed

| File | Change |
| -------------------------------------------- | ------------------------------------------------------------- |
| `src/constants/index.ts` | `spikeColor` updated (done) |
| `src/components/MainPlot/index.tsx` | Fix `plot_bgcolor`; spike/hoverlabel styling (partially done) |
| `src/state/selection/types.ts` | Add `xValue`, `yValue` to `SelectedPointData` |
| `src/containers/MainPlotContainer/index.tsx` | Extract x/y in hover handler; format and pass to PopoverCard |
| `src/components/PopoverCard/index.tsx` | Add x/y display |

## Non-goals

- Showing spike lines when hovering empty plot space (only on data points)
- Axis value labels drawn on the axis line (not achievable with `hovermode: "closest"`)
- Custom `hovertemplate` text on Plotly's trace tooltip
5 changes: 5 additions & 0 deletions src/components/MainPlot/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ const MainPlot: React.FC<MainPlotProps> = (props) => {
hoverformat: ".1f",
linecolor: GENERAL_PLOT_SETTINGS.textColor,
showgrid: false,
showspikes: true,
spikecolor: GENERAL_PLOT_SETTINGS.spikeColor,
spikethickness: 2,
spikedash: "dot",
spikemode: "toaxis+marker" as const,
tickcolor: GENERAL_PLOT_SETTINGS.textColor,
tickmode: type,
ticktext: tickConversion.tickText,
Expand Down
20 changes: 20 additions & 0 deletions src/components/PopoverCard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export interface PopoverCardProps {
description: string;
title: string;
src?: string;
xLabel?: string;
xValue?: string;
yLabel?: string;
yValue?: string;
}

const PopoverCard: React.FC<PopoverCardProps> = (props) => {
Expand Down Expand Up @@ -42,6 +46,22 @@ const PopoverCard: React.FC<PopoverCardProps> = (props) => {
return (
<Card className={styles.container} cover={cover} variant="borderless">
<Meta description={props.description} title={props.title} />
{(props.xValue || props.yValue) && (
<div className={styles.axisValues}>
{props.xValue && (
<div className={styles.axisRow}>
<span className={styles.axisLabel}>{props.xLabel}</span>
<span className={styles.axisValue}>{props.xValue}</span>
</div>
)}
{props.yValue && (
<div className={styles.axisRow}>
<span className={styles.axisLabel}>{props.yLabel}</span>
<span className={styles.axisValue}>{props.yValue}</span>
</div>
)}
</div>
)}
</Card>
);
};
Expand Down
Loading
Loading