#!/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'', f'{title}{sub} {note}', f'', f'{title}', f'{sub}'] for t in ticks: x=L+pw*t/axis_max s.append(f'') s.append(f'{tickfmt.format(t)}') for i,(lab,val,hot) in enumerate(rows): y=top+i*band; w=pw*val/axis_max; c=ACCENT if hot else GRAY s.append(f'{lab}') s.append(f'{bar(L,y,w,20)}') s.append(f'{fmt.format(val)}') ly=top+n*band+32 for j,(sw,txt) in enumerate(legend): yy=ly+j*19 s.append(f'') s.append(f'{txt}') ny=ly+len(legend)*19+10 for k,line in enumerate(nl): s.append(f'{line}') s.append(f'{CREDIT}') s.append('') 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'', f'{title}{sub} {note}', f'', f'{title}', f'{sub}'] for i,(lab,a,b,ntxt) in enumerate(rows): y=top+i*band; tot=a+b wa=pw*a/tot; wb=pw*b/tot-2 s.append(f'{lab}') s.append(f'{ntxt}') s.append(f'{bar(L,y,wa,24,0)}') s.append(f'{bar(L+wa+2,y,wb,24)}') s.append(f'' f'{a*100.0/tot:.1f}%') s.append(f'' f'{b*100.0/tot:.1f}%') ly=top+len(rows)*band+6 for j,(sw,txt) in enumerate(legend): yy=ly+j*19 s.append(f'') s.append(f'{txt}') ny=ly+len(legend)*19+10 for k,line in enumerate(nl): s.append(f'{line}') s.append(f'{CREDIT}') s.append('') 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)