HotXLS는 Excel Waterfall과 Treemap 차트를 독립된 cx:chartSpace 파트, 즉 콘텐츠 타입 application/vnd.ms-office.chartex+xml을 지닌 xl/charts/chartExN.xml로 쓰고, mc:AlternateContent 래퍼로 앵커를 걸어 Excel 2016 이상은 차트를 렌더링하고 오래된 리더는 폴백 셰이프를 받게 합니다. xlsxChartWaterfall이나 xlsxChartTreemap을 고르면 전체 파트 파이프라인이 따라옵니다
이 문제가 스스로 알리는 방식은 대개 서포트 티켓입니다. 고객이 재무팀이 Excel 2019에서 만든 워크북을 보내고, 여러분의 리포트 도구가 그것을 열고 저장하면 Waterfall 차트가 사라져 있습니다. 예외도, 경고도, 로그의 흔적도 없습니다. 차트가 그저 출력에 없을 뿐입니다. 클래식 차트 코드 경로에서 찾으러 간다면 한참을 찾게 될 것입니다. 차트는 애초에 그곳에 없었기 때문입니다
Waterfall과 Treemap 차트는 왜 c:chartSpace에 없을까요?
마이크로소프트가 거기에 둘 수 없었기 때문입니다. ECMA-376 Part 1 §21.2의 DrawingML 차트 스키마는 c:barChart, c:lineChart, c:pieChart 등을 폐쇄적 선택으로 열거하므로, Excel 2016에서 waterfall, treemap, sunburst, funnel을 추가한다는 것은 기존의 모든 리더가 의존하는 스키마를 깨뜨린다는 뜻이었습니다. 새 타입은 대신 병렬 문서로 들어갔습니다. 2014 chartex 네임스페이스 http://schemas.microsoft.com/office/drawing/2014/chartex의 cx:chartSpace 파트로, 클래식 차트 옆에 저장되지만 구조적으로는 무관합니다. 드로잉 쪽은 ECMA-376 Part 3의 호환성 메커니즘 mc:AlternateContent로 두 세계를 꿰맵니다. mc:Choice 분기는 Requires="cx1"을 선언하고, a:graphicData/@uri가 chartex 네임스페이스이고 페이로드가 알몸 <cx:chart r:id="rIdN"/> 참조인 xdr:graphicFrame을 지닙니다. mc:Fallback은 Excel 자신이 그 자리에 쓰는 공지가 담긴 사각형을 지닙니다. cx1을 이해하지 못하는 리더는 Part 3 계약에 따라 폴백을 취해야 합니다
관계 타입이 또 하나의 단서입니다. 클래식 차트는 표준 OOXML 차트 관계로 드로잉 파트에 매달리지만, chartEx 파트는 대신 http://schemas.microsoft.com/office/2014/relationships/chartEx를 쓰며 이는 [MS-ODRAWXML]에 문서화되어 있습니다. HotXLS는 두 세계를 상수로 노출합니다. 그 관계용 XlsxRtChartEx, 콘텐츠 타입용 XlsxCtChartEx, 앵커가 필요한 두 네임스페이스용 XlsxNsMsChartEx와 XlsxNsMsChartEx2015가 그것이며, 술어 XlsxChartIsChartEx가 저장 시 차트를 확장 경로로 라우팅합니다. 조합 차트와 보존된 클래식 차트 XML은 다른 경로로 가며, 이는 보존된 차트 XML과 조합 차트 기사에서 다룹니다
Excel이 파일을 열지 결정하는 스키마 규칙 네 가지
chartex 스키마는 클래식 것보다 네 곳에서 더 엄격하며, 그 각각은 틀리면 똑같은 쓸모없는 복구 프롬프트를 만들어냅니다. HotXLS의 BuildChartExKnownXml과 BuildChartExRelsXml이 네 가지를 모두 구워 넣지만, XML을 손으로 검사할 일이 있다면 알아 둘 가치가 있습니다
- 순서.
cx:chartData는 문서에서cx:chart보다 앞에 와야 합니다. 캐시가 플롯 영역 안의 시리즈 안에 사는 클래식 차트에서 얻은 직관과는 반대입니다. chartEx 파트에서는 데이터가 앞쪽의 자기 블록에 있고 차트 본문은 id로만 참조합니다 - 카디널리티.
cx:plotArea는cx:plotAreaRegion을 정확히 하나 담을 수 있습니다. 0도, 2도 아닙니다. 여기에는 조합 차트 이야기가 없습니다. 영역 하나와 그 안의 시리즈뿐입니다 - 타입 위치. 차트 타입은 요소 이름이 전혀 아닙니다. 마이크로소프트 chartex 스키마의
ST_SeriesLayout열거에서 토큰을 받는cx:series의layoutId속성입니다.XlsxChartExLayoutId는 HotXLS enum을 그 토큰에 매핑해'waterfall'이나'treemap'을 반환하며,XlsxChartExLayoutIdToType은 읽을 때 매핑을 거꾸로 돌립니다 - 클래식 차트가 결코 필요로 하지 않는 관계.
cx:chartData는<cx:externalData r:id="rId1"/>요소를 지니며, 그 관계는 데이터를 소유한 워크시트를 다시 가리키는xl/charts/_rels/chartExN.xml.rels에 존재해야 합니다. Excel이 그걸로cx:f범위 수식을 해석하기 때문입니다. rels 파트를 빠뜨리면 차트는 왜인지 설명 없이 빈 채로 열립니다
Delphi에서 Waterfall이나 Treemap 차트를 어떻게 추가할까요?
HotXLS에서 다른 차트를 추가하는 것과 같은 방식입니다. 확장 타입은 TXLSXChartType의 멤버이므로 TXLSXWorksheet.AddChart가 그것들을 직접 받고, 하류의 모든 것이 enum 값에 키를 겁니다. 배울 별도의 chartEx API는 없습니다
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Chart: TXLSXChart;
begin
Book := TXLSXWorkbook.Create;
try
Book.Open('cashflow.xlsx');
Sheet := Book.Sheets[0];
// 행/열 좌표의 앵커 사각형, 클래식 차트와 동일
Chart := Sheet.AddChart(xlsxChartWaterfall, 'Q3 Cash Flow',
2, 5, 20, 12);
Chart.AddSeries('Movement', 'Sheet1!$A$2:$A$9', 'Sheet1!$B$2:$B$9');
Book.SaveAs('cashflow-waterfall.xlsx');
finally
Book.Free;
end;
end;
저장 시 HotXLS는 XlsxChartIsChartEx가 True를 반환하는 것을 보고 별도의 chartEx 카운터에서 파트 이름을 할당해 xl/charts/chartEx1.xml, 그 rels 파일, 콘텐츠 타입 오버라이드, XlsxChartExAnchorPayloadXml이 만든 mc:AlternateContent 앵커를 씁니다. 독립 카운터는 중요합니다. 클래식 차트 둘과 waterfall 하나를 지닌 워크북은 chartEx3.xml이 아니라 chart1.xml, chart2.xml, chartEx1.xml을 얻습니다. Treemap은 그러면 waterfall과 두 가지 세부에서 달라지는데, 둘 다 같은 빌더가 처리합니다. 숫자 차원이 타입을 바꿉니다. waterfall의 cx:numDim type="val"은 treemap에서 type="size"가 됩니다. treemap 사각형이 크기를 플롯된 값이 아니라 면적으로 인코딩하기 때문입니다. 그리고 treemap은 대개 카테고리 계층을 원하며, 모델은 TXLSXChartSeries.CategoriesMultiLevel로 그것을 표현합니다. 그 플래그를 세우면 S.ResolveCategoryLevels가 쓰기 시점에 워크시트에서 범위를 읽어 S.CategoryLevelCount와 S.CategoryLevels[] 뒤에 레벨마다 TXLSXChartDataCache 하나로 쪼개고, BuildChartExLevelXml이 단일 cx:strDim 안에 레벨마다 cx:lvl 하나를 리프 레벨부터 내보냅니다. ECMA-376 Part 1 §21.2가 c:multiLvlStrCache에 쓰는 것과 같은 물리적 순서라서, 마크업 중 어느 것도 이어지지 않는데도 그 관례는 넘어옵니다
Chart := Sheet.AddChart(xlsxChartTreemap, 'Revenue by Region',
2, 8, 24, 16);
S := Chart.AddSeries('Revenue',
'Sheet1!$A$2:$B$40', // 열 두 개: 리프 라벨, 그다음 부모 그룹
'Sheet1!$C$2:$C$40');
S.CategoriesMultiLevel := True;
클래식 차트의 모든 가정을 깨는 캐시 모양
클래식 차트 캐시 리더를 작성해 봤다면, chartEx 파트에서 세 가지 습관이 여러분을 배신할 것입니다. 셋 모두 하나의 설계 결정에서 옵니다. chartex는 DrawingML이 자식 요소에 저장하는 것을 속성과 요소 콘텐츠에 저장합니다
- 캐시된 포인트는
<cx:pt idx="0">42</cx:pt>입니다. 값은 요소 콘텐츠입니다. 내려갈c:v자식이 없으므로v노드를 찾아 걷는 파서는 아무것도 찾지 못하고 빈 캐시를 보고합니다 ptCount와formatCode는cx:lvl의 속성입니다. 클래식c:numCache에서는 둘 다 자식 요소입니다- 여러 계층 레벨은 하나의
cx:strDim안의 여러cx:lvl요소입니다.c:multiLvlStrCache가c:strCache와 별개인 것 같은 별도의 멀티레벨 캐시 요소는 없습니다
BuildChartExLevelXml이 그 모양을 쓰고 ParseChartExXml이 읽어 되돌리며, 나가는 모든 값은 XlsxEscapeText를 거쳐 앰퍼샌드나 꺾쇠를 담은 카테고리 라벨이 왕복을 살아남습니다. 빌더가 숫자 레벨이 포맷 코드를 지니지 않을 때 무엇을 하는지도 주목할 만합니다. 속성을 떨어뜨리는 대신 formatCode="General"을 씁니다. 여기서 그 속성은 클래식 상대처럼 선택 사항이 아니기 때문입니다
타입 읽어내기를 실패시킨 네임스페이스 프리픽스
HotXLS chartEx 파서의 첫 버전은 완벽히 좋은 파일을 썼고 그것들을 모두 세로 막대 차트로 읽어냈습니다. 근본 원인은 한 줄이었고, 그 교훈은 여러분이 앞으로 작성할 어떤 OOXML 파서에도 일반화됩니다. TXMLReader.Name은 프리픽스를 포함해 문서에 쓰인 그대로의 이름을 반환합니다. 클래식 차트 파트에서는 대부분의 요소가 프리픽스 없이 도착하므로 SameText(Reader.Name, 'series')가 작동하고 여러분은 더 생각하지 않습니다. chartEx 파트에서는 모든 요소가 프리픽스를 지닙니다. 리더는 'cx:series'를 건네고, 비교는 그 모두에서 조용히 실패하며, 예외도 잘못된 XML도 없이 그저 빈 시리즈 목록과 기본값으로 폴백하는 차트 타입만 남습니다. 수정은 로컬 이름으로 비교하는 것이며, 호출 지점마다가 아니라 한 곳에서 해야 합니다
// Reader.Name은 네임스페이스 프리픽스를 지닙니다: 'series'가 아니라 'cx:series'
function IsEl(const LocalName: string): Boolean;
var
NodeName: string;
begin
NodeName := String(Reader.Name);
Result := SameText(NodeName, LocalName) or
EndsText(':' + LocalName, NodeName);
end;
ParseChartExXml은 모든 요소 검사에 그 헬퍼를 쓰고 속성에는 동등한 AttrVal 헬퍼를 씁니다. cx:autoUpdate 같은 것들도 프리픽스를 지니기 때문입니다. 시리즈 요소가 실제로 보이기 시작하면 타입은 처음 만나는 layoutId에서 돌아오고, 각 cx:series는 cx:dataId/@val을 cx:data/@id와 대조해 자기 cx:data 블록에 연결됩니다. 열었지만 수정하지 않은 차트는 대신 옛 경로를 탑니다. 원래 바이트가 FRawChartXml에서 재생되므로, 건드리지 않은 고객 파일은 들어간 그대로 나옵니다. 스파크라인 확장 기사에서 시트 레벨 확장을 지배하는 것과 같은 보수적 작성자 규율입니다
HotXLS가 이 차트들에 일부러 하지 않는 일
경계 두 개는 분명히 말할 가치가 있습니다. 둘 다 채워지길 기다리는 구멍이 아니라 선택이기 때문입니다. 내장 PDF와 SVG 렌더러는 waterfall이나 treemap 시리즈를 그리지 않습니다. 그 타입이 어떤 드로잉 분기도 맞지 않아 시리즈 렌더링을 건너뛰고 시트의 나머지는 정상적으로 내보내집니다. 내보낸 PDF에 그럴듯해 보이는 treemap을 위조하는 것은 아무것도 그리지 않는 것보다 나쁩니다. 같은 파일에 대해 Excel이 보여주는 것과 조용히 어긋나기 때문입니다. 그리고 xlsxChartWaterfall과 xlsxChartTreemap은 HotXLS가 모델에서 만드는 유이한 확장 레이아웃 둘입니다. ST_SeriesLayout은 sunburst, funnel, box-and-whisker, histogram도 정의하며, 그중 어느 것이든 담긴 워크북은 여전히 raw-XML 재생 경로로 왕복하지만, 오늘 Delphi 코드로 작성할 수는 없습니다. 클래식 차트 타입의 렌더링은 변함없으며 차트, 이미지, 드로잉 기사에서 다룹니다. chartEx 파이프라인은 Delphi와 C++Builder용 HotXLS Delphi Excel Component에 그 옆에 실려 출하됩니다