Skip to main content
Glama

Dumbbell Chart

render_dumbbell_chart
Read-onlyIdempotent

Visualize before-and-after comparisons using dumbbell charts with connecting bars, scale labels, and background zones for absolute positioning context.

Instructions

Render a dumbbell chart - 'How big is the gap?' Before/after dots connected by a bar. Supports scale labels and background zones for absolute positioning context.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
titleYesChart title
dataYesArray of {label, before, after} items
beforeLabelNoLabel for 'before' column (default: Before)
afterLabelNoLabel for 'after' column (default: After)
unitNoUnit suffix
scaleLabelsNoLabels at scale positions, e.g. {'40': 'Engineer', '65': 'Sr. Engineer'}
zonesNoBackground zone thresholds (same as bullet chart zones)
zoneColorsNoCustom colors per zone band
zoneLabelsNoLabels for each zone band
themeNoTheme 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
paletteNoOverride palette only (mix-and-match)
typographyNoOverride typography: professional, luxury, cyberpunk, editorial, mono, bold, system, techno
effectsNoOverride effects: none, subtle, shimmer, neon, energetic

Implementation Reference

  • Main handler function that renders a dumbbell chart. Accepts a container and DumbbellData payload, applies theming, computes percentage positions, renders zones/scale labels/data rows with before/after dots and connecting bars, attaches click handlers for sendClickMessage, and adds export/refresh buttons.
    export function renderDumbbellChart(container: HTMLElement, payload: DumbbellData): 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 allVals = payload.data.flatMap((d) => [d.before, d.after]);
      const min = Math.min(...allVals);
      const max = Math.max(...allVals);
      const range = max - min || 1;
      const beforeLabel = payload.beforeLabel || "Before";
      const afterLabel = payload.afterLabel || "After";
      const unit = payload.unit || "";
    
      // Map a value to percentage position (10% - 90% range)
      const toPct = (v: number) => ((v - min) / range) * 80 + 10;
    
      // Build zone HTML for tracks
      const zones = payload.zones || [];
      let zonesBgHtml = "";
      if (zones.length >= 2) {
        const zoneCount = zones.length + 1;
        const colors = payload.zoneColors?.length === zoneCount
          ? payload.zoneColors
          : defaultZoneColors(zoneCount);
        const labels = payload.zoneLabels || [];
        let prevPct = 0;
        for (let z = 0; z <= zones.length; z++) {
          const endPct = z < zones.length ? toPct(zones[z]) : 100;
          const widthPct = endPct - prevPct;
          const color = colors[z % colors.length];
          const label = labels[z] || "";
          const opacity = 0.12 - (z * 0.01);
          zonesBgHtml += `<div class="dumbbell__zone" style="left:${prevPct}%;width:${widthPct}%;background:${color};opacity:${Math.max(opacity, 0.04)}">`;
          if (label && widthPct > 6) {
            zonesBgHtml += `<span class="dumbbell__zone-label">${escapeHtml(label)}</span>`;
          }
          zonesBgHtml += `</div>`;
          prevPct = endPct;
        }
      }
    
      // Build scale labels
      let scaleHtml = "";
      if (payload.scaleLabels && Object.keys(payload.scaleLabels).length > 0) {
        const markers = Object.entries(payload.scaleLabels).map(([valStr, label]) => {
          const val = parseFloat(valStr);
          const pct = toPct(val);
          return `<div class="dumbbell__scale-mark" style="left:${pct}%"><span class="dumbbell__scale-label">${escapeHtml(label)}</span></div>`;
        }).join("");
        scaleHtml = `<div class="dumbbell__row dumbbell__row--scale"><div class="dumbbell__label-wrap"></div><div class="dumbbell__track dumbbell__track--scale">${markers}</div><div></div></div>`;
      }
    
      const rows = payload.data.map((item, i) => {
        const bPct = toPct(item.before);
        const aPct = toPct(item.after);
        const leftPct = Math.min(bPct, aPct);
        const widthPct = Math.abs(aPct - bPct);
        const tooltipAttr = item.tooltip ? ` title="${escapeHtml(item.tooltip)}"` : "";
    
        return `
          <div class="dumbbell__row" data-idx="${i}"${tooltipAttr}>
            <div class="dumbbell__label-wrap">
              <div class="dumbbell__label">${escapeHtml(item.label)}</div>
            </div>
            <div class="dumbbell__track">
              ${zonesBgHtml}
              <div class="dumbbell__bar" style="left:${leftPct}%;width:${widthPct}%"></div>
              <div class="dumbbell__dot dumbbell__dot--before" style="left:${bPct}%" title="${beforeLabel}: ${item.before}${unit}"></div>
              <div class="dumbbell__dot dumbbell__dot--after" style="left:${aPct}%" title="${afterLabel}: ${item.after}${unit}"></div>
            </div>
            <div class="dumbbell__gap">${item.after > item.before ? "+" : ""}${(item.after - item.before).toLocaleString()}${unit}</div>
          </div>
        `;
      }).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="dumbbell">
              <div class="dumbbell__legend">
                <span class="dumbbell__legend-item"><span class="dumbbell__dot-sm dumbbell__dot--before"></span>${escapeHtml(beforeLabel)}</span>
                <span class="dumbbell__legend-item"><span class="dumbbell__dot-sm dumbbell__dot--after"></span>${escapeHtml(afterLabel)}</span>
              </div>
              ${rows}
              ${scaleHtml}
            </div>
          </div>
        </div>
      `;
    
      container.querySelectorAll<HTMLElement>(".dumbbell__row[data-idx]").forEach((el) => {
        el.style.cursor = "pointer";
        el.addEventListener("click", () => {
          const idx = parseInt(el.dataset.idx ?? "0", 10);
          const item = payload.data[idx];
          sendClickMessage(`[Dumbbell] "${payload.title}" - ${item.label}: ${item.before}${unit} -> ${item.after}${unit}`);
        });
      });
    
      const card = container.querySelector<HTMLElement>(".chart-card")!;
      addHtmlExportButton(card, payload.title);
      addRefreshButton(card, () => (window as any).__mcpRefresh?.());
    }
  • TypeScript interfaces defining the input schema for the dumbbell chart: DumbbellItem (label, before, after values) and DumbbellData (title, data array, optional labels, zones, colors, scale labels, and theme options).
    interface DumbbellItem {
      label: string;
      before: number;
      after: number;
      tooltip?: string;
    }
    
    interface DumbbellData {
      type: "dumbbell";
      title: string;
      data: DumbbellItem[];
      beforeLabel?: string;
      afterLabel?: string;
      unit?: string;
      scaleLabels?: Record<string, string>;
      zones?: number[];
      zoneColors?: string[];
      zoneLabels?: string[];
      theme?: string;
      palette?: string;
      typography?: string;
      effects?: string;
    }
  • Registers the dumbbell chart tool with the chart registry, mapping internal type 'dumbbell' to tool name 'render_dumbbell_chart' and binding the renderDumbbellChart handler.
    registerChart("dumbbell", "render_dumbbell_chart", renderDumbbellChart);
  • Helper function that linearly interpolates between two hex colors, used by defaultZoneColors to generate zone color gradients.
    function lerpColor(a: string, b: string, t: number): string {
      const parse = (hex: string) => [
        parseInt(hex.slice(1, 3), 16),
        parseInt(hex.slice(3, 5), 16),
        parseInt(hex.slice(5, 7), 16),
      ];
      const [r1, g1, b1] = parse(a);
      const [r2, g2, b2] = parse(b);
      const r = Math.round(r1 + (r2 - r1) * t);
      const g = Math.round(g1 + (g2 - g1) * t);
      const bl = Math.round(b1 + (b2 - b1) * t);
      return `#${((1 << 24) | (r << 16) | (g << 8) | bl).toString(16).slice(1)}`;
    }
  • Helper function that generates default zone colors (red-amber-green gradient) using lerpColor, used when zoneColors are not explicitly provided.
    function defaultZoneColors(count: number): string[] {
      if (count === 1) return ["#ef4444"];
      const colors: string[] = [];
      const stops = ["#ef4444", "#f59e0b", "#22c55e"];
      for (let i = 0; i < count; i++) {
        const t = i / (count - 1);
        if (t <= 0.5) colors.push(lerpColor(stops[0], stops[1], t * 2));
        else colors.push(lerpColor(stops[1], stops[2], (t - 0.5) * 2));
      }
      return colors;
    }
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations indicate readOnlyHint=true and idempotentHint=true, which the description does not contradict. The description adds context about absolute positioning and background zones, but does not disclose behavioral details like data requirements, sorting, or rendering behavior beyond what annotations and schema provide.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

Two concise sentences: first states the core purpose, second lists additional features. No redundant or irrelevant information. Front-loaded with the main action and resource.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity (13 parameters, nested objects, no output schema), the description provides a high-level overview that covers essential aspects. However, it could mention output format or default rendering behavior to be fully complete for a chart tool.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so each parameter already has a description. The description mentions scaleLabels and zones, but does not add significant new meaning beyond the schema. Baseline 3 is appropriate as the description offers marginal extra insight.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool renders a dumbbell chart, explains it as 'Before/after dots connected by a bar', and mentions supporting features like scale labels and background zones. This distinguishes it from sibling chart tools (e.g., slope chart, lollipop chart) by specifying the visual structure and use case.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage for comparing before/after values with 'How big is the gap?' but provides no explicit guidance on when to use this versus alternatives like render_slope_chart or render_lollipop_chart. No when-not-to-use or alternative references are given.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/KyuRish/mcp-dashboards'

If you have feedback or need assistance with the MCP directory API, please join our Discord server