CSV to Heatmap

Pick a column for the rows, a column for the columns and a number to fill the cells, and this page cross-tabulates your CSV into a shaded grid. Both axes are ordered by total so the hot corner lands top left, combinations that never happened stay blank instead of faking a zero, and the whole thing is computed in your browser.

Nothing to cross-tabulate yet? Try with sample sales data · regions against products, shaded on arrival.

Want the same cross-tab as readable numbers? The pivot page adds real totals.

Five controls and what each one decides

A heatmap is the shape for the question which combination is unusual. A bar chart answers which category is biggest. A grid answers which pairing is hot, which is a question a bar chart genuinely cannot express, because the answer lives at an intersection rather than in a single category. Five controls get you there.

  • Rows. The category running down the left side. Long labels belong here, because horizontal space is cheap on the left edge and expensive across the top.
  • Columns. The category running across the top. Short labels, or a date column when you want a calendar-style reading such as month across and team down.
  • Value. The numeric column that fills the cells. Leave it empty and the grid counts records instead, which is the honest reading of a file with no measures in it.
  • Aggregate. Sum, average, count rows, count distinct, minimum, maximum, median. Sum answers how much, average answers how much per record, and median is the one to reach for when a couple of enormous cells are dragging the shading of everything else toward pale.
  • Palette. Sequential or Diverging, as a two-way segmented control. This is the choice that most changes what a reader takes away from the picture, and it gets its own section below.

Leave Columns empty and the page stops with a message asking you to pick a second column across the top, because a matrix without two sides is a bar chart wearing a costume. Leave Rows empty and it asks for one under Rows in the same way.

The 40 by 40 guard, and why it is a feature

If either side of the grid would carry more than 40 distinct values, the page refuses and quotes the dimensions it actually found: a grid of so many rows by so many columns, which it describes as a spreadsheet rather than a heatmap, along with the instruction to pick columns with at most 40 distinct values each or to aggregate the data first. That number in the message is the useful part. Refusals that just say too many categories leave you guessing whether you are near the limit or a hundred times past it. Being told you asked for 312 by 88 tells you instantly that the row field is probably an identifier.

The reasoning behind the cap is physical rather than technical. A heatmap works because a reader can hold a whole grid in view and pick out the outlier by shade. At 40 by 40 you are already at 1,600 cells, each a few pixels across, and the shade differences between neighbors stop being distinguishable at that size. Past it, you are not looking at a picture, you are looking at texture, and the only cells you can actually identify are the two extremes you could have found with a sort.

Note carefully what the cap is on: distinct values per side, not rows in the file. A million-row export cross-tabulated as region against month is a 6 by 12 grid and it draws immediately. What trips the guard is choosing a field that was never a category, usually an order ID, a timestamp with seconds in it, or a free-text note field. When that happens, the honest fix is not a bigger grid. It is a coarser field: bucket the timestamps to months, group the SKUs into product lines, and the picture becomes readable at the same time as it becomes legal.

Blank is not zero

This is the design decision I would defend hardest on the page. When a row category and a column category never appear together in the data, that cell is left unpainted. It does not get the palest shade of the ramp and it does not get a zero written in it. A note under the grid then counts the gaps: so many of the total combinations never occur in the data, and those cells stay blank rather than being drawn as a zero.

The difference matters because the two facts have opposite operational meanings. A zero says the pairing was attempted and produced nothing: the product was on sale in that region and sold none. A blank says the pairing does not exist in your file at all: the product was never stocked in that region, or the channel launched halfway through the year, or the team did not exist yet. Painting the second as the first manufactures a failure out of an absence. In a sales review that turns into somebody being asked to explain a number that was never real.

The counted note also does double duty as a sparsity check. A 12 by 20 grid with 180 of its 240 combinations missing is telling you that a heatmap is the wrong shape for this file, and that a ranked bar chart or a sankey would say more with less ink. I look at that note before I look at the shading, and it has talked me out of a heatmap more than once.

