Articolo tecnico

Gruppi di assi secondari nei chart BIFF8 Delphi con HotXLS

HotXLS scrive i gruppi di assi secondari dei chart BIFF8 emettendo un secondo blocco AxisParent, non aggiungendo un secondo chart group dopo gli assi. Nel chart substream Classic XLS ogni chart group, ChartFormat, record del tipo di chart e CrtLink, vive dentro il proprio blocco axis-group, e ogni serie si collega a uno di essi tramite SerToCrt. Invertite questo nesting e non esiste alcun secondo chart group a cui una serie possa collegarsi, indipendentemente da quanti record di asse emettiate

Perché un secondo chart group dopo gli assi non si collega a nulla?

La grammatica contiene tutta la risposta, ed è una riga di ABNF. La regola CHARTFOMATS in [MS-XLS] 2.1.7.20.1 dice AxesUsed 1*2AXISPARENT e poi esplicita AXISPARENT = AxisParent Begin Pos [AXES] 1*4CRT End. Leggete insieme le due produzioni e la forma diventa chiara: i chart group sono figli di un axis group, non suoi fratelli. Un chart substream con due coppie di assi e un chart group finale non è un chart a doppio asse con una stranezza di layout; è un chart con un solo chart group e un insieme di record di asse orfani. Questo conta perché SerToCrt ($1045), che si trova nel blocco SERIESFORMAT, contiene un indice zero-based del chart group, non un indice dell'asse. Scrivere crt = 1 quando esiste un solo blocco CRT punta la serie a un chart group mai emesso. L'intuizione che trae in inganno è il nome del record: AXESUSED ($1046) sembra contare gli assi, quindi la mossa naturale è emettere più assi. Conta i gruppi di assi, e ogni axis group porta con sé una plot area completa e un chart group

Marcare una serie per il gruppo di assi secondario

Dal lato HotXLS tutto si riduce a un Boolean. TXLSChartSeriesInfo contiene un campo SecondaryAxis, e impostarlo su una serie dell'array passato a TXLSWorksheets.AddChartSheet porta l'intero builder in modalità a due gruppi. Non esiste una chiamata separata "enable secondary axis" né un parametro per il numero di assi, perché il conteggio è deducibile: se una serie qualsiasi vuole il gruppo secondario, al chart ne servono due

var
  Wb: TXLSWorkbook;
  Series: array [0..1] of TXLSChartSeriesInfo;
begin
  Wb := TXLSWorkbook.Create;
  try
    Wb.Sheets.Add.Name := 'Data';
    // ... riempire A1:C12 con categorie, ricavi e margine ...

    Series[0] := Default(TXLSChartSeriesInfo);   // non usare mai FillChar su questo record
    Series[0].Name := 'Revenue';
    Series[0].Categories := 'Data!$A$1:$A$12';
    Series[0].Values := 'Data!$B$1:$B$12';

    Series[1] := Default(TXLSChartSeriesInfo);
    Series[1].Name := 'Margin';
    Series[1].Categories := 'Data!$A$1:$A$12';
    Series[1].Values := 'Data!$C$1:$C$12';
    Series[1].SecondaryAxis := True;             // AXESUSED diventa 2

    Wb.Sheets.AddChartSheet('Dual Axis', xlsChartTypeLine,
      'Revenue vs Margin', '', '', Series);
    Wb.SaveAs('dual-axis.xls', xlExcel97);
  finally
    Wb.Free;
  end;
end;

La riga Default(TXLSChartSeriesInfo) non è decorativa. TXLSChartSeriesInfo mescola campi managed, i nomi WideString, gli array dinamici di trendline ed error bar, con membri Boolean semplici, e Delphi garantisce l'azzeramento automatico solo dei campi managed. Lasciate SecondaryAxis non inizializzato e conterrà ciò che c'era sullo stack, il che in pratica significa che lo stesso binario produce un chart a un solo asse se eseguito da un console host e un chart a due assi sotto il test runner. Nel frattempo gli intervalli di categorie e valori vengono risolti tramite la tabella EXTERNSHEET del workbook prima che il builder li veda, la stessa macchina di indici trattata in come HotXLS classifica i link esterni BIFF SupBook e XTI, quindi un intervallo che nomina un foglio sconosciuto degrada a un placeholder BRAI vuoto invece di far fallire la build

