AnalysisOutput

Writes every analysis sheet of a generated linelist. Create takes the setup worksheet that holds the analysis definition ListObjects and the linelist facade the sheets are written into. WriteAnalysis walks each analysis scope -- normal, time series, spatial and spatio-temporal -- builds the cross-tables from their table specifications, writes the formulas, records the chart metadata in AnaTabIds and draws the charts. TransObject answers the translation provider, and entries filed along the way leave through HasCheckings and CheckingValues.

THE CLASS OWNS THE STATE ITS OWN WORK NEEDS

WriteAnalysis opens an ApplicationState of its own, turns screen updating, alerts, automatic calculation, animations and events off for the length of the build, and restores on the way out -- on the error path too. The sheets are therefore written under the state the analysis needs whoever calls it, and a caller that keeps its own scope, which is what clickGenerate does, is not disturbed: the snapshot taken here is the caller's busy state, so restoring puts that back rather than the pre-generation values.

A SECTION IS TOLD TO THE LOOP

The date bounds and the navigation dropdown of a temporal section are held by a TemporalSection. The loop closes the section it holds when it meets a table that starts a new one, and closes the last one after the loop. Nothing reads the row below to decide where a section ends.

Depends on: Linelist, LinelistSpecs, LLFormat, TranslationObject, TableSpecs, CrossTable, CrossTableFormula, Graphs, GraphSeries, TimeSeriesGraphs, SeriesBuffer, AnaTabIds, TemporalSection, ApplicationState, Buttons, Passwords, DropdownLists, HiddenNames, LLdictionary, LLVariables, FormulaData, Checking, BetterArray, ListObject names in the setup worksheet, Checking support

Version: 2026-08-12

Instantiation

Create #

create

Create an AnalysisOutput orchestrator bound to a setup worksheet and output linelist.

Signature:

Public Function Create(ByVal specSh As Worksheet, _
                       ByVal ll As Linelist) As AnalysisOutput

Factory method that creates a new AnalysisOutput instance tied to the given setup worksheet and output linelist. The setup worksheet holds the ListObjects that define which tables (global summary, univariate, bivariate, time series, spatial, spatio-temporal) should be generated. The linelist is the output workbook facade where analysis sheets are created and populated. Both parameters are validated before construction.

Parameters:

  • specSh: Worksheet. The setup worksheet holding the analysis ListObjects (Tab_Global_Summary, Tab_Univariate_Analysis, Tab_Bivariate_Analysis, Tab_TimeSeries_Analysis, Tab_Spatial_Analysis, Tab_SpatioTemporal_Analysis).
  • ll: Linelist. The output linelist workbook facade where analysis sheets will be created.

Returns: AnalysisOutput. A fully initialised orchestrator instance ready for use.

Throws:

  • InvalidArgument When the specifications worksheet is Nothing.
  • InvalidArgument When the linelist is Nothing.

Orchestration

WriteAnalysis #

write-analysis

Write all analysis tables, formulas and charts to the output.

Signature:

Public Sub WriteAnalysis(Optional ByVal part As Byte = AnalysisBuildStageAll)

Builds the four analysis sheets in order: normal (global summary, univariate and bivariate), time series, spatial and spatio-temporal. The stage says how far to go, and every sheet up to that point is finished, protected and hidden when it stayed empty.

The freeze-pane split of all four sheets runs at the end, in one pass. Activating a worksheet forces a repaint and a scroll sync that screen updating does not suppress, and it used to happen in the middle of every scope.

Parameters:

  • part: Optional Byte. A constant from the AnalysisBuildStage enum. Defaults to AnalysisBuildStageAll, which builds the four sheets. Any other value stops after the stage it names.

Remarks:

  • This is what opens and restores the application state the build runs under: see the note at the top of this class. A raise inside the build restores it before the error travels on, so no failure leaves Excel with a frozen screen.

Throws:

  • Whatever the build raises, after the application state is restored.

Checkings

TablesWritten #

tables-written

How many analysis tables this build put on a sheet

Signature:

Public Property Get TablesWritten() As Long

Returns: Long. The number of tables written.


