DEV Community

laolide
laolide

Posted on

pptxgenjs dropped one of my chart series and reported nothing

I built a PPT generator. It produced a valid file. PowerPoint opened it. The charts rendered.
But one line was gone, and two bars were missing. Nothing threw an error.
The symptom
I write a tool that generates PPT decks. It runs on Python and pptxgenjs.
The pipeline worked. The file downloaded fine. My checker said everything passed.
Then I opened the file and looked at it:
1.Line chart: should have 4 data points, rendered only 1 line
2.Bar chart: should have 3 groups, lost 2 of them
Nothing anywhere raised an error.

  • The generator did not throw
  • The file opens
  • PowerPoint does not complain
  • My own checker reported "all passed" The cause Here is the normalization code: // Wrong: this tests the container type const isSeriesList = rawSeries.length && Array.isArray(rawSeries[0]) ? rawSeries : [rawSeries[0]];

// The data looks like this:
rawSeries = [
{ name: '数量', values: [120, 180, 240] },
{ name: '占比', values: [22, 31, 40] }
]

// Array.isArray(rawSeries[0]) → false (the first item is an object, not an array)
// → the whole array gets wrapped as "one series"
// → 2 series become 1
// → the chart draws only the first line
The file stays valid. The chart stays valid. Half the data is simply gone.
The pattern behind this
Silent degradation ≠ crash
Crash → you know immediately
Silent drop → you think it worked, until a human reads the output
I call this "looks right, is wrong". It is far harder to debug than an exception.
How I debugged it, and why every step was wrong
This part matters more than the bug itself.

  1. Suspected the dual-axis format → changed it, still broken
  2. Suspected an options parameter → bisected them all, none was it
  3. Added a log statement → the combo branch never ran at all
  4. Ran the normalization alone → saw data.length == 1 Step 3 was the turning point. The branch never ran, so the condition was false. My condition at the time: if (condition) { combo branch } else { plain branch } → combo branch never ran = condition is false → the condition called Array.isArray(...) → go look at what that data actually is Steps 1 to 3 all asked "am I using the library correctly?" Step 4 was the first to ask "is my own logic wrong?" Write the check by meaning, not by shape // Wrong: structural test Array.isArray(r[0]) // a series object is of course not an array

// Also wrong, and this one bites you twice
r.every(x => 'values' in x)
That second one looks right. It is not. Array.prototype.values exists,
so the in operator walks the prototype chain and returns true for any array.
I only found this because I ran the check against both shapes instead of trusting it.
// Right: semantic test, verified against both shapes
!Array.isArray(x) && x && x.values !== undefined
// or: Object.prototype.hasOwnProperty.call(x, 'values')
Both tests ask about the shape of the data. Only one tells them apart:

  • [[{...}], [{...}]]: the raw nested form pptxgenjs uses
  • [{...}, {...}]: a flat list of series Structurally both are lists. Semantically they are opposite. Lesson I learned the hard way: I wrote the check, ran it, saw green, and stopped. The check was wrong in a way no amount of reading would have shown me. Only feeding it a bad sample exposed it. Three checks I added afterward Fixing the bug was not enough. My checker had to catch this class of problem next time. import re, zipfile

def count_series(xml: str) -> int:
# count('') misses and any namespaced prefix.
return len(re.findall(r'<(?:\w+:)?ser[\s>]', xml))

series_count = 0
with zipfile.ZipFile(pptx_path) as z:
for n in z.namelist():
if 'charts/chart' in n and n.endswith('.xml'):
series_count += count_series(z.read(n).decode('utf-8'))

if series_count < expected_series:
report("chart series were silently dropped")
Do not write xml.count(''). I did. It returns 0 on a real chart,
because the tag carries attributes and a namespace prefix. A check that
always reads zero never fires, and a check that never fires looks identical
to a passing one.
The three checks, running together:

  1. Series count → regex the XML, compare against expected
  2. Point alignment → categories must equal data points, otherwise report
  3. Empty data → all categories or all values empty, refuse to draw Check 1 is the important one. The plots list in python-pptx misses multi-series charts. Only the XML is reliable. Then I made the same mistake again I added the checks. I ran them. Everything was green. I was happy. I was about to stop. Then I did one thing: I built a broken sample the check was supposed to catch. → It reported "all passed". The checks existed. The main flow never called them. The habit now: write the check → confirm the main flow actually calls it → build a bad sample → confirm it gets caught → only then call it done I wrote this rule down. Two days later I broke it again. Writing a rule is not the same as following it. If you write generator tools
  4. "Generated successfully" does not mean "the content is right". Read the artifact.
  5. The in operator checks the prototype chain, so "key" in [] can be true.
  6. Test data shape by semantic feature, not by container type.
  7. Checks must read the artifact back. Count XML nodes, do not trust the library.
  8. Silent degradation is harder than an error, because it looks correct.
  9. Before you report all green, build one broken sample and watch it fail. Number 6 is the whole point of this post. While writing it I checked my own example against both data shapes, and found the "correct" check in my original draft was broken too. Every symptom here comes from my own tool: 2 series collapsing to 1, the four wrong debugging steps, and the three checks I added after.

Top comments (0)