Slope Chart
render_slope_chartShow how rankings change between two time periods: slope charts with lines connecting start and end positions to reveal shifts.
Instructions
Render a slope chart - 'How did rankings change?' SVG lines connecting two time periods showing relative position changes.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| title | Yes | Chart title | |
| periodStart | Yes | Label for start period (e.g. '2024') | |
| periodEnd | Yes | Label for end period (e.g. '2025') | |
| data | Yes | Array of {label, start, end} items | |
| theme | No | Theme preset: boardroom, corporate, sales-floor, golden-treasury, clinical, startup, ops-control, tokyo-midnight, zen-garden, consultant, black-tron, black-elegance, black-matrix, forest-amber, forest-earth, sky-light, sky-ocean, sky-twilight, gray-hf, gray-copilot | |
| palette | No | Override palette only (mix-and-match) | |
| typography | No | Override typography: professional, luxury, cyberpunk, editorial, mono, bold, system, techno | |
| effects | No | Override effects: none, subtle, shimmer, neon, energetic |
Implementation Reference
- src/charts/slope.ts:23-95 (handler)The renderSlopeChart function that implements the 'render_slope_chart' tool. It renders a slope chart (inline SVG with lines connecting start/end values per data item) into the provided container element.
export function renderSlopeChart(container: HTMLElement, payload: SlopeData): void { const theme = resolveTheme(payload.theme, { palette: payload.palette, typography: payload.typography, effects: payload.effects, }); if (theme) applyTheme(container, theme); const shimmer = theme?.effects.shimmerTitle ? " shimmer-text" : ""; const colors = resolveColors(undefined, payload.data.length); const allVals = payload.data.flatMap((d) => [d.start, d.end]); const minVal = Math.min(...allVals); const maxVal = Math.max(...allVals); const range = maxVal - minVal || 1; const svgW = 460; const svgH = Math.max(120, payload.data.length * 28 + 20); const padX = 90; const padY = 10; const lineX1 = padX; const lineX2 = svgW - padX; function yPos(val: number): number { return padY + ((maxVal - val) / range) * (svgH - 2 * padY); } const groups = payload.data.map((item, i) => { const color = item.color || colors[i % colors.length]; const y1 = yPos(item.start); const y2 = yPos(item.end); return ` <g class="slope__group" data-idx="${i}" style="cursor:pointer"> <line class="slope__line" x1="${lineX1}" y1="${y1}" x2="${lineX2}" y2="${y2}" stroke="${color}" /> <circle class="slope__dot" cx="${lineX1}" cy="${y1}" fill="${color}" /> <circle class="slope__dot" cx="${lineX2}" cy="${y2}" fill="${color}" /> <text class="slope__label-left" x="${lineX1 - 6}" y="${y1 + 4}">${escapeHtml(item.label)} ${item.start}</text> <text class="slope__label-right" x="${lineX2 + 6}" y="${y2 + 4}">${item.end} ${escapeHtml(item.label)}</text> </g> `; }).join(""); container.className = "chart-view"; container.innerHTML = ` <div class="card chart-card"> <div class="chart-card__header"> <div><div class="chart-card__title${shimmer}">${escapeHtml(payload.title)}</div></div> </div> <div class="chart-card__body chart-card__body--css"> <div class="slope"> <div class="slope__labels"> <span>${escapeHtml(payload.periodStart)}</span> <span>${escapeHtml(payload.periodEnd)}</span> </div> <svg class="slope__svg" viewBox="0 0 ${svgW} ${svgH}" preserveAspectRatio="xMidYMid meet"> ${groups} </svg> </div> </div> </div> `; container.querySelectorAll<SVGGElement>(".slope__group").forEach((el) => { el.addEventListener("click", () => { const idx = parseInt(el.dataset.idx ?? "0", 10); const item = payload.data[idx]; sendClickMessage(`[Slope] "${payload.title}" - ${item.label}: ${item.start} -> ${item.end}`); }); }); const card = container.querySelector<HTMLElement>(".chart-card")!; addHtmlExportButton(card, payload.title); addRefreshButton(card, () => (window as any).__mcpRefresh?.()); } - src/charts/slope.ts:4-21 (schema)Type definitions (SlopeItem and SlopeData) that define the input schema for the slope chart tool.
interface SlopeItem { label: string; start: number; end: number; color?: string; } interface SlopeData { type: "slope"; title: string; periodStart: string; periodEnd: string; data: SlopeItem[]; theme?: string; palette?: string; typography?: string; effects?: string; } - src/charts/slope.ts:97-97 (registration)Registers the slope chart type 'slope' with tool name 'render_slope_chart' pointing to the renderSlopeChart handler via registerChart().
registerChart("slope", "render_slope_chart", renderSlopeChart); - src/charts/shared.ts:188-206 (registration)The registerChart function definition in shared.ts which stores chart type → (toolName, render) mappings in the CHART_REGISTRY.
export function registerChart( type: string, toolName: string, render: (root: HTMLElement, data: any) => void, ): void { CHART_REGISTRY[type] = { toolName, render }; } export function getChartEntry(type: string): ChartEntry | undefined { return CHART_REGISTRY[type]; } export function getTypeToToolMap(): Record<string, string> { const map: Record<string, string> = {}; for (const [type, entry] of Object.entries(CHART_REGISTRY)) { map[type] = entry.toolName; } return map; } - src/themes.ts:945-973 (helper)The resolveTheme helper used by renderSlopeChart to resolve palette, typography, and effects from a theme name or overrides.
export function resolveTheme( name?: string, overrides?: { palette?: string; typography?: string; effects?: string }, ): ThemePreset | null { if (!name && !overrides?.palette && !overrides?.typography && !overrides?.effects) { return null; } const base = name ? THEME_PRESETS[name] : null; const palette = overrides?.palette ? PALETTES[overrides.palette] : base?.palette ?? null; const typography = overrides?.typography ? TYPOGRAPHY_SETS[overrides.typography] : base?.typography ?? null; const effects = overrides?.effects ? EFFECT_PRESETS[overrides.effects] : base?.effects ?? null; if (!palette) return null; return { name: name ?? "custom", palette, typography: typography ?? TYPOGRAPHY_SETS.system, effects: effects ?? EFFECT_PRESETS.none, }; }