GraphsWritten #

graphs-written

How many charts this build put on a sheet

Signature:

Public Property Get GraphsWritten() As Long

Returns: Long. The number of charts written.


HasCheckings #

has-checkings

Whether anything was reported during the build

Signature:

Public Property Get HasCheckings() As Boolean

Returns: Boolean. True when at least one entry was filed.


CheckingValues #

checking-values

The report of this build

Signature:

Public Property Get CheckingValues() As Checking

Returns: Checking. The store, or Nothing when nothing was filed.


Internal members (not exported)

Internal Properties

Seal #

seal

Seal the instance against further setup writes.

Signature:

Public Sub Seal()

Properties used during factory construction and internal wiring.


GuardNotSealed #

guard-not-sealed

Guard a setup setter against post-creation writes.

Signature:

Private Sub GuardNotSealed(ByVal propName As String)

Parameters:


OutputLL #

output-ll-get

Output linelist facade.

Signature:

Public Property Get OutputLL() As Linelist

Returns the Linelist reference representing the output workbook where analysis worksheets are created and populated.

Returns: Linelist. The output linelist workbook facade.


OutputLL #

output-ll-set

Assign the output linelist facade.

Signature:

Public Property Set OutputLL(ByVal ll As Linelist)

Parameters:


Wksh #

wksh-get

Setup worksheet reference.

Signature:

Public Property Get Wksh() As Worksheet

Returns the setup worksheet holding the analysis definition ListObjects.

Returns: Worksheet. The setup worksheet holding the analysis ListObjects.


Wksh #

wksh-set

Assign the setup worksheet reference.

Signature:

Public Property Set Wksh(ByVal specSh As Worksheet)

Parameters:


Dependency Properties

LinelistData #

linelist-data

Linelist metadata facade.

Signature:

Private Property Get LinelistData() As LinelistSpecs

Convenience accessors for frequently used collaborators retrieved through the output linelist and its metadata facade.

Convenience accessor that retrieves the LinelistSpecs object from the stored output linelist. LinelistSpecs is the central metadata facade providing access to the dictionary, translation objects, formula data, design format, passwords and sheet configuration.

Returns: LinelistSpecs. Metadata facade for the output linelist.


DesignFormat #

design-format

Design format object.

Signature:

Private Property Get DesignFormat() As LLFormat

Convenience accessor for the LLFormat design format object used to apply visual formatting (fonts, colours, borders, cell styles) to analysis worksheets, cells and chart elements. Delegates through LinelistData to the shared format singleton so all analysis output uses consistent styling from the setup workbook.

Returns: LLFormat. Design format object for the output linelist.


TransObject #

trans-object

Translation object for a given scope.

Signature:

Public Property Get TransObject(Optional ByVal scope As Byte = TranslationOfMessages) As TranslationObject

Retrieves a TranslationObject for the requested translation scope. Translation objects resolve human-readable labels, button captions, sheet names and other locale-dependent strings from the setup workbook.

Parameters:

Returns: TranslationObject. Bound to the requested translation scope.


OutputSh #

output-sh

Output worksheet for a given analysis scope.

Signature:

Private Property Get OutputSh(Optional ByVal scope As Byte = AnalysisScopeNormal) As Worksheet

Resolves the output worksheet in the linelist workbook for the given analysis scope. Each scope maps to a translated sheet name held in the messages translation table (for example "LLSHEET_Analysis" for the normal scope), so the output sheet names match the configured language.

Each sheet is resolved once and kept. Resolving one costs a translated value and a worksheet lookup, and this property is read from the sheet preparation, the button placement, the freeze panes, every table and both chart passes.

Parameters:

Returns: Worksheet. The output analysis sheet in the linelist workbook.

Throws:


OutputNames #

output-names

Metadata reader for the output worksheet of a scope.

Signature:

Private Property Get OutputNames(ByVal scope As Byte) As HiddenNames

One HiddenNames per output sheet, resolved once. Each Create walks the worksheet names and reads a comment on every tracked one, and this class used to build about ten of them per run.

Parameters:

Returns: HiddenNames. Reader bound to that scope's output sheet.