Cosa emette HotXLS quando una serie è secondaria

L'emitter cambia forma, non solo un valore. Senza serie secondarie, AXESUSED contiene 1 (oppure 0 per pie e 3D pie, che non hanno alcun axis group) e segue un solo blocco AxisParent. Con una serie secondaria, AXESUSED contiene 2 e il builder esegue il blocco due volte, con iax, la prima word del payload da 18 byte di AxisParent ($1041), impostata prima a 0 e poi a 1. Ogni passaggio emette Pos, l'asse delle categorie e quello dei valori (Axis, $101D), il marker PlotArea ($1035), un Frame predefinito, poi ChartFormat ($1014), il record del tipo di chart, CrtLink ($1022) e due marker End per chiudere chart group e axis group. La serie secondaria si collega quindi con SerToCrt crt = 1, mentre quella primaria conserva crt = 0. Un record non viene deliberatamente duplicato: la legend viene emessa solo nel primo gruppo, perché Excel assegna a un chart una sola legend indipendentemente dal numero di axis group. Altre due proprietà vanno dichiarate chiaramente. Ristrutturare l'emitter per mettere il chart group dentro il blocco axis-parent non ha cambiato l'output dei chart ordinari: senza serie secondarie il substream è byte-identico alla versione precedente, dato che parametrizzare AddAxisParent con iax = 0 è esattamente il vecchio percorso. Inoltre il builder continua a emettere una coppia completa di assi per gruppo, quindi un gruppo secondario arriva sempre con il proprio asse delle categorie anche quando vi interessa solo la scala dei valori

Come recupera l'ispezione del chart il binding del gruppo di assi?

La lettura avviene in due passaggi sulla lista dei record, e deve essere così perché AXESUSED arriva prima dei blocchi che descrive. Il primo passaggio cerca solo $1046 e legge la sua prima word come conteggio degli axis group. Quel valore parte da 1 e viene solo aumentato: HotXLS prende il massimo tra il conteggio corrente e quello dichiarato, così un AXESUSED malformato o duplicato non può far regredire un chart già visto dichiarare due gruppi. Il secondo passaggio segue il gruppo di assi corrente, aggiornandolo a ogni AxisParent, e assegna quell'indice a ogni record Axis incontrato fino all'apparizione del successivo AxisParent

var
  Model: TXLSChartModel;
  i: Integer;
begin
  // Sheets[1] è il worksheet dati, Sheets[2] il chart sheet
  Model := Wb.Sheets[2]._Chart.GetChartModel;
  try
    if Model.AxisGroupCount = 2 then
      Writeln('AXESUSED declares a secondary axis group');
    for i := 0 to Model.AxisCount - 1 do
      Writeln('axis ', i, ' group ', Model.GetAxis(i).AxisGroup);
    for i := 0 to Model.SeriesCount - 1 do
      Writeln('series ', i, ' chart group ', Model.GetSeries(i).ChartGroup);
  finally
    Model.Free;
  end;
end;

Vale la pena nominare due limiti. TXLSChartModel.AxisGroupCount riporta ciò che il file dichiara, non quanti blocchi AxisParent siano stati effettivamente trovati; un file che dice 2 e ne contiene uno riporterà 2, e AxisCount è il punto in cui ve ne accorgete. Inoltre TXLSChartAxis.AxisGroup è un contrassegno posizionale: registra in quale blocco è stato letto l'asse, l'unica informazione che il formato offre. Lato serie, il decode di SerToCrt è abilitato solo dentro un blocco Series, perché lo stesso record id compare in contesti che non rappresentano un binding di serie e un decoder senza guardia sovrascriverebbe volentieri la serie sbagliata

Verificare gli assi secondari senza un file Excel reale

Per questa verifica non è servito alcun file Excel con un asse secondario, ed è la parte utile della storia. Il decode strutturale è una proprietà della sequenza di record, quindi una sequenza sintetizzata lo dimostra esattamente quanto una catturata. La regressione costruisce AXESUSED con payload 2, poi due blocchi AxisParent che contengono ciascuno un asse delle categorie e uno dei valori, e verifica che il modello torni con AxisGroupCount = 2, quattro assi marcati 0, 0, 1, 1 e i tipi di asse attesi sulla seconda coppia

