← Back

Convoy

February 10, 2026

This was the first version of Convoy, before it grew into a Python/notebook codegen tool: a canvas-based node graph for building data pipelines, where dragging in sources, filters, and transforms produces a chart you can push straight to Datawrapper instead of a static export. The pipeline runs entirely as an in-memory DataFrame, and a chart node's job is just to describe an axis mapping and a chart type against whatever the graph produces upstream.

The Datawrapper handoff needed two conversions: the internal chart type had to map onto Datawrapper's own vocabulary (bard3-bars, scatterd3-scatter-plot, and so on), and the DataFrame itself had to become a CSV payload for their upload API. The CSV serialization is the fiddlier of the two, since it has to quote any cell that would otherwise break the format:

export function dataFrameToCSV(data: DataFrame): string {
  const columnNames = data.columns.map((c) => c.name);

  const escapeCell = (value: unknown): string => {
    if (value === null || value === undefined) return '';
    const str = String(value);
    if (str.includes(',') || str.includes('"') || str.includes('\n')) {
      return `"${str.replace(/"/g, '""')}"`;
    }
    return str;
  };

  const header = columnNames.map(escapeCell).join(',');
  const rows = data.rows.map((row) =>
    columnNames.map((col) => escapeCell(row[col])).join(',')
  );

  return [header, ...rows].join('\n');
}