NamesObject #

names-object

Lazily-initialised analysis tracking object.

Signature:

Private Property Get NamesObject() As AnaTabIds

Lazily-initialised accessor for the AnaTabIds tracking object. AnaTabIds keeps the registry of chart metadata created during the analysis build, and the export reads it back to rebuild the charts in the exported workbook. The object is created once on first access, over the temporary analysis worksheet of the linelist.

Returns: AnaTabIds. Tracking object that records the chart metadata.

Remarks:


Scope Mapping

IsKnownScope #

is-known-scope

Whether a value is one of the four analysis scopes

Signature:

Private Function IsKnownScope(ByVal scope As Byte) As Boolean

The analysis scope, the chart scope, the sheet tag and the navigation prefix are four names for one thing. Each translation is stated here once, so no call site has to assume that two enums line up by number.

Parameters:

Returns: Boolean. True for the four scopes.


GraphScopeOf #

graph-scope-of

The chart scope that goes with an analysis scope

Signature:

Private Function GraphScopeOf(ByVal scope As Byte) As Byte

GraphScope has three members and AnalysisScope has four. The two agree by number on the first three, and this class used to pass an analysis scope straight into a chart scope parameter. Spatio-temporal has no chart scope of its own, and its charts are laid out like the time series ones.

Parameters:

Returns: Byte. A constant from the GraphScope enum.


SheetTagOf #

sheet-tag-of

The sheet_type value that goes with an analysis scope

Signature:

Private Function SheetTagOf(ByVal scope As Byte) As String

EventLinelist, LLExporter, LLImporter and the epidemiological week helper all branch on this string, so an unknown scope raises here instead of stamping an empty tag.

Parameters:

Returns: String. The sheet_type value.

Throws:


GoToPrefixOf #

goto-prefix-of

The navigation prefix that goes with an analysis scope

Signature:

Private Function GoToPrefixOf(ByVal scope As Byte) As String

Parameters:

Returns: String. The prefix every navigation name of that sheet carries.


Application Helpers

AppScope #

app-scope

The application state this class builds under.

Signature:

Private Property Get AppScope() As ApplicationState

Built on first read and kept for the life of the instance. The snapshot is taken when the object is built, which is inside whatever state the caller was already in, so restoring hands that state back rather than guessing at one.

Returns: ApplicationState. Bound to the running Excel Application.

Remarks:


SplitOutputSheet #

split-output-sheet

Apply the freeze-pane split to an analysis output worksheet.

Signature:

Private Sub SplitOutputSheet(ByVal scope As Byte)

Freezes the header rows and the left navigation columns so they stay visible while the user scrolls through the tables. The row split is 2 for the normal and spatial scopes and 3 for the time series and spatio-temporal ones, which carry an extra row for the header dropdown. The column split is always 3 and the zoom is 80%.

Parameters:

Remarks:


Sheet Preparation

AddCommands #

add-commands

Add the filter/update command button to an analysis output worksheet.

Signature:

Private Sub AddCommands(ByVal scope As Byte)

Places the command button that runs either the standard filter-update macro or the spatial one, depending on the scope. The button sits at row 2 column 2, is styled with the design format and is labelled with the translated SHP_Filter shape name. Exits early when the linelist is built from a template, because the template already carries the buttons.

This runs once per scope, from PrepareSheet, after the sheet format has been applied. It used to run a second time at the end of each scope; Buttons.Add short-circuits on an existing shape, so the second call stacked nothing and cost a Buttons instance, a shape scan, a translation and a format per scope. What the second call did lose was its own diagnostics, because the instance was created inside it and dropped.

Parameters:


StampSheetType #

stamp-sheet-type

Write the sheet_type tag of an analysis output worksheet

Signature:

Private Sub StampSheetType(ByVal scope As Byte)

The tag is what every runtime reader of the linelist uses to tell one analysis sheet from another, so a sheet that ends up empty gets it too.

Parameters:


PrepareSheet #

prepare-sheet

Initialise an analysis output worksheet before any table is written.

Signature:

Private Sub PrepareSheet(ByVal scope As Byte)

