ModsCodeMERGEBLE LLC Search Claude Code Mods

ModsCode / Gallery / Designs / Slope chart

Slope chart

Before and after for each item on two columns: green lines rose, red lines fell, and close labels on each side are pushed apart.

Slope chartSlope chart
Compare · 380×240 · sample data
hooks/designs/slope.mjs
// slope: prompt-cache hit rate for eight repos, last week against this week, in candle's look. Each repo
// is a 1px line at candle's 80% wick weight between two r=2 dots on one shared scale from 38% to 86%,
// UP when it rose and DOWN when it fell, so the direction reads before any number. Both columns carry
// a name and a value; where two values sit closer than 11px the labels on that side are pushed apart as
// a group around their mean (a plain cluster dodge), and a 35% hairline ties a moved label to its dot.
// Before / after in %: api-server 62 / 81, web-app 55 / 74, cli 71 / 66, infra 40 / 58, billing 66 / 50,
// docs 80 / 84, auth 47 / 52, search 57 / 45. The rise and fall counts, and the repo count in the alt
// text, are counted from the rows, so editing a row cannot leave a stale number behind.
// Sample data only.
const W = 380
const H = 240
const STAGE = '#FAF9F5'
const INK = '#141413'
const MUTED = '#73726C'
const UP = '#2C7A39'
const DOWN = '#AB2B3F'
const FONT = "'Inter Tight','Segoe UI',system-ui,sans-serif"
const XL = 120 // the BEFORE column; name and value sit to its left
const XR = 260 // the AFTER column; value and name sit to its right
const PLOT = { top: 40, bottom: 206 }
const LO = 38
const HI = 86
const GAP = 11 // the least distance between two label baselines on one side
const f = n => n.toFixed(1)

const REPOS = [
  ['api-server', 62, 81],
  ['web-app', 55, 74],
  ['cli', 71, 66],
  ['infra', 40, 58],
  ['billing', 66, 50],
  ['docs', 80, 84],
  ['auth', 47, 52],
  ['search', 57, 45],
]
const ROSE = REPOS.filter(r => r[2] > r[1]).length
const FELL = REPOS.filter(r => r[2] < r[1]).length

const text = (x, y, s, { size = 9, fill = MUTED, anchor = 'start' } = {}) =>
  `<text x="${f(x)}" y="${f(y)}"${anchor === 'start' ? '' : ` text-anchor="${anchor}"`} font-size="${size}" font-family="${FONT}" fill="${fill}">${s}</text>`
const y = v => PLOT.bottom - ((v - LO) / (HI - LO)) * (PLOT.bottom - PLOT.top)

// Labels that would sit closer than GAP join a group, centred on the mean of where they want to be;
// groups that then touch merge, until none do. Returns each label's y in the input order.
function dodge(want) {
  let groups = want.map((w, i) => ({ ids: [i], top: w })).sort((a, b) => a.top - b.top)
  for (let merged = true; merged;) {
    merged = false
    for (let i = 1; i < groups.length; i++) {
      const a = groups[i - 1]
      const b = groups[i]
      if (a.top + a.ids.length * GAP <= b.top) continue
      const ids = [...a.ids, ...b.ids].sort((p, q) => want[p] - want[q])
      const mean = ids.reduce((s, id) => s + want[id], 0) / ids.length
      groups.splice(i - 1, 2, { ids, top: mean - ((ids.length - 1) * GAP) / 2 })
      merged = true
      break
    }
  }
  const out = []
  for (const g of groups) g.ids.forEach((id, k) => (out[id] = g.top + k * GAP))
  return out
}

function draw() {
  const parts = [`<rect width="${W}" height="${H}" rx="4" fill="${STAGE}"/>`]
  for (const cx of [XL, XR]) {
    parts.push(`<line x1="${cx + 0.5}" x2="${cx + 0.5}" y1="${PLOT.top - 10}" y2="${PLOT.bottom + 10}" stroke="${INK}" stroke-opacity="0.12" stroke-width="1"/>`)
  }
  parts.push(text(XL + 0.5, 20, 'BEFORE', { anchor: 'middle' }))
  parts.push(text(XR + 0.5, 20, 'AFTER', { anchor: 'middle' }))

  const before = REPOS.map(r => y(r[1]))
  const after = REPOS.map(r => y(r[2]))
  const lb = dodge(before)
  const la = dodge(after)
  REPOS.forEach(([name, b, a], i) => {
    const color = a > b ? UP : a < b ? DOWN : INK
    const x1 = XL + 0.5
    const x2 = XR + 0.5
    parts.push(`<line x1="${f(x1)}" x2="${f(x2)}" y1="${f(before[i])}" y2="${f(after[i])}" stroke="${color}" stroke-opacity="0.8" stroke-width="1"/>`)
    parts.push(`<circle cx="${f(x1)}" cy="${f(before[i])}" r="2" fill="${color}"/>`)
    parts.push(`<circle cx="${f(x2)}" cy="${f(after[i])}" r="2" fill="${color}"/>`)
    // A label pushed more than 2px from its dot gets a hairline back to it
    if (Math.abs(lb[i] - before[i]) > 2) parts.push(`<line x1="${f(x1 - 3)}" x2="${f(x1 - 7)}" y1="${f(before[i])}" y2="${f(lb[i])}" stroke="${INK}" stroke-opacity="0.35" stroke-width="1"/>`)
    if (Math.abs(la[i] - after[i]) > 2) parts.push(`<line x1="${f(x2 + 3)}" x2="${f(x2 + 7)}" y1="${f(after[i])}" y2="${f(la[i])}" stroke="${INK}" stroke-opacity="0.35" stroke-width="1"/>`)
    parts.push(text(XL - 9, lb[i] + 4, `${b}%`, { size: 11, fill: INK, anchor: 'end' }))
    parts.push(text(XL - 38, lb[i] + 3.5, name, { size: 10, fill: INK, anchor: 'end' }))
    parts.push(text(XR + 10, la[i] + 4, `${a}%`, { size: 11, fill: color }))
    parts.push(text(XR + 38, la[i] + 3.5, name, { size: 10, fill: INK }))
  })

  const fellLabel = `${FELL} DOWN`
  parts.push(text(W - 12, H - 8, fellLabel, { fill: DOWN, anchor: 'end' }))
  parts.push(text(W - 12 - fellLabel.length * 5.5 - 10, H - 8, `${ROSE} UP`, { fill: UP, anchor: 'end' }))
  parts.push(text(12, H - 8, 'SAMPLE CACHE HIT RATE BY REPO'))
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${H}" width="${W}" height="${H}">${parts.join('')}</svg>`
}

let source // drawn on first use and kept: the pane loads every design, so none pays at load time
export default {
  id: 'slope',
  title: 'Slope chart',
  category: 'compare',
  description: 'Before and after for each item on two columns: green lines rose, red lines fell, and close labels on each side are pushed apart.',
  element: ui => ui.Svg({ source: (source ??= draw()), alt: `Slope chart of prompt-cache hit rate for ${REPOS.length} repos, last week against this week, ${ROSE} rising and ${FELL} falling, sample data`, width: W, height: H }),
}

To use it in a mod: copy the file to your mod's hooks/designs/ and return its element(ui) from a pane's ui.render. How a pane is built.

Code: PolyForm Noncommercial License 1.0.0: noncommercial use only. Required Notice: Copyright 2026 Haruki Nakada (https://modscode.com).