Tornado Diagram for Sensitivity Analysis — D3.js

A horizontal bar chart where bars are sorted by influence magnitude, extending left and right from a base case vertical reference line. Each bar represents one input parameter and shows how varying that parameter between its low and high values affects the output, creating a characteristic tornado shape (widest bars at top, narrowest at bottom). Dual colors distinguish low-input from high-input effects, making it immediately clear which parameters drive the most uncertainty.

Tornado Diagram for Sensitivity Analysis rendered with D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// bar-tornado-sensitivity: Tornado Diagram for Sensitivity Analysis
// Library: d3 7.9.0 | JavaScript 22.22.3
// Quality: 88/100 | Created: 2026-06-02

const t = window.ANYPLOT_TOKENS;
const { width, height } = window.ANYPLOT_SIZE;

const BASE_NPV = 10.0;

// NPV sensitivity data for a capital project — values in $M
const rawData = [
  { parameter: "Revenue Growth Rate", lowNpv:  5.5, highNpv: 15.0 },
  { parameter: "Discount Rate",       lowNpv:  7.0, highNpv: 14.0 },
  { parameter: "Operating Costs",     lowNpv:  7.0, highNpv: 13.5 },
  { parameter: "Market Size",         lowNpv:  7.0, highNpv: 13.0 },
  { parameter: "Tax Rate",            lowNpv:  8.5, highNpv: 12.5 },
  { parameter: "Material Costs",      lowNpv:  8.5, highNpv: 12.0 },
  { parameter: "Labour Efficiency",   lowNpv:  8.5, highNpv: 11.5 },
  { parameter: "Customer Retention",  lowNpv:  9.0, highNpv: 11.0 },
];

// Sort by range descending — widest bar at top (tornado shape)
const data = rawData.slice().sort(
  (a, b) => (b.highNpv - b.lowNpv) - (a.highNpv - a.lowNpv)
);

const margin = { top: 90, right: 150, bottom: 100, left: 234 };
const iw = width - margin.left - margin.right;
const ih = height - margin.top - margin.bottom;

// SVG mount
const svg = d3.select("#container")
  .append("svg").attr("width", width).attr("height", height);
const g = svg.append("g")
  .attr("transform", `translate(${margin.left},${margin.top})`);

// Scales
const x = d3.scaleLinear().domain([4, 17]).nice().range([0, iw]);
const y = d3.scaleBand()
  .domain(data.map(d => d.parameter))
  .range([0, ih]).padding(0.38);
const bh = y.bandwidth();

// Imprint palette — high scenario = brand green (#009E73), low = semantic red (#AE3030)
const COLOR_HIGH = t.palette[0];
const COLOR_LOW  = t.palette[4];

// Gridlines (vertical, subtle)
x.ticks(7).forEach(v => {
  g.append("line")
    .attr("x1", x(v)).attr("x2", x(v))
    .attr("y1", 0).attr("y2", ih)
    .attr("stroke", t.grid).attr("stroke-width", 1);
});

// Base case reference line (dashed)
const baseX = x(BASE_NPV);
g.append("line")
  .attr("x1", baseX).attr("x2", baseX)
  .attr("y1", -20).attr("y2", ih + 10)
  .attr("stroke", t.inkSoft).attr("stroke-width", 2)
  .attr("stroke-dasharray", "10 6");

// Base case label
g.append("text")
  .attr("x", baseX + 8).attr("y", -24)
  .attr("fill", t.inkSoft).style("font-size", "14px")
  .text(`Base: $${BASE_NPV}M`);

// Bars (one row group per parameter)
const rows = g.selectAll(".row").data(data).join("g")
  .attr("class", "row")
  .attr("transform", d => `translate(0,${y(d.parameter)})`);

// Low-scenario bars — extend left from base
rows.append("rect")
  .attr("x", d => x(d.lowNpv))
  .attr("y", 0)
  .attr("width", d => x(BASE_NPV) - x(d.lowNpv))
  .attr("height", bh)
  .attr("fill", COLOR_LOW)
  .attr("stroke", t.pageBg)
  .attr("stroke-width", 1.5)
  .attr("opacity", 0.88);