Four steps: apply the AllAnalysisSheet base format to the whole worksheet, stamp the sheet_type tag, place the ComputedOnFiltered() warning formula at row 2 column 5 with the warning format, and add the command button.

The freeze-pane split is not part of this. Activating a worksheet forces a repaint that screen updating does not suppress, so the four splits are applied in one pass at the end of the build.

Parameters:


GoTo Dropdown Helpers

AddGoTo #

add-goto

Create a data-validation dropdown for section, header or graph navigation.

Signature:

Private Sub AddGoTo(ByVal cellRng As Range, _
                    ByVal dropArray As BetterArray, _
                    ByVal goToLabel As String, _
                    Optional ByVal goToPrefix As String = "ua_", _
                    Optional ByVal goToSuffix As String = vbNullString)

Creates a data-validation dropdown in one cell that navigates to a named section, header or graph on the analysis sheet. Selecting an entry runs the GoTo macro that scrolls to the matching named range. The cell is formatted as a selection dropdown, its initial value is the label, and it is given a workbook-level name.

An empty list leaves the cell without a dropdown and still names it. EventLinelist reads those names without guarding, and the one entry that used to keep the normal sheet's list non-empty was a dropdown entry that navigated nowhere.

Parameters:


Temporal Section Helpers

WriteSectionBounds #

write-section-bounds

Write the two date bounds of a temporal section

Signature:

Private Sub WriteSectionBounds(ByVal outsh As Worksheet, ByVal secId As String, _
                               ByVal minFormula As String, ByVal maxFormula As String)

Writing out a temporal section once the loop has finished with it.

MIN_MIN_DATE_ and MAX_MAX_DATE_ are created by CrossTable in the new-section branch of its temporal header block, so a section whose anchor table failed leaves them absent and the write raises 1004. The absence is filed as a warning and the rest of the section is kept.

Parameters:


NameSpatioTemporalColumn #

name-spatio-temporal-column

Name the formula column that spans a spatio-temporal section

Signature:

Private Sub NameSpatioTemporalColumn(ByVal outsh As Worksheet, ByVal secId As String, _
                                     ByVal lastTabId As String)

GeoModule reads SPT_FORMULA_COLUMN_ at runtime. It spans from the column categories of the first table of the section to those of the last one, so it can only be named once the section is finished.

Parameters:


CloseTemporalSection #

close-temporal-section

Finish a temporal section and write everything it held

Signature:

Private Sub CloseTemporalSection(ByVal outsh As Worksheet, _
                                 ByVal section As TemporalSection, _
                                 ByVal scope As Byte, _
                                 ByVal goToHeaderLabel As String)

Writes the two date bounds, adds the header dropdown two rows under the section anchor, and names the spatio-temporal formula column. Does nothing when no section is open, so the caller can call it at every section boundary and once more after the loop.

Parameters:


AddSectionGoTo #

add-section-go-to

Hang the header dropdown of one section under its anchor

Signature:

Private Sub AddSectionGoTo(ByVal outsh As Worksheet, ByVal secId As String, _
                           ByVal headers As BetterArray, ByVal scope As Byte, _
                           ByVal goToHeaderLabel As String)

The anchor is SECTION_, which CrossTable writes only in its new-section branch, so a section whose anchor table failed has none. That failure used to end CloseTemporalSection, which took NameSpatioTemporalColumn with it and left SPT_FORMULA_COLUMN_ uncreated -- a name GeoModule reads at runtime in the delivered linelist. The two are guarded apart now, the way WriteSectionBounds already was.

Parameters:


NameGraphStart #

name-graph-start

Name the cell the charts of a temporal sheet start at

Signature:

Private Sub NameGraphStart(ByVal outsh As Worksheet, ByVal secId As String, _
                           ByVal scope As Byte)

The charts go five rows under the last table of the sheet. Both chart passes read this name, and it is created once the tables are done.

Parameters:


Table Creation

WriteTable #

write-table

Iterate setup ListObject rows and build cross-tables on the output sheet.

Signature:

Private Sub WriteTable(ByVal loName As String, _
                       ByVal sectionsList As BetterArray, _
                       ByVal scope As Byte)

