Dynamic Charts (FileReader) Get Certified
Once you can draw a chart from data, the next step is making it dynamic: reading a CSV or JSON file with FileReader, parsing it, and redrawing the chart with the new values. This lesson shows clean, reusable patterns that work well in the live editor.
Parse CSV Text and Draw a Bar Chart
Dynamic charts follow three steps: read the file, parse its text into data, and draw the data. Keeping each step in its own function makes every one easy to test and reuse. On a real page, the file comes from input.files[0] and readFile() uses a real FileReader; here a stand-in file and reader (at the bottom of the example) keep it runnable in the editor.
What this code does: Reads a CSV file with a reader, parses its text into row objects and draws a bar chart of the revenue column. The three steps are three separate functions: readFile, parseCSV and drawBarChart. The text output shows the file name and size, how many rows were parsed and what the first row looks like (the revenue is still text at that point). The finished chart appears below the text output.
// 1) READ: get the text of a file
function readFile(file, onText, onError) {
const reader = createMockFileReader(); // real page: new FileReader()
reader.onload = () => onText(reader.result);
reader.onerror = () => onError(reader.error);
reader.readAsText(file);
}
// 2) PARSE: turn CSV text into an array of objects
function parseCSV(text) {
const lines = text.trim().split("\n");
const headers = lines[0].split(",");
return lines.slice(1).map(row => {
const values = row.split(",");
const obj = {};
headers.forEach((h, i) => obj[h.trim()] = values[i].trim());
return obj;
});
}
// 3) DRAW: turn the objects into a chart
function drawBarChart(data) {
const canvas = document.createElement("canvas");
canvas.width = 480;
canvas.height = 280;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, 480, 280);
const values = data.map(d => Number(d.revenue));
const labels = data.map(d => d.month);
const max = Math.max(...values);
const barWidth = 50;
const gap = 25;
const startX = 40;
const baseY = 230;
values.forEach((val, i) => {
const height = (val / max) * 180;
const x = startX + i * (barWidth + gap);
ctx.fillStyle = "#3b82f6";
ctx.fillRect(x, baseY - height, barWidth, height);
ctx.fillStyle = "#1e293b";
ctx.font = "13px Arial";
ctx.textAlign = "center";
ctx.fillText(labels[i], x + barWidth / 2, baseY + 20);
ctx.fillText(val, x + barWidth / 2, baseY - height - 8);
});
return canvas;
}
// A "file" the user might have chosen
const file = createMockFile("revenue.csv", `month,revenue
Jan,4200
Feb,5100
Mar,4800
Apr,6300
May,5900`, "text/csv");
readFile(file, (text) => {
console.log("File read:", file.name, "(" + file.size + " bytes)");
const data = parseCSV(text);
console.log("Parsed", data.length, "rows from CSV");
console.log("First row:", data[0]);
const chart = drawBarChart(data);
console.log("Bar chart created from CSV data");
showCanvas("Revenue chart built from " + file.name, chart);
}, (error) => {
console.log("Could not read the file:", error.message);
});
// ===== Helpers below are not part of the lesson =====
function showCanvas(label, source) {
const copy = document.createElement("canvas");
copy.width = source.width;
copy.height = source.height;
copy.getContext("2d").drawImage(source, 0, 0);
copy.style.maxWidth = "100%";
setTimeout(() => {
const box = document.createElement("div");
box.className = "demo-canvas-box";
box.innerHTML = "<p class='demo-canvas-label'>" + label + "</p>";
box.appendChild(copy);
document.getElementById("previewContent").appendChild(box);
}, 0);
}
function createMockFile(name, content, type) {
return {
name: name,
size: content.length,
type: type || "text/plain",
_content: content
};
}
function createMockFileReader() {
const reader = {
result: null,
error: null,
onload: null,
onerror: null,
readAsText(file) {
if (!file || file._unreadable) {
reader.error = new Error("The file could not be read");
if (reader.onerror) reader.onerror({ target: reader });
return;
}
reader.result = file._content;
if (reader.onload) reader.onload({ target: reader });
}
};
return reader;
}
JSON Data to Pie Chart
JSON is easier than CSV: JSON.parse() turns the text straight into arrays and objects, with real numbers and no manual splitting.
What this code does: Reads a JSON file containing a budget breakdown, parses it with JSON.parse() and draws a pie chart with a legend. The text output lists each category and its value. The numbers in the JSON stay numbers, so no conversion is needed, unlike the CSV example. The chart appears below the text output.
// 1) READ: get the text of a file
function readFile(file, onText, onError) {
const reader = createMockFileReader(); // real page: new FileReader()
reader.onload = () => onText(reader.result);
reader.onerror = () => onError(reader.error);
reader.readAsText(file);
}
function drawPieChart(slices) {
const canvas = document.createElement("canvas");
canvas.width = 460;
canvas.height = 300;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, 460, 300);
const total = slices.reduce((sum, s) => sum + s.value, 0);
let startAngle = -Math.PI / 2;
const cx = 130, cy = 150, radius = 100;
slices.forEach((slice, i) => {
const angle = (slice.value / total) * Math.PI * 2;
// Wedge
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, radius, startAngle, startAngle + angle);
ctx.closePath();
ctx.fillStyle = slice.color;
ctx.fill();
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 2;
ctx.stroke();
// Legend entry
ctx.fillStyle = slice.color;
ctx.fillRect(270, 90 + i * 30, 16, 16);
ctx.fillStyle = "#1e293b";
ctx.font = "14px Arial";
ctx.textAlign = "left";
ctx.fillText(slice.label + " (" + Math.round(slice.value / total * 100) + "%)", 294, 103 + i * 30);
startAngle += angle;
});
return canvas;
}
const budget = [
{ label: "Marketing", value: 35, color: "#3b82f6" },
{ label: "Engineering", value: 40, color: "#f97316" },
{ label: "Support", value: 15, color: "#22c55e" },
{ label: "Other", value: 10, color: "#a855f7" }
];
// Turn the array into JSON text, like the content of a saved .json file
const file = createMockFile("budget.json", JSON.stringify(budget), "application/json");
readFile(file, (text) => {
const data = JSON.parse(text);
console.log("Parsed JSON with", data.length, "categories");
data.forEach(item => console.log(item.label + ": " + item.value));
const chart = drawPieChart(data);
console.log("Pie chart drawn from JSON data");
showCanvas("Budget breakdown from " + file.name, chart);
}, (error) => {
console.log("Could not read the file:", error.message);
});
// ===== Helpers below are not part of the lesson =====
function showCanvas(label, source) {
const copy = document.createElement("canvas");
copy.width = source.width;
copy.height = source.height;
copy.getContext("2d").drawImage(source, 0, 0);
copy.style.maxWidth = "100%";
setTimeout(() => {
const box = document.createElement("div");
box.className = "demo-canvas-box";
box.innerHTML = "<p class='demo-canvas-label'>" + label + "</p>";
box.appendChild(copy);
document.getElementById("previewContent").appendChild(box);
}, 0);
}
function createMockFile(name, content, type) {
return {
name: name,
size: content.length,
type: type || "text/plain",
_content: content
};
}
function createMockFileReader() {
const reader = {
result: null,
error: null,
onload: null,
onerror: null,
readAsText(file) {
if (!file || file._unreadable) {
reader.error = new Error("The file could not be read");
if (reader.onerror) reader.onerror({ target: reader });
return;
}
reader.result = file._content;
if (reader.onload) reader.onload({ target: reader });
}
};
return reader;
}
Updating the Same Chart with New Data
A chart does not need special ‘update’ code: call the same draw function again with new data and use the new canvas in place of the old one. Real dashboards do exactly this every time a new file or data set arrives.
What this code does: Draws a bar chart of Week 1 sales, then calls the same function again with Week 2 data, and shows both charts so you can compare before and after. Both charts use a fixed scale of 100 (the optional third argument), so a bar of the same height means the same value in both pictures. Without it, each chart would stretch its own tallest bar to the full height and compare misleadingly. The text output shows the weekly totals and the change.
function drawBarChart(data, title, maxValue) {
const canvas = document.createElement("canvas");
canvas.width = 480;
canvas.height = 280;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, 480, 280);
const values = data.map(d => d.value);
const labels = data.map(d => d.label);
const max = maxValue || Math.max(...values);
const barWidth = 45;
const gap = 20;
const startX = 40;
const baseY = 220;
// Title
ctx.fillStyle = "#1e293b";
ctx.font = "bold 16px Arial";
ctx.textAlign = "center";
ctx.fillText(title, 240, 25);
values.forEach((val, i) => {
const height = (val / max) * 160;
const x = startX + i * (barWidth + gap);
ctx.fillStyle = "#0ea5e9";
ctx.fillRect(x, baseY - height, barWidth, height);
ctx.fillStyle = "#334155";
ctx.font = "12px Arial";
ctx.textAlign = "center";
ctx.fillText(labels[i], x + barWidth / 2, baseY + 18);
ctx.fillText(val, x + barWidth / 2, baseY - height - 6);
});
return canvas;
}
function total(data) {
return data.reduce((sum, d) => sum + d.value, 0);
}
// First dataset
const week1 = [
{ label: "Mon", value: 40 },
{ label: "Tue", value: 65 },
{ label: "Wed", value: 50 },
{ label: "Thu", value: 80 },
{ label: "Fri", value: 55 }
];
console.log("Drawing chart with Week 1 data...");
const chart1 = drawBarChart(week1, "Week 1 Sales", 100);
showCanvas("Before: Week 1 data", chart1);
// Simulate new data arriving
const week2 = [
{ label: "Mon", value: 55 },
{ label: "Tue", value: 70 },
{ label: "Wed", value: 60 },
{ label: "Thu", value: 90 },
{ label: "Fri", value: 75 }
];
console.log("Updating chart with Week 2 data...");
const chart2 = drawBarChart(week2, "Week 2 Sales", 100);
showCanvas("After: Week 2 data (same function, new data)", chart2);
console.log("Week 1 total:", total(week1));
console.log("Week 2 total:", total(week2));
const change = total(week2) - total(week1);
console.log("Change:", "+" + change, "(" + (change / total(week1) * 100).toFixed(1) + "%)");
console.log("Same function, new data → updated chart");
// ===== Helpers below are not part of the lesson =====
function showCanvas(label, source) {
const copy = document.createElement("canvas");
copy.width = source.width;
copy.height = source.height;
copy.getContext("2d").drawImage(source, 0, 0);
copy.style.maxWidth = "100%";
setTimeout(() => {
const box = document.createElement("div");
box.className = "demo-canvas-box";
box.innerHTML = "<p class='demo-canvas-label'>" + label + "</p>";
box.appendChild(copy);
document.getElementById("previewContent").appendChild(box);
}, 0);
}
Handling Bad or Empty Files
Users upload empty files, files with only a header, and files that cannot be read at all. Good code checks for these cases and shows a clear message instead of a broken chart.
What this code does: Tests four files: a valid CSV, an empty file, a file that has a header but no data, and a file that cannot be read (like one deleted after it was chosen). Failures to parse are caught with try/catch, and failures to read are handled in onerror. For every file the text output explains what happened, and a coloured panel below it shows what the user would see: green for success and red for a clear fallback message.
function parseCSV(text) {
const lines = text.trim().split("\n").filter(line => line.trim() !== "");
if (lines.length < 2) {
throw new Error("CSV has no data rows");
}
const headers = lines[0].split(",");
return lines.slice(1).map(row => {
const values = row.split(",");
const obj = {};
headers.forEach((h, i) => obj[h.trim()] = values[i] ? values[i].trim() : "");
return obj;
});
}
function drawPanel(message, ok) {
const canvas = document.createElement("canvas");
canvas.width = 420;
canvas.height = 70;
const ctx = canvas.getContext("2d");
ctx.fillStyle = ok ? "#dcfce7" : "#fee2e2";
ctx.fillRect(0, 0, 420, 70);
ctx.strokeStyle = ok ? "#16a34a" : "#dc2626";
ctx.lineWidth = 3;
ctx.strokeRect(1.5, 1.5, 417, 67);
ctx.fillStyle = ok ? "#166534" : "#991b1b";
ctx.font = "bold 16px Arial";
ctx.textAlign = "center";
ctx.fillText((ok ? "✔ " : "✖ ") + message, 210, 41);
return canvas;
}
function safeDraw(file) {
const reader = createMockFileReader();
reader.onload = () => {
try {
const data = parseCSV(reader.result);
console.log(file.name + ": parsed", data.length, "rows");
showCanvas(file.name, drawPanel("Loaded " + data.length + " rows", true));
} catch (err) {
console.log(file.name + ": could not process file -", err.message);
console.log("Showing a fallback message instead of a broken chart.");
showCanvas(file.name, drawPanel(err.message, false));
}
};
reader.onerror = () => {
console.log(file.name + ": could not read the file -", reader.error.message);
showCanvas(file.name, drawPanel(reader.error.message, false));
};
reader.readAsText(file);
}
safeDraw(createMockFile("good.csv", "month,sales\nJan,100\nFeb,150"));
safeDraw(createMockFile("empty.csv", ""));
safeDraw(createMockFile("header-only.csv", "month,sales"));
const locked = createMockFile("locked.csv", "month,sales\nJan,100");
locked._unreadable = true; // simulates a file deleted or locked after it was chosen
safeDraw(locked);
// ===== Helpers below are not part of the lesson =====
function showCanvas(label, source) {
const copy = document.createElement("canvas");
copy.width = source.width;
copy.height = source.height;
copy.getContext("2d").drawImage(source, 0, 0);
copy.style.maxWidth = "100%";
setTimeout(() => {
const box = document.createElement("div");
box.className = "demo-canvas-box";
box.innerHTML = "<p class='demo-canvas-label'>" + label + "</p>";
box.appendChild(copy);
document.getElementById("previewContent").appendChild(box);
}, 0);
}
function createMockFile(name, content, type) {
return {
name: name,
size: content.length,
type: type || "text/plain",
_content: content
};
}
function createMockFileReader() {
const reader = {
result: null,
error: null,
onload: null,
onerror: null,
readAsText(file) {
if (!file || file._unreadable) {
reader.error = new Error("The file could not be read");
if (reader.onerror) reader.onerror({ target: reader });
return;
}
reader.result = file._content;
if (reader.onload) reader.onload({ target: reader });
}
};
return reader;
}
Practice: Multi-Week Dashboard
The payoff of separating read, parse and draw: loading a different file is just another call to the same three functions.
What this code does: Simulates a user loading two different CSV files, a 4-week file and then an updated 6-week file. loadAndDraw() reads each one, parses it, prints the number of weeks and the total sales, and draws a chart with the same draw function. The output shows both charts, with more bars in the second one. Here each chart scales to its own tallest bar; the previous example used a fixed scale instead, which is better when charts will be compared. Try adding a third file.
// 1) READ: get the text of a file
function readFile(file, onText, onError) {
const reader = createMockFileReader(); // real page: new FileReader()
reader.onload = () => onText(reader.result);
reader.onerror = () => onError(reader.error);
reader.readAsText(file);
}
function parseCSV(text) {
const [header, ...rows] = text.trim().split("\n");
const keys = header.split(",");
return rows.map(row => {
const vals = row.split(",");
const obj = {};
keys.forEach((k, i) => obj[k] = vals[i]);
return obj;
});
}
function drawBarChart(data, caption) {
const canvas = document.createElement("canvas");
canvas.width = 500;
canvas.height = 280;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, 500, 280);
const values = data.map(d => Number(d.sales));
const labels = data.map(d => d.week);
const max = Math.max(...values);
const barW = 55;
const gap = 18;
const startX = 40;
const baseY = 220;
ctx.fillStyle = "#0f172a";
ctx.font = "bold 15px Arial";
ctx.textAlign = "center";
ctx.fillText(caption, 250, 22);
values.forEach((v, i) => {
const h = (v / max) * 160;
const x = startX + i * (barW + gap);
ctx.fillStyle = "#16a34a";
ctx.fillRect(x, baseY - h, barW, h);
ctx.fillStyle = "#1e293b";
ctx.font = "12px Arial";
ctx.textAlign = "center";
ctx.fillText(labels[i], x + barW / 2, baseY + 18);
ctx.fillText(v, x + barW / 2, baseY - h - 6);
});
return canvas;
}
function loadAndDraw(file, caption) {
readFile(file, (text) => {
const data = parseCSV(text);
const total = data.reduce((sum, row) => sum + Number(row.sales), 0);
console.log(file.name + ": " + data.length + " weeks, total sales " + total);
showCanvas(caption, drawBarChart(data, caption));
}, (error) => {
console.log("Error:", error.message);
});
}
// First upload
const file1 = createMockFile("sales-4-weeks.csv", `week,sales
W1,120
W2,150
W3,90
W4,200`, "text/csv");
// Second upload (more data)
const file2 = createMockFile("sales-6-weeks.csv", `week,sales
W1,120
W2,150
W3,90
W4,200
W5,175
W6,230`, "text/csv");
console.log("Loading first file (4 weeks)...");
loadAndDraw(file1, "Sales Dashboard – 4 Weeks");
console.log("Loading updated file (6 weeks)...");
loadAndDraw(file2, "Sales Dashboard – 6 Weeks");
console.log("Chart updated with new data using the same draw function");
// ===== Helpers below are not part of the lesson =====
function showCanvas(label, source) {
const copy = document.createElement("canvas");
copy.width = source.width;
copy.height = source.height;
copy.getContext("2d").drawImage(source, 0, 0);
copy.style.maxWidth = "100%";
setTimeout(() => {
const box = document.createElement("div");
box.className = "demo-canvas-box";
box.innerHTML = "<p class='demo-canvas-label'>" + label + "</p>";
box.appendChild(copy);
document.getElementById("previewContent").appendChild(box);
}, 0);
}
function createMockFile(name, content, type) {
return {
name: name,
size: content.length,
type: type || "text/plain",
_content: content
};
}
function createMockFileReader() {
const reader = {
result: null,
error: null,
onload: null,
onerror: null,
readAsText(file) {
if (!file || file._unreadable) {
reader.error = new Error("The file could not be read");
if (reader.onerror) reader.onerror({ target: reader });
return;
}
reader.result = file._content;
if (reader.onload) reader.onload({ target: reader });
}
};
return reader;
}
Quick Summary
- Separate read → parse → draw into different functions.
- The same draw function can be called again with new data to “update” the chart.
- Use a fixed scale when charts will be compared, and an automatic scale when each file stands alone.
- Always validate parsed data and handle empty, bad or unreadable files gracefully.
- CSV needs manual splitting and its values are text; JSON only needs
JSON.parse().
Tip: Click “Test My Code” and compare the numbers in the text output with the charts below it. Change a value in the data, run it again, and watch the chart change.
🧪 Test Your JavaScript Code
Edit the code on the left and click “Run Code” to see the console output on the right.
Click “Run Code” to see the output here.
Code Explanation
The complete example stays visible. Use Previous and Next to move through its logical blocks.
Select Next to begin exploring the code.