diff --git a/CHANGELOG.md b/CHANGELOG.md
index 00fa2fb9..5c443468 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- ๐งญ **Inspector**: select anything โ a timeline frame, a call tree or analysis row, a SOQL/DML/SOSL statement โ and inspect it without leaving the tab you're on. ([#113])
- **A selection** shows its details and governor metrics as `used / limit`, the call stack that led to it, and its own subtree in **Time Order**, **Aggregated** or **Bottom-Up**. Click a frame in the call stack to walk up it โ the details and subtree follow, and the stack stays anchored to what you selected. On the Timeline it also splits the self time under the selection by the namespace whose code ran it.
- **Nothing selected** shows the whole log instead of an empty panel: a governor overview on every tab, time by category, self time by namespace and governor trends on the Timeline, log-wide findings and how per-call self time spreads on Analysis, the hot path and hot spots on the Call Tree, and, on Database, which namespaces asked for and burned the database time, every call path that ends in a query, DML or search with total and self time, and how few statements hold the time. ([#373])
- - Every row is a link: click it to reveal the frame, row or statement behind it in the tab you're on. Hover works both ways without moving the view โ hover a row to pick out what it names in the tab you're on, or hover there to mark the rows that name it, and what you click stays picked out until `Escape`. Right-click for copy actions.
+ - Every row is a link: click it to reveal the frame, row or statement behind it in the tab you're on. Hover works both ways without moving the view โ hover a row to pick out what it names in the tab you're on, or hover there to mark the rows that name it, and what you click stays picked out until `Escape`. Click a point on a governor usage chart to move the Timeline to that instant and zoom in on it. Right-click for copy actions.
- **Findings** list the statements behind them, most repeated first with how often each ran, and report one query built per record and run a row at a time. The severities head the list and filter it, any number at once, a finding the log times shows how long it took and what that is of the log, and selecting an Analysis row narrows the list to the findings that name that method or anything it called.
- **Detail | Summary** switches between what you picked and the tab's summary of the whole log, keeping the selection to come back to.
- Dock it left, right or bottom, drag to resize any section โ double-click a divider to restore the defaults โ and collapse the sections you don't need; the layout is remembered. `Escape` clears the selection and returns the whole-log view. ([#63])
diff --git a/lana-docs/docs/docs/features/inspector.md b/lana-docs/docs/docs/features/inspector.md
index 64d990f4..c7ea5ac4 100644
--- a/lana-docs/docs/docs/features/inspector.md
+++ b/lana-docs/docs/docs/features/inspector.md
@@ -39,7 +39,7 @@ Press **Summary** in the panel's header to read the whole log without giving up
With nothing selected the inspector reads the whole log. Every tab opens with an **Overview** โ the six governor metrics closest to their limit โ then adds what its own tab can answer at log scope:
-- **Timeline** โ time by category, **self time by namespace**, governor usage over time, and the whole-log call tree.
+- **Timeline** โ time by category, **self time by namespace**, governor usage over time, and the whole-log call tree. Click a point on a usage chart, or step the arrow keys across a focused chart and press `Enter`, to move the Timeline to that instant and zoom in on it. Nothing is selected, so the inspector keeps this whole-log reading.
- **Call Tree** โ the **hot path** the log spent its time in, and the **hot spots** with the most self time.
- **Database** โ **Namespace duration**: **Called from namespace** โ the namespace that issued the statement โ and, when they differ, **Ran in namespace**, the namespaces of whatever ran beneath it, such as a package trigger firing on your DML. **Call tree**: every call path that ends in a query, DML or search, with **Total Time** โ the database time at or below the row โ beside **Self Time**, the row's own code. A row with all total and no self is waiting on the database; the reverse is the Apex around it. **Database duration**: how few statements hold the time, with cost per row, how often each ran, and its duration split into self time and descendants, so a DML that is cheap in itself but fires seven seconds of triggers reads as one.
- **Analysis** โ **Findings**: what is slow or wrong in the log, and what to do about it, led by the findings by severity โ press any number of them to hold the list to those. A finding whose events the log times also shows how long they took and what that is of the log. Each finding lists the statements behind it, most repeated first; click one to reveal its row in the grid.
diff --git a/log-viewer/src/components/GovernorTrends.ts b/log-viewer/src/components/GovernorTrends.ts
index 2d7d8415..53bdd72e 100644
--- a/log-viewer/src/components/GovernorTrends.ts
+++ b/log-viewer/src/components/GovernorTrends.ts
@@ -5,6 +5,7 @@ import { consume } from '@lit/context';
import { LitElement, css, html, svg } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
+import { eventBus } from '../core/events/EventBus.js';
import { logContext } from '../core/log/logContext.js';
import type { LogStore } from '../core/log/LogStore.js';
import { formatDuration } from '../core/utility/Util.js';
@@ -13,6 +14,7 @@ import {
governorTier,
} from '../features/database/components/GovernorSummary.js';
import { apexLimitTimeSeries } from '../features/timeline/optimised/apex-limit-series.js';
+import { SEEK_LOG_SHARE } from '../features/timeline/utils/navigate-window.js';
import { globalStyles } from '../styles/global.styles.js';
import { inspectorSectionStyles } from '../styles/inspectorSection.styles.js';
import {
@@ -27,6 +29,11 @@ import { NO_CUMULATIVE_LIMITS_TEXT } from './logOverviewMetrics.js';
const VIEW_W = 100;
const VIEW_H = 30;
+/** How far one arrow key moves the cursor: the seek window's own share of the
+ * log, so successive steps sweep the log without leaving a gap between the
+ * windows they can reach. */
+const KEY_STEP = SEEK_LOG_SHARE;
+
/** The pieces of a chart that depend only on the data, never the hover. */
interface TrendGeometry {
line: string;
@@ -74,12 +81,18 @@ function trendGeometry(series: TrendSeries, logTotal: number): TrendGeometry {
* metrics closest to their limits. Every chart shares the same x-domain (the
* whole log) so shapes are comparable; each y-domain runs to at least 100% so
* a flat safe line reads as safe. Colours follow the gauges' tiers.
+ *
+ * A click, or Enter on a focused chart, moves the timeline to that instant and
+ * zooms in on it, so a spike leads straight to its cause. The chart selects
+ * nothing, so the inspector keeps its whole-log reading.
*/
@customElement('governor-trends')
export class GovernorTrends extends LitElement {
- /** The sample under the pointer, on the one hovered chart. */
+ /** The sample under the pointer or the arrow keys, on the one chart that holds
+ * it. `from` says which placed it: a pointer leaving takes its own cursor
+ * with it, never one the keys placed. */
@state()
- private _hover: { label: string; point: TrendPoint } | null = null;
+ private _cursor: { label: string; point: TrendPoint; from: 'pointer' | 'key' } | null = null;
/** The log on screen, from the app root. */
@consume({ context: logContext, subscribe: true })
@@ -133,11 +146,28 @@ export class GovernorTrends extends LitElement {
color: var(--lana-fg-muted);
}
+ /* A button, not the svg itself: Chromium matches :focus-visible on a
+ click for a focusable svg, so the ring appeared on every seek. */
.trend__chart {
display: block;
width: 100%;
- height: 44px;
+ border: 0;
border-bottom: 1px solid var(--lana-surface-border);
+ padding: 0;
+ background: none;
+ color: inherit;
+ cursor: pointer;
+ }
+
+ .trend__plot {
+ display: block;
+ width: 100%;
+ height: 44px;
+ }
+
+ .trend__chart:focus-visible {
+ outline: var(--lana-stroke) solid var(--lana-focus-border);
+ outline-offset: calc(-1 * var(--lana-stroke));
}
.trend--safe {
@@ -172,7 +202,7 @@ export class GovernorTrends extends LitElement {
/* A vertical line, not a circle โ preserveAspectRatio="none" would
distort any shape with area. */
- .trend__hover {
+ .trend__cursor {
stroke: currentColor;
stroke-width: 1;
vector-effect: non-scaling-stroke;
@@ -198,47 +228,119 @@ export class GovernorTrends extends LitElement {
private _renderTrend(series: TrendSeries, logTotal: number) {
const { line, area, guideY, x } = trendGeometry(series, logTotal);
- const hovered = this._hover?.label === series.label ? this._hover.point : null;
- const hoverX = hovered ? x(hovered.t).toFixed(2) : null;
+ const cursor = this._cursorFor(series);
+ const cursorX = cursor ? x(cursor.t).toFixed(2) : null;
return html`