CSV to Funnel Chart

Point this page at a stage column and a value column and it draws the funnel: one band per stage, each carrying its name, its formatted value and its share of the total, printed inside the band where no leader line is needed. Nothing uploads. The PNG, the SVG and the aggregated numbers all come straight back out.

Nothing to hand? Try it on campaign data · set Stage to channel and Value to conversions.

Need to filter or reshape first? Open the app

The label is the chart

A funnel has no axis. There is no scale down the side and no gridline to measure against, so the width of a band is only ever a rough impression. That is a real weakness of the shape, and the way to compensate for it is to write the numbers on the picture. Every band on this page carries one monospaced line inside it: the stage name, the value formatted the way that column deserves, and the percentage that value represents of everything drawn. A band reads Signup  4,112  31% and there is nothing left to hover for.

Inside the band, not beside it. External labels on a funnel need leader lines, and leader lines on a stack of touching trapezoids get confusing quickly. Putting the text where the shape is removes the question entirely. It also means the chart survives being a screenshot: pasted into a document, printed for a meeting, or dropped into a chat thread, it still says everything it said in the browser, because none of the meaning was hiding in a tooltip.

The bands are separated by a two-pixel border in the panel's own background color and by a small gap, and each one takes a different color from the categorical palette. That is a departure from most charts here, which use exactly one color unless the encoding genuinely needs more. In a funnel the bands touch and the widths change gradually, and separation is a real job that color is doing. It is not a ranking. A darker band is not worse than a lighter one.

Five controls

  • Stage. The column whose distinct values become bands. Step names in a checkout flow, lifecycle statuses in a CRM, channels, tiers, or anything else with a small number of repeating labels.
  • Value. The number measured at each stage. Set it to Count rows when your file is one row per event and the count itself is the measurement, which is how most raw exports arrive.
  • Aggregate. Sum, average, count, count distinct, minimum, maximum or median, applied within each stage. Sum is the default and covers both the one-row-per-stage file and the one-row-per-event file. Count distinct is the one to reach for when the rows are events and the question is about people.
  • Sort. Biggest first by default, which produces a funnel shape from almost any file without configuration. Switch to File order whenever your stages have a real sequence, and put the rows in your file in that sequence. Ranking a fixed process is how a broken step gets hidden.
  • Show. Top 5, 10, 20, 50 or everything. Ten is the default because a band has to be tall enough to hold its own label, and thirty bands in a 400 pixel panel are not.

Nothing here re-reads the file. Every control redraws from rows that were already parsed, so switching the aggregate or the sort is instant even on a large export.

Worked example: conversions by channel

The sample campaign file has eight columns, one row per campaign day:

date,campaign,channel,impressions,clicks,spend,conversions,revenue
2024-01-01,Retargeting,Affiliate,79590,4112,588.75,49,12173.75
2024-01-03,Brand,Display,76060,599,1033.92,15,4423.55
2024-01-05,Spring Launch,Display,30223,864,2022.08,52,7505.49
2024-01-07,Always On,Paid Search,51877,2290,1476.31,88,19204.02

Set Stage to channel and Value to conversions. The panel titles itself conversions by channel, the kicker reads FUNNEL, and the caption underneath reads Sum of conversions · biggest first · 5 stages. Five bands appear, widest at the top, each in its own palette color, each with one white monospaced line across it: Paid Search  1,842  34%, then Affiliate  1,203  22%, and so on down to the narrowest.

Be clear about what that chart is. Channels are not sequential stages, so this is a ranked composition wearing a funnel's clothes, and the tapering shape means only that the values decrease. It is a perfectly useful picture of where conversions come from, but calling it a conversion funnel would be sloppy. The genuine funnel from this file lives in the other columns: impressions, then clicks, then conversions, three measures rather than three rows. To chart that, reshape the file so those three become a stage column and a value column, which is a two-step unpivot in the app. Then Stage is metric, Value is amount, Sort goes to File order, and the bands read impressions, clicks, conversions, in that order, with the drop-off between them impossible to miss.

Download the stages writes a two-column CSV in exactly the drawn order. The percentages are not in that file, only the values, because a percentage of a total is a derived number and the total is right there.

When a stage is bigger than the one above it

Sooner or later you will draw a funnel where a band bulges wider than its predecessor, and the instinct is to assume the chart is broken. It is not. That bulge is a finding, and it almost always comes from one of two places.

The first is double counting, which is really a mismatch of grain. The upstream stage was measured per person and the downstream one per event, so a customer who added to cart three times contributes one to visited and three to added to cart. Every analytics export I have worked with has at least one column like this, and the fix is to switch Aggregate to Count distinct on an identifier column so both stages are counting the same kind of thing.

The second is that the stages are not actually sequential. Somebody listed the states a record can be in and assumed they form a pipeline, when in fact two of them are parallel branches and a third can be entered directly from outside. Nothing requires the population of a later state to be a subset of an earlier one, so nothing forces the widths to decrease. When this is what is going on, a funnel is the wrong shape and a sankey is usually the right one, because it can show the branches.

A third and rarer cause is worth a mention: rows that legitimately enter partway through, like an imported list of leads that skips signup. That is not an error either, and if it is material it belongs in the caption of your slide rather than in a footnote nobody reads.

Four exports

  • Download PNG. Two-times density on the panel background, so it lands cleanly on a light or a dark slide instead of turning into a floating black box.
  • Copy image. The same PNG on the clipboard, ready to paste into a doc or a chat without writing a file first.
  • Download SVG. Rendered a second time as vectors, so the bands and their labels stay crisp at any size and can be recolored to a house palette in Figma or Illustrator.
  • Download the stages. The aggregated stage and value pairs as CSV, in drawn order.

To publish the funnel on a page rather than in a document, the embed builder packs the data and the settings into an iframe URL, so it renders from a link with no backend and nothing stored.

What this page will not do

  • No stage-over-stage conversion rate. The percentage on each band is a share of the total, not a ratio to the band above. Computing the step rate takes one calculated column in the app, and I would rather show one number that is defined than two that get confused.
  • No comparison funnels. One file, one funnel. Two segments side by side means two charts, or a dashboard.
  • It will not unpivot for you. If your stages are columns rather than rows, this page cannot reshape them. Do that in the app first.
  • No cohort or time dimension. A funnel here is a snapshot. Movement over time belongs on csv-to-graph or a timeline.
  • No filtering. Every row goes in. Filter beforehand, or split the file with the CSV splitter.
  • Negative values are not meaningful. A funnel band cannot have negative width, and a file containing negatives will produce a shape that is technically drawn and analytically nonsense. Use a waterfall for signed movements.
  • A header row is required. Without one, the first line of data becomes your column names.

Frequently Asked Questions

What exactly is written on each band?

Three things, on one line, in monospace, inside the band itself: the stage name, the formatted value, and that value as a percentage of the total of every stage drawn. So a band reads Signup 4,112 31%. The label sits inside rather than outside because a funnel is a stack of shapes with no axis, and an external label needs a leader line to say which band it belongs to. Putting the text in the band removes the ambiguity and removes the leader line at the same time. Values are formatted from the role of the column, so money keeps its currency symbol and large counts are abbreviated.

Is the percentage a share of the total or a step conversion rate?

A share of the total. Every value drawn is added together, and each band shows its own value divided by that sum, rounded to a whole number. It is not the ratio to the previous stage and it is not the ratio to the first stage. This matters, because most funnel articles you have read are describing step conversion, and reading these numbers as though they were step rates will give you a wrong answer every time. If you want stage-over-stage conversion, compute it as a column in the app and chart it separately, or read the values themselves, which is usually faster.

Why does the sort default to biggest first here?

Because the widest band belongs at the top for the shape to read as a funnel, and biggest first produces that with no configuration on most real files. It is a convenience default, not a claim about your data. When your stages have a fixed sequence, and they usually do, switch Sort to File order and arrange the rows in your file in the order the process actually happens. That is the more honest chart, because it will show you a stage that is out of order instead of hiding it by ranking.

One of my stages is bigger than the stage above it. Is that a bug?

No, and it is worth taking seriously rather than sorting away. In practice it means one of two things. Either you are double counting, because the later stage is measured per event while the earlier one is measured per person, so a user who checked out twice appears twice downstream. Or the stages are not actually sequential, and what you have drawn as a funnel is really a set of parallel paths that happen to share a chart. Both are real findings. Check the grain of each stage first, then check whether every row in stage three genuinely passed through stage two.

Why does each band get a different color?

Because the bands need separating from each other and there is nothing else on the canvas to do that job. Most charts on this site are drawn in a single color, since one measure split by one dimension does not need a rainbow. A funnel is different: the bands touch, the widths change gradually, and a two-pixel border in the panel background color is not enough on its own. So the categorical palette is used, purely as separation. The colors carry no ranking and no judgment. Adjacent pairs in that palette are machine-checked for separation under common forms of color vision deficiency, in both light and dark mode.

Can I draw two funnels side by side to compare segments?

Not on this page. One file produces one funnel. To compare mobile against desktop, filter the file into two and chart each, or build a dashboard where several panels come from the same source. Overlaying two funnels in one shape is a chart type I have deliberately avoided, because the visual comparison it appears to offer is much weaker than the two numbers you would get by simply reading the values.

What if the same stage appears on many rows?

The rows are grouped by stage and combined with whatever the Aggregate control says, which starts at Sum. That is the normal case for event data, where one row is one event and the stage column repeats thousands of times. If your file already holds one row per stage with a final count in it, Sum leaves those numbers untouched, so the same default works for both shapes of file. Count distinct is the aggregate to reach for when the raw rows are events and what you actually want is people.

Why did some of my stages vanish?

The Show control caps how many bands are drawn, and it starts at ten. A funnel with thirty bands is a gradient, not a chart, because each band ends up a few pixels tall and the label inside it will not fit. Raise Show to 20, 50 or Everything if you really do have that many stages, but consider grouping the small ones first. The panel says how many stages were left out whenever it drops any.

Draw the funnel

Drop the CSV, pick the stage and the value, read the percentages off the bands. No sign-up, no upload.

Back to the funnel maker