City metrics
What a building in the code city is measured by, how a measurement becomes a dimension, and what the legend says. Set with city --height / --footprint.
Built-in metric sources
A metric source is {name, unit, describe, value(context)}. unit and describe are the strings that reach the artifact’s bindings block and the renderer’s legend.
| Name | Unit | Definition |
|---|---|---|
loc |
source lines | Lines the type’s own source anchor spans (end - start + 1). |
members |
entities | Base entities that folded into the building, the type included. |
methods |
methods | Members carrying TInvocable — methods, constructors and lambdas. |
fields |
fields | Members carrying TStructural whose container is the type itself. |
fanIn |
dependents | Distinct types that depend on this one, at type level, under the view. |
fanOut |
dependencies | Distinct types this one depends on, at type level, under the view. |
degree |
relations | fanIn + fanOut — how connected the type is, in either direction. |
one |
buildings | The constant 1 — a uniform city, for isolating one channel at a time. |
Open forms
An extractor’s own measures live in the entity’s TMetrics map, whose keys are deliberately open. Two forms read that map by name:
| Form | Reads | Unit |
|---|---|---|
attribute:<key> |
the type’s own measure | <key> |
sum:<key> |
that measure summed over everything that folded into the building | <key> |
sum: is the form complexity wants, because complexity is measured per invocable and a building is a type: --height sum:cyclomatic --footprint loc builds the complexity city. The Java extractor emits sloc and cyclomatic.
The city will not invent a measure by re-parsing source it cannot see — only the extractor measures. A top-level numeric key of the same name is still read, since an entity is a loose object, but it is uncontractual: the map wins.
An unknown name throws (UnknownMetricError, naming what exists) rather than falling back to a default. A silent fallback would put a documented metric’s name on a different metric’s numbers.
“I do not know” is an answer
A source returns undefined, never a substitute zero: a stub type has no anchor, so it has no line count, and a zero there would be a measurement nobody took. sum: likewise returns undefined — not 0 — when no member carries the key.
An unmeasured building is drawn at the channel minimum, counted in bindings[].unmeasured and in diagnostics.unmeasured, and reported on stderr:
warning: height is unmeasured on 18 buildings (metric loc) — those are drawn at the channel minimum, not at zero.Channels
| Channel | Default metric | Default scale | Default range | Reading |
|---|---|---|---|---|
height |
loc |
linear |
1 – 40 | a tower twice as tall holds twice the source |
footprint |
members |
sqrt |
2 – 20 | the metric maps onto the SIDE, so base AREA is proportional to it |
sqrt on the footprint is the one non-obvious default: mapping a metric linearly onto the side length would make area grow with its square, and a city plan invites the reader to compare areas. Both are overridable per channel with --height-scale / --footprint-scale, and whichever was used ships in bindings.
--carry M1,M2 measures extra metrics onto every building without binding them to a channel; they appear in Building.metrics and nowhere else.
--framework spring adds a third, categorical channel: Building.role, with CityModel.roles as its legend. It is an inference from written annotations, and a building with no role stays neutral rather than being coloured “plain”.
Scales
linear · sqrt · log
log is log1p, not log: metrics legitimately reach 0 (a type nobody depends on has fan-in 0) and log(0) is -Infinity, which would silently render a measured zero at the range floor. Negative inputs are clamped to 0.
A degenerate domain maps to the MIDDLE of the range. When every building measures the same, identical inputs must render identically; flooring them all would read as “everything is minimal” when the truth is “everything is equal”.
Dimensions are rounded to 3 decimals, so two runs serialize byte-identically.
The order of operations
A dimension is relative to a domain, so the domain must exist first:
- Place. Fold to type level, resolve each building’s district.
- Measure. Ask every source for every building. No dimension exists yet.
- Scale. Compute each channel’s domain over the measured values, then map.
The legend, in machine form
bindings is the legend a renderer prints without knowing anything about codegraph. One entry per bound channel:
{
"channel": "height",
"metric": "loc",
"unit": "source lines",
"describe": "Lines the type's own source anchor spans (end - start + 1).",
"scale": "linear",
"range": { "min": 1, "max": 40 },
"domain": { "min": 4, "max": 51 },
"unmeasured": 18
}| Field | Meaning |
|---|---|
channel |
height or footprint |
metric |
the resolved name, open forms included |
unit |
what one unit of the value is |
describe |
the source’s own one-line description |
scale |
how to read the mapping |
range |
the dimension given to the smallest and the largest measured value |
domain |
observed across the buildings that had a value; absent when none did |
unmeasured |
buildings the model could not measure on this channel |
Building.metrics carries the raw measurement next to the dimension it produced: a height of 17.596 means nothing on its own, and loc: 24 beside it is what a tooltip, a legend and a bug report need. null means the model does not say.