d3-dsv parses text you already have (d3.csvParse, d3.tsvParse, d3.dsvFormat(';')) and writes it back (d3.csvFormat); d3-format and d3-time-format turn numbers and dates into labels. A row function converts each record as it is parsed, which is clearer than autoType when types matter:
import { csvParse, format, utcParse } from 'd3';
const text = 'month,book,units\n2025-12,5,91\n2025-12,1,82\n'; // two December rows
console.log(csvParse(text)[0]); // every value a string
const rows = csvParse(text, ({ month, book, units }) => // a row function
({ month: utcParse('%Y-%m')(month), book: +book, units: +units }));
console.log(rows[0]);
const usd = format('$,.2f'), pct = format('.1%'), si = format('.2~s');
console.log(usd(91 * 16.2), pct(1218 / 5425), si(118551.96), format('+d')(-169));Output
{ month: '2025-12', book: '5', units: '91' }
{ month: 2025-12-01T00:00:00.000Z, book: 5, units: 91 }
$1,474.20 22.5% 120k −169Specifiers follow Python's mini-language ($,.2f currency, .1% percent, ~s SI prefix). The minus is a true U+2212, d3-format's default, which d3.formatLocale() can change. d3-dsv also installs a csv2json command.
<!doctype html>
<style>
body { margin: 0; padding: 8px; background: #fafaf7; font: 12px system-ui, sans-serif; color: #263238; max-width: 600px; }
.row { display: flex; gap: 10px; }
textarea { width: 190px; height: 150px; font: 12px monospace; }
svg { flex: 1; min-width: 0; }
pre { margin: 4px 0; }
</style>
<script src="https://cdn.jsdelivr.net/npm/d3@7.9.0/dist/d3.min.js"></script>
<div class="row">
<textarea id="csv">month,book,units
2025-12,5,124
2025-12,1,111
2025-12,3,100
2025-12,2,84
2025-12,6,64
2026-03,4,100</textarea>
<svg viewBox="0 0 380 160" font-size="11"></svg>
</div>
<pre id="out"></pre>
<script>
const titles = { 1: 'The Quiet Harbor', 2: 'Patterns of the Deep Web', 3: 'Salt and Saffron',
4: 'Small Steps to Big Summits', 5: "The Clockmaker's Paradox", 6: 'Gardens in Glass' };
const usd = d3.format('$,.2f'), pct = d3.format('.1%'), si = d3.format('.2~s'), month = d3.utcFormat('%b %Y');
function update() {
const text = document.getElementById('csv').value;
const raw = d3.csvParse(text); // every value is a string
// A row function converts each record as it is parsed
const rows = d3.csvParse(text, ({ month, book, units }) =>
({ month: d3.utcParse('%Y-%m')(month), book: +book, units: +units }));
const total = d3.sum(rows, d => d.units);
const y = d3.scaleBand(rows.map((d, i) => i), [0, 150]).padding(0.15);
const x = d3.scaleLinear([0, d3.max(rows, d => d.units) || 1], [0, 170]);
const g = d3.select('svg').selectAll('g').data(rows).join(enter => {
const g = enter.append('g');
g.append('rect').attr('fill', '#1f5f8b');
g.append('text').attr('class', 't').attr('dy', '0.35em');
g.append('text').attr('class', 'v').attr('dy', '0.35em');
return g;
}).attr('transform', (d, i) => `translate(0,${y(i)})`);
g.select('rect').attr('x', 130).attr('height', y.bandwidth()).attr('width', d => x(d.units) || 0);
g.select('.t').attr('x', 125).attr('y', y.bandwidth() / 2).attr('text-anchor', 'end')
.text(d => `${titles[d.book] ?? 'book ' + d.book}`.slice(0, 20));
g.select('.v').attr('x', d => 134 + (x(d.units) || 0)).attr('y', y.bandwidth() / 2)
.text(d => `${d.units} (${pct(d.units / total)})`);
d3.select('#out').text(`csvParse without a row function: ${JSON.stringify(raw[0])}\n` +
`with one: month ${rows[0] && month(rows[0].month)}, units ${rows[0]?.units} (a number)\n` +
`formats: ${usd(124 * 16.2)} ${pct(1218 / 5425)} ${si(123979)} ${d3.format('+d')(-169)}\n` +
`back to text: ${d3.csvFormat(rows.slice(0, 1).map(d => ({ ...d, month: month(d.month) })))}`.replace(/\n(?=month,)/, ' '));
}
document.getElementById('csv').addEventListener('input', update);
update();
</script>