// High-scenario bars — extend right from base
rows.append("rect")
  .attr("x", x(BASE_NPV))
  .attr("y", 0)
  .attr("width", d => x(d.highNpv) - x(BASE_NPV))
  .attr("height", bh)
  .attr("fill", COLOR_HIGH)
  .attr("stroke", t.pageBg)
  .attr("stroke-width", 1.5)
  .attr("opacity", 0.88);

// Value labels at bar ends
rows.append("text")
  .attr("x", d => x(d.lowNpv) - 7)
  .attr("y", bh / 2).attr("dy", "0.35em")
  .attr("text-anchor", "end")
  .attr("fill", t.inkSoft).style("font-size", "14px")
  .text(d => `$${d.lowNpv}M`);

rows.append("text")
  .attr("x", d => x(d.highNpv) + 7)
  .attr("y", bh / 2).attr("dy", "0.35em")
  .attr("text-anchor", "start")
  .attr("fill", t.inkSoft).style("font-size", "14px")
  .text(d => `$${d.highNpv}M`);

// Callout: annotate the highest-impact parameter (widest bar at top)
const topParam = data[0];
const topBarTopY = y(topParam.parameter);
const topBarMidX = (x(topParam.lowNpv) + x(topParam.highNpv)) / 2;
g.append("line")
  .attr("x1", x(topParam.lowNpv) + 6).attr("x2", x(topParam.highNpv) - 6)
  .attr("y1", topBarTopY - 10).attr("y2", topBarTopY - 10)
  .attr("stroke", t.inkSoft).attr("stroke-width", 1).attr("stroke-dasharray", "4 3");
g.append("text")
  .attr("x", topBarMidX).attr("y", topBarTopY - 14)
  .attr("text-anchor", "middle")
  .attr("fill", t.inkSoft).style("font-size", "13px").style("font-style", "italic")
  .text("highest impact");

// Y-axis (parameter names)
const yAxis = g.append("g").call(d3.axisLeft(y).tickSize(0).tickPadding(14));
yAxis.selectAll("text").attr("fill", t.ink).style("font-size", "16px");
yAxis.select(".domain").attr("stroke", "none");

// X-axis
const xAxis = g.append("g")
  .attr("transform", `translate(0,${ih})`)
  .call(d3.axisBottom(x).ticks(7).tickSize(0).tickPadding(8).tickFormat(d => `$${d}M`));
xAxis.selectAll("text").attr("fill", t.inkSoft).style("font-size", "14px");
xAxis.select(".domain").attr("stroke", "none");

// X-axis label
g.append("text")
  .attr("x", iw / 2).attr("y", ih + 72)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink).style("font-size", "17px")
  .text("Net Present Value ($ Millions)");

// Legend — in right margin, vertically centered near top of chart
const lgX = iw + 20;
const lgY = Math.round(ih / 2) - 30;
[{ label: "High Scenario", color: COLOR_HIGH },
 { label: "Low Scenario",  color: COLOR_LOW  }].forEach((item, i) => {
  g.append("rect")
    .attr("x", lgX).attr("y", lgY + i * 34)
    .attr("width", 16).attr("height", 16)
    .attr("fill", item.color).attr("opacity", 0.88);
  g.append("text")
    .attr("x", lgX + 22).attr("y", lgY + i * 34 + 12)
    .attr("fill", t.inkSoft).style("font-size", "14px")
    .text(item.label);
});

// Chart title
svg.append("text")
  .attr("x", width / 2).attr("y", 52)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink).style("font-size", "22px").style("font-weight", "600")
  .text("bar-tornado-sensitivity · javascript · d3 · anyplot.ai");

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/bar-tornado-sensitivity/d3/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "bar-tornado-sensitivity",
  "language": "javascript",
  "library": "d3",
  "page": "https://anyplot.ai/bar-tornado-sensitivity/javascript/d3",
  "hub": "https://anyplot.ai/bar-tornado-sensitivity",
  "code_json": "https://api.anyplot.ai/specs/bar-tornado-sensitivity/d3/code",
  "spec_json": "https://api.anyplot.ai/specs/bar-tornado-sensitivity",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/bar-tornado-sensitivity/javascript/d3/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/bar-tornado-sensitivity/javascript/d3/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/bar-tornado-sensitivity/javascript/d3/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/bar-tornado-sensitivity/javascript/d3/plot-dark.html",
  "quality_score": 88.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Tornado Diagram for Sensitivity Analysis on anyplot.ai.

Other implementations