#!/usr/bin/env python3 """Generate the four embeddable charts for field study No. 06 (Nashville / Davidson County, TN). Every plotted value is READ FROM nashville-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_nashville.py, published beside it. python3 make-charts.py [path/to/nashville-commercial-record-2026-09.csv] Same drawing code as studies No. 03, 04 and 05, so the six 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, "nashville-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 share(metric): """The share as the CSV prints it, as a number. Never recomputed here.""" if metric not in V or not V[metric][1]: raise KeyError("no share in the CSV for: " + metric) return float(V[metric][1].rstrip("%")) 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: the address is not the government --------------------------- CITIES = ["Berry Hill", "Belle Meade", "Oak Hill", "Forest Hills", "Goodlettsville", "Ridgetop"] rows = [] for c in CITIES: pc = share(f"{c} parcels with a NASHVILLE property address") rows.append((f"{c} ({num(c + ' parcels'):,} parcels)", pc, pc > 99)) chart("field-study-06-satellite-cities.svg", "Six cities inside Davidson County. Four of them are addressed Nashville", "Share of each separately incorporated city's parcels whose property " "address reads NASHVILLE", rows, 100, [0, 25, 50, 75, 100], [(ACCENT, "Every parcel in the city carries a Nashville address"), (GRAY, "Some or none do")], "Berry Hill and Belle Meade return exactly one distinct property-city " "value between them, and it is NASHVILLE. Counted 20 September 2026 from " "Metro Nashville's own parcel service.", fmt="{:.1f}%", tickfmt="{}%") # --- Finding 02: the whitespace that hides a district ------------------------- rows = [] for code, label in [("USD", "Urban Services District"), ("GSD", "General Services District"), ("GO", "Goodlettsville"), ("BH", "Berry Hill"), ("FH", "Forest Hills"), ("OH", "Oak Hill")]: k = f"TaxDist {code} parcels missed by an exact match" rows.append((f"{label} ({code})", num(k), code in ("USD", "GSD"))) chart("field-study-06-whitespace.svg", "43,132 parcels are invisible to an exact-match filter", "Parcels whose tax district is stored with trailing whitespace, and so " "are missed by a query for the district's own code", rows, 40000, [0, 10000, 20000, 30000, 40000], [(ACCENT, "Metro Nashville's own service districts"), (GRAY, "Separately incorporated cities")], "The column holds 22 distinct values. There are 15 districts. Nothing " "errors; the rows simply do not come back. Counted 20 September 2026.", fmt="{:,}", tickfmt="{:,}") # --- Finding 04: one code, two classifications -------------------------------- SPLITS = [("012", "DUPLEX"), ("015", "RESIDENTIAL CONDO"), ("020", "VACANT COMMERCIAL LAND"), ("016", "ZERO LOT LINE"), ("010", "VACANT RESIDENTIAL LAND"), ("011", "SINGLE FAMILY")] rows = [] for code, desc in SPLITS: k = f"Code {code} ({desc}) assessed at 40%" rows.append((f"{code} {desc.title()}", share(k), code in ("012", "011"))) chart("field-study-06-classification.svg", "The same land use code, assessed under two different classifications", "Share of each code's parcels assessed at Tennessee's 40% commercial " "and industrial ratio; the rest are at 25% or zero", rows, 100, [0, 25, 50, 75, 100], [(ACCENT, "Codes whose own description reads as residential"), (GRAY, "Other codes split across both ratios")], "Tennessee assesses commercial and industrial real property at 40% of " "appraised value and residential and farm property at 25%. Seven codes " "carry parcels on both sides. Counted 20 September 2026.", fmt="{:.1f}%", tickfmt="{}%") # --- Finding 06: what the sale record carries --------------------------------- comm = num("Parcels classified commercial or industrial by the assessor") noprice = num("Commercial parcels carrying no sale price at all") priced = num("Commercial parcels carrying a sale price") stacked("field-study-06-sale-record.svg", "Two in five commercial parcels carry no sale price at all", "Davidson County parcels the assessor classifies commercial or " "industrial, by whether the record carries a price", [("Commercial parcels", noprice, priced, f"{comm:,} parcels assessed at 40%")], [(ACCENT, "No sale price in the record"), (GRAY, "A sale price, under one of 28 undocumented codes")], "The field named ValidSale is populated on 4 of the county's 287,112 " "parcels. Counted 20 September 2026.")