ジェネレータがチャートを書き直したあと、C 列を表示するはずのラベルが裸の数字で返ってきたり、あるバーが 20.0 ではなく S1; 2; 20 と読めたりします。それでいて Excel は文句ひとつなくファイルを開きます。Value From Cells ラベルは Office 2013 のチャート拡張の中にあり、HotXLS は TXLSXChartSeries.DataLabelsRange から、スキーマ順に揃った完全な点ごとラベル設定として書き出します
Excel は Value From Cells ラベルをどこに置くのか
ECMA-376 のチャートスキーマ(Part 1、§21.2)には、「このラベルのテキストをワークシート範囲から取れ」という意味の要素がありません。型 CT_DLbls、つまり系列の中の <c:dLbls> ブロックは、任意個数の点ごとの <c:dLbl> 上書きを先頭に置き、その後に <c:delete> か共有ラベルグループが来て、末尾に拡張リストを持ちます。共有グループは固定の順序です:numFmt、spPr、txPr、dLblPos、showLegendKey、showVal、showCatName、showSerName、showPercent、showBubbleSize、separator、showLeaderLines、leaderLines。Excel の Format Data Labels ペインのチェックボックスはほぼすべて、これらの要素のどれかに対応します。Value From Cells は対応しません。Excel 2013 が標準の上に積んだのが、c15 名前空間(http://schemas.microsoft.com/office/drawing/2012/chart)の拡張リストです。Microsoft は [MS-ODRAWXML] で文書化していますが、ECMA-376 スキーマに厳密に基づいて作られたリーダーには、そこにある不透明な <c:ext> 要素しか見えません。大事な部分だけを抜き出すと、セル範囲ラベルを持つ系列は次のようになります:
<c:ser>
<c:idx val="0"/><c:order val="0"/>
<c:tx>...</c:tx>
<c:dLbls>
<!-- 0 個以上の <c:dLbl> 点上書きがブロックの先頭に来る -->
<c:showLegendKey val="0"/>
<c:showVal val="1"/>
<c:showCatName val="0"/>
<c:showSerName val="0"/>
<c:showPercent val="0"/>
<c:showBubbleSize val="0"/>
<c:extLst>
<c:ext uri="{CE6537A1-D6FC-4f65-9D91-7224C49458BB}"
xmlns:c15="http://schemas.microsoft.com/office/drawing/2012/chart">
<c15:showDataLabelsRange val="1"/>
</c:ext>
</c:extLst>
</c:dLbls>
<c:cat>...</c:cat>
<c:val>...</c:val>
<c:extLst>
<c:ext uri="{02D57815-91ED-43cb-92C2-25804820EDAC}"
xmlns:c15="http://schemas.microsoft.com/office/drawing/2012/chart">
<c15:datalabelsRange>
<c15:f>Sales!$C$1:$C$3</c15:f>
<c15:dlblRangeCache>
<c:ptCount val="3"/>
<c:pt idx="0"><c:v>Target met</c:v></c:pt>
...
</c15:dlblRangeCache>
</c15:datalabelsRange>
</c:ext>
</c:extLst>
</c:ser>
この機能は 2 つの場所に分かれています。範囲は系列のものです。URI {02D57815-91ED-43cb-92C2-25804820EDAC} の系列レベル拡張が c15:datalabelsRange を運び、その c15:f が参照を、c15:dlblRangeCache がファイル保存時の表示テキストを保持します。スイッチはラベルのものです。c15:showDataLabelsRange は <c:dLbls> の末尾にある URI {CE6537A1-D6FC-4f65-9D91-7224C49458BB} の拡張の中に座り、同じ拡張で個々の <c:dLbl> を閉じれば、1 点の範囲テキストをオンオフできます
どちらの半分も単独では機能しません。範囲だけあってスイッチがないファイルはセルのテキストを表示せず、スイッチだけあって範囲がないファイルは表示するものを持ちません。§21.2 が定義するものだけをモデル化したプログラムは、チャートパートを再生成するときに両方を失います。だからこそ HotXLS は未モデル化の ChartML を編集を通じて保つのです。キャッシュは参照と同じくらい大事です。範囲を解決できないコンシューマー、たとえば他のワークブックを指す範囲でも、描くテキストは必要です
系列にラベル範囲を設定する
HotXLS では、範囲は系列のプロパティです。TXLSXChartSeries.DataLabelsRange を代入すると ShowDataLabelsRange もオンになり、明示的に設定された印が付きます。スイッチなしの範囲は決して表示されないからです。空文字列を代入すれば再びオフになります。セルのテキストは通常のスイッチ(ShowValue、ShowCategoryName、ShowSeriesName、ShowPercent、ShowLegendKey、ShowBubbleSize)と組み合わされ、Separator で連結され、LabelNumFmt が値の部分を整形します。近似曲線の数式ラベルはこのブロックの一部ではなく、<c:trendline> の中にあり、近似曲線の数式テキストに対する Excel 独自の規則に従います。チャートの保存時には、ResolveDataLabelsRange がセルの整形済みテキストから DataLabelsCache を更新します。チャートが可視セルだけをプロットするときは、隠し行と隠し列をスキップします。他のワークブックを指す範囲は、開いたときのキャッシュを保ちます。ワークブックとチャートの作成はいつもの HotXLS チャートパターンに従います。新しいワークブックには、追加するまでシートがなく、Sheets.Items は 1 起点です
uses
lxHandleX;
procedure BuildRevenueChart(const FileName: WideString);
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Chart: TXLSXChart;
Series: TXLSXChartSeries;
begin
Book := TXLSXWorkbook.Create;
try
Sheet := Book.Sheets.Add('Sales');
Sheet.Cells[1, 1].Value := 'East';
Sheet.Cells[1, 2].Value := 18.25;
Sheet.Cells[1, 3].Value := 'Target met';
Sheet.Cells[2, 1].Value := 'Central';
Sheet.Cells[2, 2].Value := 20;
Sheet.Cells[2, 3].Value := 'Restated';
Sheet.Cells[3, 1].Value := 'West';
Sheet.Cells[3, 2].Value := 14.6;
Sheet.Cells[3, 3].Value := 'Below plan';
Chart := Sheet.AddChart(xlsxChartBar, 'Revenue by region', 1, 5, 16, 12);
Series := Chart.AddSeries('Revenue', 'Sales!$A$1:$A$3', 'Sales!$B$1:$B$3');
// ShowDataLabelsRange := True も行い、設定済みとして記録する
Series.DataLabelsRange := 'Sales!$C$1:$C$3';
Series.ShowValue := True; // セルのテキストと値の両方
Series.Separator := ' | ';
Series.LabelNumFmt := '0.0'; // 値の部分は 20 ではなく 20.0 と読める
Series.LabelPos := xlsxDlblPosOutEnd;
Book.SaveAs(FileName);
finally
Book.Free;
end;
end;
スキーマの ST_DLblPos 列挙は 9 つの位置トークンを列挙し、どのチャートでもどれでも受け付けるので、バリデーターは折れ線系列の outEnd に異議を唱えません。どの位置が意味を成すかはチャートの種類で決まります。棒と縦棒の系列は ctr、inBase、inEnd、outEnd を取り、円グラフは bestFit を加え、折れ線と散布図の系列は l、r、t、b を使います。HotXLS は選ばれた TXLSXChartDataLabelPos 値をそのトークンとして書き、判断は Excel に任せるので、LabelPos は自分のチャート種類に属するサブセットから選ぶべきです。xlsxDlblPosDefault を代入すると LabelPosSet がクリアされ、dLblPos は一切書かれず、Excel がラベルを自分で配置します。上の例の期待結果:各バーが端の外側にラベルを付け、セルのテキストと小数 1 桁の値を、 | で連結して示します
点ごとのラベルは何を繰り返すべきなのか
系列ラベルの言うことすべてです。スキーマは点上書きを安価に見せています。CT_DLbl は idx と、delete かラベルグループ(同じ numFmt から separator までの並びの前に、省略可能な layout と tx)と省略可能な拡張リストから成り、グループの全メンバーが省略可能です。だから idx と showDataLabelsRange 拡張しか持たない <c:dLbl> はスキーマ的に妥当です。Excel はそのようなラベルを 20.0 ではなく S1; 2; 20 と描画します。省略された show* スイッチをそれぞれオンと見なし、系列名とカテゴリが現れ、系列の数値形式は引き継がないのです。点ラベルは、その点の系列ラベルに継ぎ足すのでなく置き換えます。Excel 自身が書く点ラベルがスイッチ一式を全部運ぶのはこのためです。XML を小さく保とうと最小の上書きを出すジェネレータは、検証は通り、それでいて間違ったテキストを表示します
HotXLS はすべての点ラベルを完全な置き換えとして書きます。SetPointShowDataLabelsRange(APointIndex, AVisible) は、1 点の範囲テキストのオンオフを記録します。キーになるのは idx になる 0 起点の点インデックスです。保存時には、そのような点ごとに 1 つの <c:dLbl> が付きます。idx、系列の数値形式、位置、スイッチがグループ順に並び、最後にその点の値を運ぶ {CE6537A1-…} 拡張です。SetPointLabelText(APointIndex, AText) は <c:tx> でリテラルテキストを加えます。テキスト付きのインデックスも、範囲上書きも、保存されたレイアウトも、1 つの <c:dLbl> に収まります。読み込みでは、系列スイッチは系列レベルから取られるので、点ラベルの中のコピーがそれを上書きすることはありません。下のコードは収益系列に 2 つの上書きを設定し、その後の XML が点 1 に期待されるラベルです
procedure OverridePoints(Series: TXLSXChartSeries);
var
I: Integer;
Visible: Boolean;
begin
// 2 本目のバー(idx 1):セルのテキストなしで値だけ表示する
Series.SetPointShowDataLabelsRange(1, False);
// 3 本目のバー(idx 2):ラベル全体をリテラルテキストで置き換える
Series.SetPointLabelText(2, 'Q3 pending audit');
for I := 0 to Series.DataLabelRangeOverrideCount - 1 do
Writeln('point ', Series.DataLabelRangeOverrideIndexes[I],
' range text: ', Series.DataLabelRangeOverrideValues[I]);
if Series.TryGetPointShowDataLabelsRange(0, Visible) then
Writeln('point 0 has its own override')
else
Writeln('point 0 follows ShowDataLabelsRange = ',
Series.ShowDataLabelsRange);
end;
<!-- 点 1 に期待される XML -->
<c:dLbl>
<c:idx val="1"/>
<c:numFmt formatCode="0.0" sourceLinked="0"/>
<c:dLblPos val="outEnd"/>
<c:showLegendKey val="0"/>
<c:showVal val="1"/>
<c:showCatName val="0"/>
<c:showSerName val="0"/>
<c:showPercent val="0"/>
<c:showBubbleSize val="0"/>
<c:separator> | </c:separator>
<c:extLst>
<c:ext uri="{CE6537A1-D6FC-4f65-9D91-7224C49458BB}"
xmlns:c15="http://schemas.microsoft.com/office/drawing/2012/chart">
<c15:showDataLabelsRange val="0"/>
</c:ext>
</c:extLst>
</c:dLbl>
Excel がチャートを開けても XML はほぼ何も証明しない
Excel は子の順序が崩れたチャートも開けます。<c:dLbls> の中に未知の要素がいても、プロパティを正しく読み戻します。val="banana" のような不正な属性値が Workbooks.Open を落とす、というのが落とし穴です。他のコンシューマーはもっと厳しい。Open XML SDK の OpenXmlValidator は順序不正の子をスキーマエラーとして報告し、スキーマに厳密に束縛するツールは拒否します。「Excel では動く」チャートでも、下流のパイプラインを壊せるのです。生成したチャートパートは OpenXmlValidator で検証してください。Excel でのダブルクリックが教えてくれるのは、Excel が切り抜けたことだけです。§21.2 スキーマの順序規則のうち、間違えやすいものは次のとおりです:
<c:dLbls>では点上書きが先頭に来て、共有グループが続き、extLstがブロックを閉じます。グループの中ではdLblPosがすべてのshow*スイッチに先行し、showLegendKeyがshowValに先行します- 点ラベルの中では
layoutとtxがidxに続き、numFmtの前に来ます。点の拡張リストが閉じます - 棒系列は
idx、order、tx、spPr、invertIfNegative、pictureOptions、dPt、dLbls、trendline、errBars、cat、val、shape、extLstの順に並びます。折れ線系列は、棒系列のinvertIfNegativeとpictureOptionsの位置にmarkerを、shapeの位置にsmoothを持ちます - 散布図、面、バブルの系列は同じ骨格を保ちます(ラベル、近似曲線、エラーバー、データの順)。それぞれのメンバーが付きます。散布図とバブルのデータには
xValとyVal、バブル系列ではデータの後にbubbleSizeとbubble3Dです - よくある系列レベルの書き間違いは、データ参照を
dLblsの前に書くこと、errBarsをtrendlineの前に置くこと、smoothをデータの前に置くことです。Excel はどれにも文句を言いません
手動のラベル位置は点ラベルに乗る
レイアウトは、直感とスキーマが別れ道になる場所です。ユーザーが Excel でラベルをドラッグすると、手動位置はそのラベルの <c:dLbl> の中に <c:layout> として、idx の直後に保存されます。CT_DLbls には layout の子はありません。だから全体の配置を意図して <c:dLbls> の真下に書くジェネレータは、バリデーターが不正な子として旗を立てるファイルを作ります。Excel はそのファイルを開き、誰も頼んでいないことをします。レイアウトを最初のラベルだけに適用し、idx 0 の点レイアウトが置くはずの場所に置き、残りのラベルの幅を誤読します。37.7 のラベル幅が 52.3 として返ってくるのです。チャートはほぼ正しく見えるので、この間違いはレビューでも見逃しやすい
拡張からコピーしたレイアウトは、ワークブック全体を読めなくできます。Excel は c15:layout をラベルの拡張リストの中に保持することもあります。c15 接頭辞は、断片ではなく、それを包む <c:ext> の上で宣言されています。ローカル名でレイアウトを探すコードはこの要素も拾い、<c:dLbls> に貼り付けると宣言なしの接頭辞が運ばれます。再構築されたチャートパートは未宣言の名前空間接頭辞を含み、名前空間を理解する XML パーサーはどれもロードしません。レイアウトの照合は名前空間と、直接の子という位置で行い、ローカル名だけでは決して行わないでください
HotXLS はラベルのレイアウトを、<c:dLbls> か <c:dLbl> の直接の <c:layout> 子からだけ取り、拡張の中からは決して取りません。点インデックスをキーに、一字一句そのまま保持します。LabelLayoutXml は最初のラベルのレイアウトを保持します。<c:dLbls> の真下に見つかった迷子のレイアウトも含みます。Excel がそう解釈するからです。PointLabelLayoutCount、PointLabelLayoutIndexes、PointLabelLayoutXmls が残りを列挙します。チャートを再生成するとき、そのレイアウトは idx 0 の <c:dLbl> に載り、他のレイアウトはそれぞれ自分のインデックスの <c:dLbl> に載ります。各レイアウトはその点の範囲上書きとテキストと 1 つのラベルに統合されます
procedure ReportLabelLayouts(const FileName: WideString);
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Series: TXLSXChartSeries;
C, S, I: Integer;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open(FileName) <> 1 then
Exit;
Sheet := Book.Sheets[1]; // シートインデックスは 1 起点
for C := 0 to Sheet.Charts.Count - 1 do
for S := 0 to Sheet.Charts[C].SeriesCount - 1 do
begin
Series := Sheet.Charts[C].Series[S];
Writeln('chart ', C, ' series ', S,
' label range: ', Series.DataLabelsRange,
' cached texts: ', Series.DataLabelsCache.Count);
if Series.LabelLayoutXml <> '' then
Writeln(' first label has a manual layout');
for I := 0 to Series.PointLabelLayoutCount - 1 do
Writeln(' point ', Series.PointLabelLayoutIndexes[I],
' layout: ', Series.PointLabelLayoutXmls[I]);
end;
finally
Book.Free;
end;
end;
Excel は点ラベルをラベル全体として読みます。だから 1 つとして書けば、手動位置を同じ <c:dLbl> に載せて、S1; 2; 20 のテキストも、ずれた最初のラベルも、チャートに入りません。HotXLS では、そのモデルが DataLabelsRange と、点の上書き呼び出しと、保存されたレイアウトのプロパティの背後に座っています