Sequential or diverging

The Palette control has two settings and choosing wrongly does not break anything, which is exactly why it goes wrong so often. The picture still looks professional. It just answers a different question than the one you asked.

  • Sequential runs from light to dark within a single hue. Light means little, dark means a lot, and the reading is ordered along one direction. Use it for revenue, units, headcount, ticket counts, durations, anything whose floor is zero and whose meaning is magnitude. Every step of the ramp is monotonic in lightness, so it survives being printed in grayscale and it survives most forms of color-vision deficiency.
  • Diverging centers on zero and runs outward in two hues, one for each sign, with the palest shade at the middle rather than at one end. Use it for variance against a target, week-over-week change, margin delta, forecast error, or any column that can genuinely be negative. Because the neutral point is pinned to zero rather than to the middle of the data range, a grid where everything happens to be positive shows as one-sided, which is correct: the zero line is a fact about the measure, not a cosmetic center.

The two failure modes are symmetric. Sequential on a change column hides the sign, so a cell at negative eight thousand and a cell at positive eight thousand can end up the same shade of nothing, and the one thing you needed to see is the one thing that vanished. Diverging on a plain magnitude column invents a meaningful midpoint where none exists, splitting revenue into two color families that imply good and bad about numbers that were all simply sales.

Worked example: regions against products

Seven columns and 180 rows of orders spanning 2024. The first few lines:

date,region,product,channel,units,unit_price,revenue
2024-01-02,West,Doohickey,Direct,58,211.64,12275.12
2024-01-04,West,Sprocket,Partner,13,40.73,529.49
2024-01-06,North,Sprocket,Retail,31,147.97,4587.07
2024-01-08,East,Gizmo,Direct,7,172.02,1204.15

Set Rows to region, Columns to product, Value to revenue and leave Aggregate on Sum. The panel draws a grid titled revenue by region and product, with a caption reading Sum of revenue per combination · 4 x 5 · blanks stay blank. Four regions run down the left in descending order of their own totals, five products run across the top in descending order of theirs, and the darkest cell sits in the top left corner of the grid because both axes were sorted that way.

Twenty cells is well under the 120-cell threshold, so each cell prints its own value in mono, formatted as money because revenue was profiled as money rather than as a bare number: $61.4k, $48.9k, and so on. That makes this small grid quotable. You can lift a number straight out of the picture into a sentence without going back to the source.

Now swap Columns to channel. With four regions and three channels the grid is 4 by 3, and if one region never sold through Partner, that cell is left unpainted and a note appears underneath saying 1 of 12 combinations never occurs in the data and stays blank rather than being drawn as a zero. Switch Aggregate to Average and the same twelve cells redraw as revenue per order, which reorders the shading entirely: the region with the most orders is rarely the region with the biggest orders, and that gap is the whole reason to look.

Download the matrix writes it out as CSV: one line per region, the region name first, then one field per column in the same order shown on screen, with absent combinations written as empty fields. The image exports sit alongside it. PNG comes out at twice pixel density on the panel's own background, SVG re-renders through a vector path renderer so the cells stay crisp at poster size, and Copy image puts the PNG on the clipboard for a chat message.

What this page will not do

Real limits, stated plainly, so you find them here rather than five minutes in:

  • No grid larger than 40 on a side. The guard is hard and it quotes the dimensions you asked for. Aggregate to a coarser field instead of hunting for an override.
  • No row and column totals. A heatmap shades cells; margins would be a different scale sharing the same ramp and would read as if they belonged to the grid. Totals that are recomputed from the source rows live on the pivot page.
  • No clustering or reordering by similarity. Both axes are sorted by total, full stop. There is no dendrogram and no seriation, which is a real limitation if you came here from a bioinformatics habit.
  • No manual color scale. You choose sequential or diverging; you do not set the endpoints, the number of steps or the hues. Palette control lives on the embed builder, where a chart has to match somebody else's page.
  • No filtering. Every row in the file is cross-tabulated. Filter first in the app.
  • Cell values only under 120 cells. Above that the numbers are dropped rather than shrunk into illegibility. Use the export when you need every figure from a large grid.
  • A header row is required. Without one, the first data line becomes the column names and both axes will be wrong in a way that looks right.