// Verifica strutturale senza coinvolgere alcun file Excel
Chart := TXLSCustomChart.Create(nil, $0600);
try
  AddWordRecord($1046, [2]);   // AXESUSED: due axis group
  AddAxisParentGroup(0);       // AxisParent iax=0 + Begin + 2 Axis + End
  AddAxisParentGroup(1);       // AxisParent iax=1 + Begin + 2 Axis + End

  Model := Chart.GetChartModel;
  Assert.AreEqual(2, Model.AxisGroupCount);
  Assert.AreEqual(4, Model.AxisCount);
  Assert.AreEqual(0, Model.GetAxis(1).AxisGroup);
  Assert.AreEqual(1, Model.GetAxis(2).AxisGroup);
finally
  Model.Free;
  Chart.Free;
end;

Se sintetizzate record da soli, tenete presenti due note pratiche. TXLSCustomChart.AddData(RecID, Len, nil) dereferenzia il payload quando Len è diverso da zero, quindi i marker Begin ($1033) ed End ($1034) devono essere aggiunti con lunghezza zero, non con un nil blob e una length vecchia. Inoltre una sequenza sintetica dimostra il decoder, mai l'accettazione del vostro output da parte di Excel: il lato scrittura è stato strutturato dall'ABNF e poi controllato con un round-trip attraverso GetChartModel, verificando i contrassegni per asse 0/0/1/1 e i chart group per serie 0/1, con il percorso byte-identico senza secondario come rete di sicurezza. È lo stesso atteggiamento conservativo su cui si basa il resto del supporto HotXLS a chart, immagini e drawing in Delphi: decodificare ciò che dicono i record e rifiutarsi di indovinare un layout binario che non si è letto nella specifica. La suite Delphi completa ha eseguito 1650 test su 1650 su Win32 e Win64 dopo l'introduzione del lato scrittura

Parametri della scena Chart3d e il tranello fAuto

Due dettagli adiacenti colpiscono chi va oltre il chart predefinito. Il primo è Chart3d ($103A, [MS-XLS] 2.4.46), un payload piatto da 14 byte emesso dentro il chart group per le varianti 3D: anRot (rotazione, da 0 a 360), anElev (elevazione, signed, da -90 a 90), pcDist (distanza prospettica, da 0 a 100, ignorata se fPerspective non è impostato), pcHeight e pcDepth (percentuale della larghezza del chart, da 5 a 500), pcGap (da 0 a 500) e un grbit i cui bit sono fPerspective $0001, fCluster $0002, fAutoscale $0004, f3DScaling $0010 e f2DWalls $0020. La specifica aggiunge vincoli che il layout del record non applica per voi: su un bar chart trasposto anRot e anElev non devono superare 44, e su un pie chart anElev non deve essere negativo

Il secondo è il bit fAuto, quello che produce il bug report "i miei colori sono stati ignorati". LineFormat ($1007), AreaFormat ($100A) e MarkerFormat ($1009) contengono tutti fAuto nel bit 0 del loro grbit, e quando quel bit è impostato Excel applica il proprio stile automatico e tratta i valori RGB espliciti, lo stile della linea, la forma e la dimensione del marker accanto come decorazione. Qualsiasi emitter che scriva uno stile di serie personalizzato deve azzerare il bit 0; gli emitter predefiniti lo tengono impostato proprio per lasciare che Excel scelga la palette. Se state modificando un workbook esistente invece di costruirne uno, le regole di preservazione cambiano ancora e sono trattate in modificare chart Excel senza perdere il ChartML conservato

I gruppi di assi secondari, il binding SerToCrt e il chart model tipizzato mostrato qui sono inclusi nel componente spreadsheet Delphi HotXLS per Delphi e C++Builder, che legge e scrive chart BIFF8 senza Excel installato; la pagina prodotto contiene il riferimento completo ai record chart e l'elenco degli overload di AddChartSheet