Core table-creation loop and supporting write logic.

Core table-creation loop. Walks every data row of the setup ListObject named by loName and, for each valid row, creates a TableSpecs, builds a CrossTable on the output sheet, applies the formulas, formats the table and, for a row that asks for one, creates a chart. For the time series and spatio-temporal scopes it also carries the open TemporalSection: a row that starts a new section closes the one before it, and the last section is closed after the loop.

Each row runs inside a per-table error handler, so one malformed row is skipped and named in the generation report while the rest of the scope is built.

Parameters:

Remarks:


ChartAnchorOf #

chart-anchor-of

The cell a table's chart hangs from.

Signature:

Private Function ChartAnchorOf(ByVal tabl As CrossTable, _
                               ByVal outsh As Worksheet) As Range

Two rows above the table and two columns past its last one, which is where the chart used to be placed inline. A spatial table carries two more columns of controls, so its chart starts further right. The table's own geometry is read from the instance, which holds it once Build has run, so this answers the same cell whether it is asked during the table pass or after it.

Parameters:

Returns: Range. The anchor cell.


ReserveChartRows #

reserve-chart-rows

Hold the rows a deferred chart will cover.

Signature:

Private Sub ReserveChartRows(ByVal tabl As CrossTable, ByVal outsh As Worksheet, _
                             ByVal scope As Byte)

The next table is placed below the charts of the tables above it, and those charts are no longer there to be measured when the placing happens. Graphs answers how tall the chart will be in rows of this sheet, and the table keeps the line.

Parameters:


DrawPendingCharts #

draw-pending-charts

Draw every chart the table pass put off, in one pass.

Signature:

Private Sub DrawPendingCharts(ByVal pending As Collection, ByVal outsh As Worksheet, _
                              ByVal namObj As AnaTabIds, ByVal scope As Byte)

The charts used to be drawn one per table, in the middle of writing the tables. Excel 16.112 dies on the FIRST of them: the table before it is complete, the chart takes both its series, and the host falls over on the next call. Drawing them together keeps every chart operation out of the table writing, which is the thing being tested.

A table carries everything a chart needs -- its own geometry and its specification -- so the pass holds cross-tables and reads the rest back here.

Each chart is guarded on its own. The inline block sat inside the per-table handler, so a chart that failed cost its table and nothing more; the same has to be true here, or one bad chart would cost every chart after it.

Parameters:


Graph Creation

AddGraphSeries #

add-graph-series

Add one series to a chart and register it for the export

Signature:

Private Sub AddGraphSeries(ByVal gr As Graphs, ByVal namObj As AnaTabIds, _
                           ByVal scope As Byte, ByVal graphId As String, _
                           ByVal seriesName As String, ByVal seriesType As String, _
                           ByVal seriesPos As String, ByVal seriesLabel As String, _
                           ByVal seriesColumnLabel As String, _
                           ByVal hardCodeLabels As Boolean, _
                           ByVal outRangeAddress As String, _
                           Optional ByVal prefix As String = vbNullString, _
                           Optional ByVal prefixOnly As Boolean = False)

The two chart passes that run after the tables of a sheet are written, and the two steps they share.

The chart and the registry have to be told the same thing about a series, and three places told them separately. AnaTabIds.AddGraphInfo takes nine required arguments; one of those three left the ninth out, which is a compile fault in an early-bound call, so the whole file stopped compiling. One call site cannot make that mistake twice.

Parameters:


FinishGraph #

finish-graph

Format a chart, register its look and harvest what it reported

Signature:

Private Sub FinishGraph(ByVal gr As Graphs, ByVal namObj As AnaTabIds, _
                        ByVal scope As Byte, ByVal graphId As String, _
                        ByVal catTitle As String, ByVal valuesTitle As String, _
                        ByVal hardCodeLabels As Boolean, _
                        ByVal heightFactor As Long, ByVal plotTitle As String)

The second half every chart shares. The chart scope comes from the analysis scope through GraphScopeOf, so a chart is never formatted against a number that happens to line up.

Parameters:


GraphAnchor #

graph-anchor

The cell the charts of a sheet start at

Signature:

Private Function GraphAnchor(ByVal outsh As Worksheet, ByVal anchorName As String) As Range

The name is created after the last table of the sheet is written, so a scope whose tables all failed does not carry it. Answering Nothing is what lets the caller give up on the charts rather than end the whole build.

Parameters:

Returns: Range. The anchor cell, or Nothing when the name is absent.


WriteTimeSeriesGraphs #

write-time-series-graphs

Build all time series charts from the graph definition ListObjects.

Signature:

Private Sub WriteTimeSeriesGraphs(ByVal graphLoName As String, _
                                  ByVal graphTitleLoName As String, _
                                  ByVal tabLoName As String, _
                                  ByVal graphsList As BetterArray)

Reads three setup ListObjects: the graph definition table, the graph title table and the time series table. TimeSeriesGraphs resolves them into multi-series graphs with mixed chart types and separate axis positions. Each graph is then created, its series added and registered, formatted and given its title cell, and the anchor moves down fifty rows. A graph dropdown is placed at row 3 column 3 at the end.

Parameters:

Remarks:


WriteSpatioTemporalGraph #

write-spatio-temporal-graph

Build the spatio-temporal charts from the table definition ListObject.

Signature:

Private Sub WriteSpatioTemporalGraph(ByVal loName As String, _
                                     ByVal graphsList As BetterArray)

Walks the same setup ListObject the spatio-temporal tables were built from and creates one chart per row that asks for one. The plot title is the translated graph label and the value of the table's start column. Charts are spaced fifty rows apart, and a graph dropdown is placed at row 3 column 3 at the end.

Parameters:

Remarks:


Orchestration

ScopeHasRows #

scope-has-rows

Whether any setup ListObject of a scope carries a row

Signature:

Private Function ScopeHasRows(ByVal loNames As BetterArray) As Boolean

Top-level entry point that builds the four analysis output worksheets.

Formatting an analysis sheet writes a font size and a column width over all sixteen thousand columns, and a scope with no row is hidden at the end of its own build anyway.

Parameters:

Returns: Boolean. True when at least one of them has a data row.


BuildScope #

build-scope

Build one analysis sheet from end to end

Signature:

Private Sub BuildScope(ByVal scope As Byte, ByVal loNames As BetterArray, _
                       ByVal buildGraphs As Boolean)

The four scopes were four copies of the same twenty lines. Each one prepares its sheet, writes its tables, adds its charts, hangs the section dropdown at row 2 column 3, protects the sheet and hides it when nothing was written.

Parameters:


ScopeTables #

scope-tables

The setup ListObjects one scope is built from

Signature:

Private Function ScopeTables(ByVal scope As Byte) As BetterArray

Parameters:

Returns: BetterArray. The ListObject names, in the order they are written.


Checkings

LogInfo #

log-info

Record one entry in this class's own checking store

Signature:

Private Sub LogInfo(ByVal label As String, Optional ByVal scope As Byte = checkingSuccess)

The key names this class. Every collaborator of a build files its entries into one report, Checking.Add raises on a duplicate key, and a bare counter starting at zero is what each of them used to write.

Parameters:


HarvestCollaboratorCheckings #

harvest-collaborator-checkings

Fold a collaborator's entries into the report of this build

Signature:

Private Sub HarvestCollaboratorCheckings(ByVal collaboratorChecks As Checking)

Parameters:


LogMilestone #

log-milestone

File one entry for a table or a chart put on a sheet

Signature:

Private Sub LogMilestone(ByVal label As String)

A linelist carries tens of analysis tables and charts, so these entries go to the __check worksheet with the problems. They are what shows the analysis phase did work, where a clean phase used to look the same whether it built forty tables or none.

Parameters:


Error Handling

ThrowError #

throw-error

Raise a project error with standardised source.

Signature:

Private Sub ThrowError(ByVal errNumber As Long, ByVal message As String)

Wrapper around Err.Raise that standardises the source to CLASS_NAME, providing a consistent stack trace across all methods in this class.

Parameters:

Throws:


Used in (22 file(s))