Comma, semicolon, tab and pipe delimited files all load without configuration. Excel workbooks and JSON flatten into the same rectangle on their own pages and then behave exactly like this one.

Frequently Asked Questions

Why does the heatmap refuse to draw my grid?

Because one of the two chosen columns has more than 40 distinct values, and past that point a heatmap stops being readable. The message quotes the dimensions it actually found, in the form of a rows by columns grid, and says that is a spreadsheet rather than a heatmap. Pick columns with at most 40 distinct values each, or aggregate the data down first. The limit is on distinct values per side, not on rows in the file: a million orders across six regions and twelve months is a 6 by 12 grid and draws instantly.

Why are some cells empty instead of zero?

Because those combinations never occur in the data, and a combination that never happened is not a measurement of zero. A blank cell says nobody sold that product through that channel; a cell shaded as zero says somebody tried and got nothing. Those are different facts and only one of them is in your file. A note under the grid counts them, in the form of how many of the total combinations never occur, so the emptiness is reported rather than silently absorbed.

How are the rows and columns ordered?

By total, descending, on both axes independently. Each row is summed across its cells, each column is summed down its cells, and both are sorted so the largest sits first. The practical effect is that the hot corner lands top left, which is where the eye starts on a left-to-right page. Alphabetical ordering scatters the interesting cells at random and makes you hunt for them; ordering by magnitude turns the same grid into a shape you can read in one pass.

When do the numbers appear inside the cells?

When the grid is 120 cells or fewer, so anything up to a 10 by 12 or an 8 by 15. Below that threshold there is room for a legible number in each cell and the heatmap doubles as a small table you can quote from. Past it, the labels would collide and the shading has to carry the reading on its own, with the exact values available from the export and the preview table underneath.

Should I use the sequential or the diverging palette?

Sequential for magnitude, diverging for movement around a midpoint. A sequential ramp runs light to dark in one hue, so it encodes more and less, which is what you want for revenue, counts, durations or anything with a natural floor at zero. A diverging ramp centers on zero and runs two hues outward, so it encodes above and below, which is what you want for variance against a target, week-over-week change, a margin delta or any column that can genuinely be negative. Using sequential on a change column hides the sign, and using diverging on a plain magnitude column invents a meaningful midpoint that does not exist.

Which columns do Rows, Columns and Value take?

Rows and Columns each take a categorical or date column and become the two sides of the grid. Value takes the numeric column being aggregated, and Aggregate decides how: sum, average, count rows, count distinct, minimum, maximum or median. If you leave Value empty the grid counts records instead, which is the right reading for a file that has no numbers in it at all. Leaving Columns empty stops the heatmap with a message asking for a second column across the top, because a matrix needs two sides.

Can I export the matrix as numbers rather than as a picture?

Yes. Download the matrix writes the grid as CSV with one line per row of the heatmap, the row label first and then one field per column, in the same order shown on screen. Cells that never occurred are written as empty fields rather than as zeros, so the export preserves the same distinction the picture makes. The image exports are there too: PNG at twice pixel density, SVG with real vector paths, and Copy image straight to the clipboard.

Does the file get uploaded to build the heatmap?

No. Reading the CSV, grouping it into cells, sorting the axes and painting the grid all happen inside your browser tab. There is no upload endpoint behind this page and no request carrying your rows. This matters more than usual for a heatmap, because the files people cross-tabulate tend to be the sensitive ones: headcount by team, spend by vendor, incidents by service.

Shade your grid

Two columns, one number, one picture of where the heat actually is. No sign-up and no upload.

Back to the heatmap builder