#!/usr/bin/env python3
"""Generate the four embeddable charts for field study No. 04 (Harris County, TX).
Every plotted value is READ FROM harris-commercial-record-2026-09.csv at run time.
Nothing is typed in by hand: if a metric is not in the CSV, this script raises
rather than draw it. The CSV is produced by count_harris.py, published beside it.
python3 make-charts.py [path/to/harris-commercial-record-2026-09.csv]
Same drawing code as page/field-study-03/make-charts.py, so the four studies
share one visual language."""
import csv, os, sys
HERE = os.path.dirname(os.path.abspath(__file__))
CSV_PATH = sys.argv[1] if len(sys.argv) > 1 else os.path.join(
HERE, "harris-commercial-record-2026-09.csv")
def load(path):
vals = {}
with open(path, newline="", encoding="utf-8") as fh:
rows = [ln for ln in fh if not ln.startswith("#")]
for r in csv.DictReader(rows):
vals[r["metric"]] = (r["value"], r["share"])
if not vals:
raise SystemExit("no metrics parsed from " + path)
return vals
V = load(CSV_PATH)
def num(metric):
"""A figure, straight from the CSV. KeyError if it was never counted."""
if metric not in V:
raise KeyError("not in the CSV, so it was not counted: " + metric)
s = V[metric][0]
return float(s) if "." in s else int(s)
def pct(metric):
"""The share as the CSV prints it, e.g. '50.2%'. Never recomputed here."""
if metric not in V or not V[metric][1]:
raise KeyError("no share published for: " + metric)
return V[metric][1]
SURFACE="#f7f5ed"; INK="#173e32"; MUTED="#7b9384"
ACCENT="#0d6b47"; GRAY="#b6c4b5"; GRID="#d9e0d4"
FONT="system-ui,-apple-system,'Segoe UI',Helvetica,Arial,sans-serif"
CREDIT="Sterling Digital Partners ยท glvtl.com/field-study"
def bar(x,y,w,h,r=4):
if w<=r: return f''
return (f'')
def wrap(t,n=112):
out,cur=[],""
for w in t.split():
if len(cur)+len(w)+1>n: out.append(cur); cur=w
else: cur=(cur+" "+w).strip()
if cur: out.append(cur)
return out
def chart(fn,title,sub,rows,axis_max,ticks,legend,note,W=720,L=248,fmt="{:,}",tickfmt=None):
n=len(rows); band=38; top=92
tickfmt = tickfmt or fmt
nl=wrap(note)
H=top+n*band+34+len(legend)*19+len(nl)*16+32
R=66; pw=W-L-R
s=[f'')
open(os.path.join(HERE,fn),'w').write('\n'.join(s))
print("wrote",fn,f"({W}x{H})")
def stacked(fn,title,sub,rows,legend,note,W=720,L=206):
nl=wrap(note); band=54; top=98
H=top+len(rows)*band+18+len(legend)*19+len(nl)*16+32
R=26; pw=W-L-R
s=[f'')
open(os.path.join(HERE,fn),'w').write('\n'.join(s))
print("wrote",fn,f"({W}x{H})")
# --- Finding 01: who taxes it -------------------------------------------------
no_city = num("commercial_in_no_city")
in_city = num("commercial_in_a_city")
val_no = num("value_no_city_usd")
val_in = num("value_in_city_usd")
total_act = num("commercial_accounts_f1_f2")
total_val = num("commercial_appraised_value_usd")
stacked("field-study-04-jurisdiction-split.svg",
"A third of Harris County's commercial property is taxed by no city",
"Harris County commercial and industrial accounts (state class F1 and F2) by whether any "
"municipality taxes them.",
[("Commercial accounts", no_city, in_city, f"n = {total_act:,}"),
("Commercial appraised value", val_no, val_in,
f"n = ${total_val/1e9:,.1f}bn")],
[(ACCENT,"No municipality on the account's taxing-jurisdiction list"),
(GRAY,"Taxed by at least one of the county's 34 municipalities")],
"The City of Houston itself taxes {houston:,} of these accounts ({hsh}) and ${hv:.1f}bn of the value ({hvs}). "
"This counts property-tax jurisdiction, which is what the roll records; it is not a count of "
"extraterritorial jurisdiction, and a property outside every city's tax jurisdiction may still sit "
"inside a city's limited-purpose annexation. Source: Harris Central Appraisal District 2026 CAMA "
"files, dated 13 Sep 2026, counted 20 Sep 2026.".format(
houston=num("houston_accounts"), hsh=pct("houston_accounts"),
hv=num("houston_value_usd")/1e9, hvs=pct("houston_value_usd")))
# --- Finding 02: leaving the city does not simplify the bill ------------------
chart("field-study-04-jurisdiction-stack.svg",
"Leaving the city does not mean fewer taxing jurisdictions",
"Mean number of distinct taxing jurisdictions per Harris County commercial account.",
[("All commercial accounts", num("mean_taxing_jurisdictions"), False),
("Inside a city", num("mean_taxing_jurisdictions_in_city"), False),
("Outside every city", num("mean_taxing_jurisdictions_no_city"), True),
("Outside every city, in a utility district", num("mean_taxing_jurisdictions_no_city_in_utility"), True)],
10.0, [0,2,4,6,8,10],
[(ACCENT,"Outside every city's property-tax jurisdiction"),
(GRAY,"Inside a city, or the whole county")],
"The city is one layer of roughly nine, and removing it usually adds a utility district in its place. "
"{a:,} of {t:,} commercial accounts ({as_}) are taxed by seven or more jurisdictions and {b:,} ({bs}) "
"by ten or more; the maximum on the roll is {m:.0f}, out of 1,078 districts. Source: Harris Central "
"Appraisal District 2026 CAMA files, dated 13 Sep 2026, counted 20 Sep 2026.".format(
a=num("commercial_taxed_by_7_or_more"), t=total_act, as_=pct("commercial_taxed_by_7_or_more"),
b=num("commercial_taxed_by_10_or_more"), bs=pct("commercial_taxed_by_10_or_more"),
m=num("max_taxing_jurisdictions")),
L=306, fmt="{:.2f}", tickfmt="{:.0f}")
# --- Finding 03: the absence of price -----------------------------------------
chart("field-study-04-price-absence.svg",
"The Harris County conveyance record has no price in it",
"deeds.txt, the county appraisal district's complete recorded-conveyance file. Five columns: "
"acct, dos, clerk_yr, clerk_id, deed_id.",
[("Recorded conveyances", num("recorded_conveyances_in_deeds_txt"), False),
("... carrying a date", num("conveyances_with_a_date_and_no_price"), False),
("... on a commercial account", num("commercial_conveyance_rows"), False),
("... carrying a price", num("deeds_txt_price_columns"), True)],
2600000, [0,650000,1300000,1950000,2600000],
[(ACCENT,"Columns that could hold a price, consideration or instrument type: 0 of 5"),
(GRAY,"What the file does record")],
"Texas is a non-disclosure state: there is no statute requiring a sale price to be reported, so the "
"appraisal district does not publish one. This is an absence and is never expressed as a share of "
"hidden sales, because how many of these conveyances were arm's-length sales, and for how much, is "
"exactly what non-disclosure hides. Source: Harris Central Appraisal District 2026 CAMA files, dated "
"13 Sep 2026, counted 20 Sep 2026.",
L=248)
# --- Finding 04: address collisions, as an upper bound ------------------------
chart("field-study-04-address-collisions.svg",
"One site address, more than one commercial account",
"Harris County commercial accounts sharing a site address with another account. "
"n = {:,} commercial accounts.".format(total_act),
[("Share an address", num("commercial_sharing_an_address"), False),
("... excluding placeholder addresses", num("commercial_sharing_an_address_excl_placeholder"), False),
("... and on 2+ Key Map grid pages", num("commercial_sharing_address_across_key_maps"), True),
("... both filters applied", num("commercial_sharing_address_across_key_maps_excl_placeholder"), True)],
11000, [0,2750,5500,8250,11000],
[(ACCENT,"Upper bound on genuinely dispersed collisions"),
(GRAY,"Every shared address, dispersed or stacked")],
"HCAD publishes no parcel coordinate in this extract, so study No. 03's distance test cannot be run "
"here. The Key Map grid page is the finest locator the district publishes, and it is a cell rather "
"than a point: a subdivision straddling a grid line reads as dispersed, which the largest group on "
"the roll does. Treat the bottom two bars as an upper bound, not a measurement. {p:,} accounts "
"({ps}) carry no house number at all. Source: Harris Central Appraisal District 2026 CAMA files, "
"dated 13 Sep 2026, counted 20 Sep 2026.".format(p=num("commercial_address_no_house_number"),
ps=pct("commercial_address_no_house_number")),
L=248)