Skip to content

chart: a title styling prop is rejected on a chart with no title element, while the axis path creates one #536

Open

Description

@argszero

Summary

A title.* styling property is rejected on a chart that has no title element yet, while the same key on the axis path creates the title and applies the style. The chart path and the axis path disagree about the same situation.

Measured in xlsx, docx and pptx, on the shipped 1.0.151 and on a 1.0.158 build of current main (06ab5201).

Reproduction

officecli create a.xlsx
officecli add a.xlsx /Sheet1 --type chart --prop 'data=Sales:10,20'

# chart path: the chart has no <c:title> element
officecli set a.xlsx '/Sheet1/chart[1]' --prop 'title.size=20'
Error: No properties applied to /Sheet1/chart[1]
UNSUPPORTED props: title.size.

The axis path, same file, same starting condition (no axis title exists):

officecli set a.xlsx '/Sheet1/chart[1]/axis[@role=value]' --prop 'title.size=20'
Updated /Sheet1/chart[1]/axis[@role=value]: title.size=20 (applied: title=, title.size=20pt)

and the part gains a title: <c:title> count 0 → 1, holding <a:defRPr sz="2000" b="1"/> with empty text.

Measured

xlsx, chart starting with zero <c:title> elements, counting them in xl/drawings/charts/chart1.xml before and after:

path command exit code <c:title> before → after
chart set 'title.size=20' 2 0 → 0
chart set 'title.bold=true' 2 0 → 0
chart set 'title.color=FF0000' 2 0 → 0
chart set 'title.font=Arial' 2 0 → 0
axis set 'title.size=20' 0 0 → 1
chart, title present set 'title.size=20' 0 1 → 1, sz="2000"

The same split holds for the other two formats:

format path used chart with no title after title=Sales
xlsx /Sheet1/chart[1] rc 2 rc 0
docx /chart[1] rc 2 rc 0
pptx /slide[1]/chart[1] rc 2 rc 0

Two things that are not affected, so the fix does not need to touch them:

  • title=Sales alone creates the title (rc 0).
  • A single call carrying text and styling works: --prop 'title=Sales' --prop 'title.size=20' → rc 0, sz="2000" written. Only the styling-only case is rejected.

Where it is

src/officecli/Core/Chart/ChartHelper.Setter.cs:301-310 — the fan-out for the styling keys:

case "title.font" or "titlefont":
case "title.size" or "titlesize":
case "title.color" or "titlecolor":
case "title.bold" or "titlebold":
case "title.glow" or "titleglow":
case "title.shadow" or "titleshadow":
case "title.lang" or "titlelang":
{
    var ctitle = chart.GetFirstChild<C.Title>();
    if (ctitle == null) { unsupported.Add(key); break; }

The axis twin has already solved exactly this: src/officecli/Core/Chart/ChartHelper.Axis.cs:747-754 buffers the four keys into pendingAxisTitleStyling and applies them after the translated forward, and its comment names the failure verbatim — these keys "used to fall to default and forward to the chart-level title handler, which then mutated the wrong title (or returned UNSUPPORTED when no chart title existed)". The chart level is the site that was left behind.

One design question

The axis path resolves the situation by creating an empty title and styling it. The chart path could mirror that, which is what I would submit — or you may prefer a clear "set title= first" error over an empty title element that renders as nothing. Either way the two paths should agree; right now a caller cannot tell from the docs why the same key works on one and not the other.

I'd like to work on this.

Activity

  1. argszero commented on Oct 22, 2026

    @argszero
    ContributorAuthor

    Fixed in #537.

    Cause. The chart-level branch kept a if (ctitle == null) { unsupported.Add(key); break; } guard for all seven title.* styling pairs, while the axis branch had already solved the same situation in R52 bt-2 (ChartHelper.Axis.cs) by building an empty <c:title> and applying the key to its runs. So title.size=20 on a chart without a title element was reported as an unknown prop (exit 2) while the same request one level down on an axis returned exit 0 — the asymmetry in the report.

    Fix. Mirror the axis precedent at chart level: when the chart has no title, synthesize the same empty-title shape the axis path builds, insert it as c:title at its CT_Chart position (first child, the site title= already uses), then run the existing per-run styling code unchanged. The text stays empty — the prop describes how a title looks, not what it says — and autoTitleDeleted is left untouched, so a title the caller suppressed does not reappear blank. This is the direction I described when claiming the issue; if you would rather the styling props keep reporting UNSUPPORTED on a title-less chart, say so and the PR is easy to drop.

    Verification (two-arm, both binaries from the same clone, base 06ab5201):

    • Synthetic: title.size / title.bold / title.color / title.font flip from exit 2 to exit 0; title=, text-plus-style in one call, an already-titled chart and the axis path itself are byte-for-byte unchanged; dump→batch round trip keeps the created title and validates clean. Same flip for docx and pptx, which share this setter.
    • Real documents: two decks in examples/ each hold a reachable chart with no <c:title> — budget_review_v2.pptx (slide 5 chart 1) goes 1 → 2 titles, exit 2 → 0, empty title with sz="2000", idempotent on a second call; charts-area.pptx (slide 2 chart 4, autoTitleDeleted val="1") goes 33 → 34 titles with the suppression flag and its value intact.
    • No regressions: 63 example decks replayed through both arms — chart parts identical 63/63, <c:title> counts equal (459/459), dump output byte-identical, validate exit 0 throughout.
  2. goworm commented on Oct 26, 2026

    @goworm
    Contributor

    Confirmed. We went the other way from "mirror the axis": neither path fabricates a title any more. A title.* styling key on a chart or axis that has no title is now rejected as invalid_value with a message (and a structured suggestion) saying to pass title=<text> in the same command, which keeps working as before. The axis path's empty-title creation is removed, since an empty <c:title> renders as nothing. Presets still skip title styling on a title-less chart. Will be in the next release; #537 is not merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions