LLSpatial
Fills the spatial analysis tables of a generated linelist, orders them, and reads ranked values back out for the TopAdmin, TopPop and TopHF worksheet functions. Exists says whether the registry table is there, Update fills the tables from the data sheets, Sort orders them, and TopGeoValue and TopHFValue answer one ranked value.
This class also owns the formula rewriting of the spatial analysis events. RewriteFormulas moves a token through every formula of a range while keeping each formula's array state, ChangeAdminLevel moves one table to a new admin level, PreviousSectionLevel and MigrateSection move the formula columns of a spatio-temporal section the same way, ApplyPopulationFactor divides values by the population or restores them, and FormatPopulationFactor shows or hides the factor cells. The event dispatch stays with the callers: EventLinelist and GeoModule read what the user changed and hand the work over here, so the array-formula handling is written once.
THE CONTRACT WITH SPATIALTABLES
SpatialTables builds these tables at build time and this class fills them at run time. The two never reference each other: the contract is a set of names agreed by string convention across a workbook boundary, and both files hold their own copy of the four constants and the two name builders. That is the house rule, a class keeps every constant it needs inside itself. Change a name here and change it in SpatialTables in the same commit.
listofgeovars one row per spatial variable, "
The width of a table and the meaning of each column are SpatialTables to decide. Every column is resolved by name here, so a table that grows a column keeps working.
WHERE THE VALUES COME FROM
Update walks the HList sheets of the workbook. A column controlled by geo1 or by hf names a variable, and the filtered companion sheet of that HList carries the concatenated geo values of that variable, one column per administrative level. The distinct values of that column are the rows of the spatial table. The reading and the de-duplication happen in memory: one read of the column and a delimited index of the values already kept.
WHAT A MISSING REGISTRY MEANS
Linelist creates the spatial worksheet on every linelist, and SpatialTables creates the listofgeovars table when a spatial cross-table is defined. So a linelist with geo variables and no spatial analysis has the sheet with no table in it, and Update on such a sheet does nothing: Exists, the variable lookup and Sort all answer empty when the table is absent.
RNG_TESTINGFORMULA BELONGS TO SPATIALTABLES
SpatialTables.Prepare creates it and SpatialTables.FailedFormula reads it. This class leaves it alone.
Depends on: BetterArray, HiddenNames, Checking
Version: 2026-08-12
Factory
Create #
create
Create a new LLSpatial instance from the spatial tables worksheet
Signature:
Public Function Create(ByVal sh As Worksheet) As LLSpatial
Factory method that checks the supplied worksheet is given and is named "__spatial_tables", then returns a new instance. Called on the predeclared instance.
The listofgeovars table is left out of the check on purpose: the worksheet and the table have different lifecycles, and a linelist that carries the sheet with no table still needs an instance for the two callers that build one on every run. See the class description.
Parameters:
sh: Worksheet. The spatial tables worksheet.
Returns: LLSpatial. A fully initialised instance ready for use.
Throws:
- ProjectError.ObjectNotInitialized When the worksheet is Nothing.
- ProjectError.InvalidArgument When the worksheet name is not "__spatial_tables".
The Registry Of Spatial Variables
Exists #
exists
Check if a variable has spatial tables
Signature:
Public Function Exists(ByVal varName As String) As Boolean
Answers True when the listofgeovars table carries at least one name built on this variable. A spatial sheet with no listofgeovars table answers False.
Parameters:
varName: String. The bare variable name to look up.
Returns: Boolean. True when the variable is registered.
Update
Update #
update
Update all spatial tables from HList filtered data
Signature:
Public Sub Update()
Walks the worksheets of the workbook and works on the HList ones. For each of them it resolves the filtered companion sheet, then walks the variable columns of the table looking for a control value of geo1 or of hf. A geo1 column takes the concat column of the companion and fills the four administrative tables of the variable; an hf column fills the single facility table. The spatial sheet is recalculated at the end so the formula columns answer for the rows just written.
The control value of a variable is a hidden name of the sheet, written by
VarWriter as "
HOW THE SHEET TAG IS READ
The tag test runs through HiddenNames.QuickValue, which reads the one stored name off the worksheet. The full store is built for the HList sheets alone, because those are the sheets whose variable columns are read below. The walk used to build a store per worksheet of the workbook, and every name of an analysis sheet was walked to answer a tag that named it as no HList.
Throws:
- Any error raised while walking a sheet, after the spatial sheet has been recalculated. The caller is what puts Excel back the way it found it.
Sort
Sort #
sort
Order the spatial tables of one analysis table
Signature:
Public Sub Sort(ByVal tabId As String, Optional ByVal onAR As Boolean = False)
Finds the variable the table identifier belongs to, then orders its tables on the value column or on the attack rate column, highest first. A facility variable carries one table and a geographic one carries four.
Parameters:
tabId: String. The identifier of the analysis table.onAR: Optional Boolean. True to order on the attack rate. Defaults to False.
Spatial Formula Rewriting
RewriteFormulas #
rewrite-formulas
Replace a token in every formula of a range
Signature:
Public Sub RewriteFormulas(ByVal valuesRng As Range, ByVal oldText As String, _
ByVal newText As String)
The formula rewriting of the spatial analysis events. The caller reads what the user changed and hands the sheet, the table identifier and the admin code over; the loops that touch the formulas live here, so the array-formula handling is written once for every caller.
Walks the cells of the range and rewrites each formula that holds the old text, with the new text in its place. A plain formula is written back through Formula and an array formula through FormulaArray, decided by HasArray. Range.FormulaArray answers the ordinary formula string for a cell outside an array formula, so a read through it cannot tell the two apart: a reader built on it pushed every write through FormulaArray, which turns a plain formula into an array one and raises 1004 for any formula over 255 characters.
Parameters:
valuesRng: Range. The block of cells to rewrite.oldText: String. The text to replace.newText: String. The replacement.
ChangeAdminLevel #
change-admin-level
Move the formulas of one spatial table to a new admin level
Signature:
Public Sub ChangeAdminLevel(ByVal sh As Worksheet, ByVal tabId As String, _
ByVal newAdmin As String)
Reads the level the table stands on from its PREVIOUS_ADM_ cell, rewrites every formula of the OUTER_VALUES_ block from the old concat column to the new one, writes the new level back, and recalculates the block. The caller reads the dropdown and turns its label into an admin code, because that answer needs the geo sheet and this class holds none.
The rewrite reaches the FindTopAdmin and FindTopPop formulas of the block too, and moves the level inside their quoted variable argument with the rest. That is why EventLinelist.BareSpatialName strips the concat tag at any level: it used to know the adm1 spelling alone, so the first level change broke every ranked lookup of the table.
Parameters:
sh: Worksheet. The spatial analysis sheet.tabId: String. The identifier of the analysis table.newAdmin: String. The admin code to move to, "adm1" to "adm4" or "hf".
PreviousSectionLevel #
previous-section-level
Read the admin level a spatio-temporal section stands on
Signature:
Public Function PreviousSectionLevel(ByVal sh As Worksheet, _
ByVal rngName As String, _
ByVal tabId As String) As Long
The cell right of the level selector records the level the section's formulas read. A value outside 1 to 4 means the cell was cleared or overwritten: a rewrite on it would migrate nothing while still recording the new level, and the sheet would then be one level behind with no way back. GeoModule reads this before it unprotects the sheet, so a raise here leaves a deliberately open sheet open.
Parameters:
sh: Worksheet. The spatio-temporal analysis sheet.rngName: String. The named range of the level selector.tabId: String. The identifier of the analysis table, for the message.
Returns: Long. The recorded level, 1 to 4.
Throws:
- ProjectError.InvalidArgument When the cell reads outside 1 to 4.
MigrateSection #
migrate-section
Move the formula columns of one spatio-temporal section to a new admin level
Signature:
Public Sub MigrateSection(ByVal sh As Worksheet, ByVal rngName As String, _
ByVal tabId As String, ByVal prevAdm As Long, _
ByVal actAdm As Long)
Walks the SPT_FORMULA_COLUMN_ header row of the section. A header formula naming the selector marks a formula column, the VALUES block of that column derives from the name of its header cell, and a header cell with no name leaves its column alone -- a cell has a name only when something named it. The block is grown by two rows to reach the Total and Missing rows of its own table, which RowsCategoriesRange trims by count at build time, and their formulas move levels with the block. Every formula of the grown block moves from the old concat column to the new one, and the new level is recorded beside the selector.
THE RECALCULATION IS THE CALLER'S
The caller owns one recalculation of the sheet per validated place. It fires on every validate and reaches here only when the level changed, so a recalculation at the foot of this sub would leave the other paths dry and would run the same used range twice on the path it does reach.
Parameters:
sh: Worksheet. The spatio-temporal analysis sheet.rngName: String. The named range of the level selector.tabId: String. The identifier of the analysis table.prevAdm: Long. The level the section stands on.actAdm: Long. The level to move to.
ApplyPopulationFactor #
apply-population-factor
Divide the values of one spatial table by the population, or restore them
Signature:
Public Sub ApplyPopulationFactor(ByVal sh As Worksheet, ByVal tabId As String, _
ByVal adminName As String, _
Optional ByVal revertBack As Boolean = False)
Walks the OUTER_VALUES_ block and rewraps each formula that reads the concat column of the given admin code. The first application wraps the formula as "IFERROR(factor * formula / population cell, "")", so a row whose population cell holds 0 or nothing shows an empty cell. The division used to be written bare, and every such row showed #DIV/0!. A later application swaps the old factor for the new, and revertBack unwraps the whole thing, the bare shape an older linelist wrote included. The factor comes from the POPFACT_ cell and falls back to 100 when that cell does not read as a number. The tables are reordered on the attack rate while the division is on, and on the plain value once it is off. The factor applied is written into the POPPREVFACT_ cell, so the next call knows what to swap or unwrap.
Parameters:
sh: Worksheet. The spatial analysis sheet.tabId: String. The identifier of the analysis table.adminName: String. The admin code the formulas read, "adm1" to "adm4" or "hf".revertBack: Optional Boolean. True restores the plain formulas. Defaults to False.
FormatPopulationFactor #
format-population-factor
Show or hide the population factor cells of one spatial table
Signature:
Public Sub FormatPopulationFactor(ByVal sh As Worksheet, ByVal tabId As String, _
ByVal factorVisible As Boolean)
The factor cell and its label are white, locked and hidden while the division is off, and black, editable and readable while it is on. The caller decides which, from the dropdown the user changed.
Parameters:
sh: Worksheet. The spatial analysis sheet.tabId: String. The identifier of the analysis table.factorVisible: Boolean. True shows the factor cells.
TopGeoValue / TopHFValue
TopGeoValue #
top-geo-value
Value at a given rank from an ordered spatial table
Signature:
Public Property Get TopGeoValue(ByVal adminName As String, _
ByVal orderValue As Long, _
ByVal inputVarName As String, _
Optional ByVal tabId As String = vbNullString, _
Optional ByVal columnChoice As Byte = 1) As String
Reads the table of one variable at one administrative level and answers the value at the requested rank, either the admin unit itself or its population. Answers an empty string when the table is absent, holds no data, or is shorter than the rank asked for.
A rank below 1 used to read the header cell or the cells above the table, and the rank comes straight from the argument of a worksheet function, so =FindTopAdmin(ADM_DROPDOWN_sp1, 0, ...) in a cell answered with the internal table name. The level arrives here as the raw code; the worksheet function takes the label of the level and EventLinelist resolves the two.
Parameters:
adminName: String. Level identifier ("adm1" to "adm4", or "hf").orderValue: Long. Rank position in the ordered table, 1 for the top one.inputVarName: String. The bare variable name.tabId: Optional String. The identifier of the analysis table. Defaults to vbNullString.columnChoice: Optional Byte. 1 for the admin unit, anything else for its population. Defaults to 1.
Returns: String. The value at that rank, or an empty string.
TopHFValue #
top-hf-value
Value at a given rank from an ordered facility table
Signature:
Public Property Get TopHFValue(ByVal orderValue As Long, _
ByVal inputVarName As String, _
Optional ByVal tabId As String = vbNullString) As String
The facility table is the same shape as an administrative one under another level name, so this is that read with the level fixed. The two used to be two copies of one function, and every fix to one of them had to be made twice.
Parameters:
orderValue: Long. Rank position in the ordered table, 1 for the top one.inputVarName: String. The bare variable name.tabId: Optional String. The identifier of the analysis table. Defaults to vbNullString.
Returns: String. The facility at that rank, or an empty string.
Internal members (not exported)
Factory
Seal #
seal
Seal the instance against further setup writes.
Signature:
Public Sub Seal()
Marks the instance as sealed so setup-only setters raise if called again. Called by the factory after the last setup write.
GuardNotSealed #
Guard a setup-only setter against post-seal writes.
Signature:
Private Sub GuardNotSealed(ByVal propName As String)
Parameters:
propName: String. The property name used in the error message.
Wksh #
wksh
Spatial tables worksheet
Signature:
Public Property Get Wksh() As Worksheet
Returns: Worksheet. The backing spatial tables worksheet.
Wksh #
wksh-set
Assign the spatial tables worksheet
Signature:
Public Property Set Wksh(ByVal sh As Worksheet)
Parameters:
sh: Worksheet. The worksheet to store.
Naming Conventions
CurrentLoName #
current-lo-name
ListObject name for a spatial table
Signature:
Private Property Get CurrentLoName(ByVal varName As String, _
ByVal adminName As String) As String
Builds the conventional ListObject name for a spatial table. The pattern is "spatial_{adminName}_{varName}", for instance "spatial_adm1_cases_sp1". SpatialTables builds the same string from the same pieces.
Parameters:
varName: String. The variable name component, with its table identifier.adminName: String. The admin level component (e.g. "adm1", "hf").
Returns: String. The constructed ListObject name.
Worksheet Lookups
LoOrNothing #
lo-or-nothing
The named ListObject of the spatial sheet, or Nothing
Signature:
Private Function LoOrNothing(ByVal loName As String) As ListObject
The guarded resolvers every other section reads through. Each one declares its own local, so a failed lookup answers Nothing rather than the answer of the call before it.
Parameters:
loName: String. The ListObject name to resolve.
Returns: ListObject. The table, or Nothing when the sheet carries no such name.
ColumnBodyOrNothing #
column-body-or-nothing
The data body of one named column of a table, or Nothing
Signature:
Private Function ColumnBodyOrNothing(ByVal Lo As ListObject, _
ByVal colName As String) As Range
Parameters:
Lo: ListObject. The table to read.colName: String. The column name.
Returns: Range. The data body of that column, or Nothing.
ColumnRangeOrNothing #
column-range-or-nothing
One named column of a table with its header, or Nothing
Signature:
Private Function ColumnRangeOrNothing(ByVal Lo As ListObject, _
ByVal colName As String) As Range
Parameters:
Lo: ListObject. The table to read.colName: String. The column name.
Returns: Range. The whole column including its header, or Nothing.
RangeValues #
range-values
Read a range into a two dimensional array in one crossing
Signature:
Private Function RangeValues(ByVal rng As Range) As Variant
Range.Value hands back a scalar for a single cell and an array for anything wider, so a one-cell range is wrapped to give every caller the same shape.
Parameters:
rng: Range. The range to read.
Returns: Variant. A 1-based two dimensional array of the values.
The Registry Of Spatial Variables
RegisteredNames #
registered-names
Every name the listofgeovars table holds
Signature:
Private Function RegisteredNames() As BetterArray
One crossing reads the whole column into memory, and every match in this class is then a string test. Range.Find inherits LookIn, SearchOrder and SearchDirection from the last search of the Excel session, including one the user ran from the Find dialog, so a lookup built on it can miss for a reason that has nothing to do with the workbook.
The header cell is left out because the data body is read on its own. A search over the whole table range answered True for "list" on an empty table, and a header hit could become a variable name.
Returns: BetterArray. The registered names, empty when the table is absent.
ListOfExistingVarNames #
list-of-existing-var-names
The spatial names one variable carries
Signature:
Private Function ListOfExistingVarNames(ByVal varName As String) As BetterArray
A registered name is always "
Parameters:
varName: String. The bare variable name.
Returns: BetterArray. The matching registered names.
RegisteredNameForTable #
registered-name-for-table
The variable a table identifier belongs to
Signature:
Private Function RegisteredNameForTable(ByVal tabId As String) As String
The test is on the suffix. A substring test matched "sp10" and "sp11" when it was asked for "sp1", so it broke on the day a linelist carried ten spatial cross-tables.
Parameters:
tabId: String. The table identifier.
Returns: String. The registered name, empty when nothing carries that identifier.
Update
UpdateOneHList #
update-one-hlist
Fill the spatial tables of the variables of one HList sheet
Signature:
Private Sub UpdateOneHList(ByVal sh As Worksheet, ByVal wb As Workbook, _
ByVal shHn As HiddenNames)
Walks every header cell of the variable row, from the _START anchor to the last used column, one column at a time. A geo1 or an hf control names a variable to update and every other header is passed over. The walk used to jump the width one geo expansion is expected to take, and a sheet whose columns sat closer than that landed the jump past the next geo variable, so its spatial tables were never filled and FindTopAdmin answered nothing for them. It also used to stop on the first empty header cell, and a variable the writer skipped leaves exactly that gap. The header row is read in one crossing.
A sheet with no filtered companion, no table name or no first table is left alone.
Parameters:
sh: Worksheet. The HList worksheet.wb: Workbook. The workbook holding the sheet.shHn: HiddenNames. The hidden name store of that sheet.
NameWithoutPrefix #
name-without-prefix
Cut a known prefix off the front of a name
Signature:
Private Function NameWithoutPrefix(ByVal fullName As String, _
ByVal prefix As String) As String
The header cell of a geo column always carries its prefix at the front. Replace took the prefix out wherever it sat, so a variable named "adm1_site_adm1_ref" came back as "sitref".
Parameters:
fullName: String. The name as the header cell holds it.prefix: String. The prefix to cut.
Returns: String. The name with the leading prefix removed.
UpdateSpatialValue #
update-spatial-value
Fill the spatial tables of one variable from a filtered column
Signature:
Private Sub UpdateSpatialValue(ByVal inputRng As Range, _
ByVal spatialNames As BetterArray, _
Optional ByVal isHF As Boolean = False)
A geographic variable has one table per administrative level, and the columns of the companion sheet hold concat_adm1 through concat_adm4 side by side, so the input range walks one column right per level. A facility variable has one table and one column.
Parameters:
inputRng: Range. The filtered column the first level reads.spatialNames: BetterArray. The registered names of this variable.isHF: Optional Boolean. True for the facility table. Defaults to False.
DistinctValues #
distinct-values
The distinct values of a range, read in one crossing
Signature:
Private Function DistinctValues(ByVal rng As Range) As Variant
The values used to go out to a scratch column, through RemoveDuplicates, and back again — five worksheet round trips per level per variable per sheet, on a range that can run to tens of thousands of rows. The index is a delimited string, and the separator sits on both sides of every entry so a short entry cannot match inside a longer one.
Blanks are dropped here. EventLinelist.GeoConcat answers an empty string for a record whose geo levels are not all filled, RemoveDuplicates collapsed those to one blank, and that blank became a data row that could take rank 1 once the tables were ordered.
Two values that differ only in case are one value, which is what RemoveDuplicates answered and what COUNTIFS answers over the same data.
Parameters:
rng: Range. The column to read.
Returns: Variant. A 1-based two dimensional array of the distinct values, or Empty when the range holds none.
FillOneTable #
fill-one-table
Write the values of one spatial table and order it
Signature:
Private Sub FillOneTable(ByVal Lo As ListObject, ByVal adminName As String, _
ByRef values As Variant)
The rows below the first are cleared before the resize, so a table that shrinks leaves nothing of its old self standing under it. The first data row keeps the formulas SpatialTables wrote, and they are copied down over the new rows.
The table is calculated before it is ordered. Update recalculates the whole sheet, and that happens once every sheet has been walked, so the sort would read the values of the run before.
Parameters:
Lo: ListObject. The spatial table to fill.adminName: String. The level identifier the sort column is named for.values: Variant. The values for the first column, or Empty for none.
ClearDataRowsBelowFirst #
clear-data-rows-below-first
Empty every data row of a table but the first
Signature:
Private Sub ClearDataRowsBelowFirst(ByVal Lo As ListObject)
ClearContents leaves the number formats, the borders and the validation of the column standing. Clear took those with the values, so the key column lost its formatting on every update.
Parameters:
Lo: ListObject. The table to clear.
FillFormulaColumns #
fill-formula-columns
Copy the formulas of the first data row over every row
Signature:
Private Sub FillFormulaColumns(ByVal Lo As ListObject)
The rows used to be left to the calculated-column autofill of the host, which is an option a user can switch off, and Resize is a weaker trigger for it than a row insert. SpatialTables writes the value column as an array formula, and an array formula is the least likely of all to be filled that way, so only the first row of a table held a number and the sort ranked it against blanks.
AutoFill needs a destination taller than one cell and raises 1004 otherwise, which a table of one row produces on the ordinary path.
Parameters:
Lo: ListObject. The table whose formula columns are filled.
Sort
SortOneTable #
sort-one-table
Order one spatial table on one of its value columns
Signature:
Private Sub SortOneTable(ByVal Lo As ListObject, ByVal adminName As String, _
ByVal onAR As Boolean)
Excel wants the sort key inside the range it sorts and raises 1004 when it sits outside. Both sort sites of this class ordered the first column alone with a key in the second or the fourth, under On Error Resume Next, so no spatial table has ever been ordered and every "top N admin units" figure was in insertion order.
Sorting the whole table is safe: SpatialTables writes the population and the attack rate as relative references to the cells of their own row, so Excel moves them with the row they belong to.
Parameters:
Lo: ListObject. The table to order.adminName: String. The level identifier the value columns are named for.onAR: Boolean. True to order on the attack rate.
Spatial Formula Rewriting
HeaderFormulaTexts #
header-formula-texts
Read the header row of formulas in one crossing
Signature:
Private Function HeaderFormulaTexts(ByVal headerRng As Range) As Variant
A one-cell range answers a scalar through Formula, so it is wrapped to give the caller one shape.
Parameters:
headerRng: Range. The SPT_FORMULA_COLUMN_ row of one section.
Returns: Variant. A two-dimensional array of formula strings.
FormulaTextOf #
formula-text-of
Read the formula of one cell through the property that owns it
Signature:
Private Function FormulaTextOf(ByVal cellRng As Range, _
ByVal isArrayFormula As Boolean) As String
Parameters:
cellRng: Range. The cell to read.isArrayFormula: Boolean. What HasArray answered for the cell.
Returns: String. The formula string.
WriteFormulaText #
write-formula-text
Write the formula of one cell through the property that owns it
Signature:
Private Sub WriteFormulaText(ByVal cellRng As Range, _
ByVal formulaText As String, _
ByVal isArrayFormula As Boolean)
Parameters:
cellRng: Range. The cell to write.formulaText: String. The formula string.isArrayFormula: Boolean. True writes through FormulaArray.
Validation
CheckRequirements #
check-requirements
Validate that the worksheet is the spatial tables sheet
Signature:
Private Sub CheckRequirements(ByVal sh As Worksheet)
Guards the factory by checking the worksheet is given and its name matches the expected "__spatial_tables" constant.
Parameters:
sh: Worksheet. The worksheet to validate.
Throws:
- ProjectError.ObjectNotInitialized When the worksheet is Nothing.
- ProjectError.InvalidArgument When the worksheet name does not match "__spatial_tables".
Error Handling
ThrowError #
throw-error
Raise a ProjectError-based exception
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:
errNumber: Long. The ProjectError error code to raise.message: String. Human-readable description of the failure.
Throws:
- ProjectError.
Always raises the specified error.
Used in (10 file(s))
- SpatialTables.cls
- LLdictionary.cls
- EventLinelist.cls
- Linelist.cls
- LLDataEntry.cls
- EventsLinelistButtons.bas
- GeoModule.bas
- FormLogicGeo.bas
- TestSpatialTables.bas
- TestLLSpatial.bas