Skip to content

Commit c808acb

Browse files
author
Oussama
committed
feat: art-directed polish and country artist stories
1 parent 38a61f3 commit c808acb

11 files changed

Lines changed: 804 additions & 160 deletions

File tree

data/build_aggregates.py

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,41 @@ def lifespan(begin: int | None, end: int | None) -> str:
331331
return "lifespan unknown"
332332

333333

334+
def artist_story(
335+
artist_name: str,
336+
country_name: str,
337+
n_works: int,
338+
top_medium: str,
339+
first_decade: int | None,
340+
last_decade: int | None,
341+
work_title: str | None,
342+
work_year: int | None,
343+
) -> str:
344+
"""Build a factual collection note without inventing biography."""
345+
country_phrase = (
346+
f"the {country_name}"
347+
if country_name in {"United States", "United Kingdom", "Netherlands"}
348+
else country_name
349+
)
350+
if first_decade and last_decade and first_decade != last_decade:
351+
span = f"from the {first_decade}s to the {last_decade}s"
352+
elif first_decade:
353+
span = f"in the {first_decade}s"
354+
else:
355+
span = "across undated or partially dated records"
356+
work = ""
357+
if work_title:
358+
work = f" One representative record is {work_title}"
359+
if work_year:
360+
work += f" ({work_year})"
361+
work += "."
362+
return (
363+
f"In this dataset, {artist_name} is the most represented artist credited to {country_phrase}, "
364+
f"with {n_works:,} credited works. MoMA's records place this artist mostly in {top_medium}, "
365+
f"with works appearing {span}.{work}"
366+
)
367+
368+
334369
def load_lookup_files() -> tuple[dict[str, str], dict[str, str]]:
335370
with (DATA_DIR / "nationality_to_iso3.json").open("r", encoding="utf-8") as handle:
336371
raw_nationality_to_iso = json.load(handle)
@@ -479,10 +514,19 @@ def main() -> None:
479514
medium_counter = Counter(group["medium_group"])
480515
sorted_group = group.sort_values(["artist_name", "title", "year"], na_position="last")
481516
sample = sorted_group.iloc[seeded_index(str(iso3), len(sorted_group))]
517+
featured_artist_id = int(group["artist_id"].value_counts().sort_values(ascending=False).index[0])
518+
featured_group = group[group["artist_id"] == featured_artist_id].sort_values(["year", "title"], na_position="last")
519+
featured_sample = featured_group.iloc[0]
520+
featured_medium = Counter(featured_group["medium_group"]).most_common(1)[0][0]
521+
featured_decades = [int(decade) for decade in featured_group["decade"].dropna().unique()]
522+
featured_first = min(featured_decades) if featured_decades else None
523+
featured_last = max(featured_decades) if featured_decades else None
524+
featured_work_year = json_ready(featured_sample["year"])
525+
country_name = ISO3_TO_COUNTRY.get(iso3, iso3)
482526
country_summary_records.append(
483527
{
484528
"iso3": iso3,
485-
"country_name": ISO3_TO_COUNTRY.get(iso3, iso3),
529+
"country_name": country_name,
486530
"region": regions.get(iso3, "Unknown"),
487531
"n": int(len(group)),
488532
"n_artists": int(group["artist_id"].nunique()),
@@ -492,6 +536,29 @@ def main() -> None:
492536
"sample_work_title": sample["title"],
493537
"sample_work_year": json_ready(sample["year"]),
494538
"sample_work_medium": sample["medium"],
539+
"featured_artist_id": featured_artist_id,
540+
"featured_artist": featured_sample["artist_name"],
541+
"featured_artist_n_works": int(len(featured_group)),
542+
"featured_artist_lifespan": lifespan(
543+
featured_sample["year_birth"],
544+
featured_sample["year_death"],
545+
),
546+
"featured_artist_gender": featured_sample["gender"],
547+
"featured_artist_medium": featured_medium,
548+
"featured_decade_first": featured_first,
549+
"featured_decade_last": featured_last,
550+
"featured_work_title": featured_sample["title"],
551+
"featured_work_year": featured_work_year,
552+
"featured_story": artist_story(
553+
featured_sample["artist_name"],
554+
country_name,
555+
int(len(featured_group)),
556+
featured_medium,
557+
featured_first,
558+
featured_last,
559+
featured_sample["title"],
560+
featured_work_year,
561+
),
495562
}
496563
)
497564
country_summary_records.sort(key=lambda item: item["n"], reverse=True)

data/build_report.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
DArts aggregate build report
2-
generated_at: 2026-05-07T20:52:07+00:00
2+
generated_at: 2026-05-08T21:20:30+00:00
33

44
raw_artworks: 160,248
55
cleaned_artworks: 144,149
@@ -42,7 +42,7 @@ top_unmapped_nationality_labels:
4242
outputs:
4343
artist_index.json: 3,548,506 bytes
4444
country_by_decade.json: 50,233 bytes
45-
country_summary.json: 38,512 bytes
45+
country_summary.json: 113,489 bytes
4646
gender_by_decade.json: 3,753 bytes
4747
gender_by_decade_country.json: 79,461 bytes
4848
gender_by_decade_department.json: 21,341 bytes

website/public/data/country_summary.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

website/public/data/summary.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"n_artworks_total":144149,"n_artworks_dated":141884,"n_artist_credits":160035,"n_artists_total":11879,"year_min":1768,"year_max":2026,"generated_at":"2026-05-07T20:52:07+00:00","total_artworks":144149,"total_artists":11879,"unique_nationalities":129,"top_department":"Drawings & Prints"}
1+
{"n_artworks_total":144149,"n_artworks_dated":141884,"n_artist_credits":160035,"n_artists_total":11879,"year_min":1768,"year_max":2026,"generated_at":"2026-05-08T21:20:30+00:00","total_artworks":144149,"total_artists":11879,"unique_nationalities":129,"top_department":"Drawings & Prints"}

website/src/lib/charts/LineChart.svelte

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,12 +95,16 @@
9595
<style>
9696
.line-chart {
9797
margin: 0;
98+
padding: var(--space-1);
99+
background: var(--bg-paper);
100+
border: 1px solid var(--rule-on-light);
98101
}
99102
100103
svg {
101104
width: 100%;
102105
height: auto;
103106
display: block;
107+
background: var(--bg-paper);
104108
}
105109
106110
.grid line {
@@ -148,13 +152,13 @@
148152
149153
.area {
150154
fill: var(--gender-female);
151-
opacity: 0.08;
155+
opacity: 0.12;
152156
}
153157
154158
.series {
155159
fill: none;
156160
stroke: var(--gender-female);
157-
stroke-width: 2;
161+
stroke-width: 3;
158162
stroke-linejoin: round;
159163
stroke-linecap: round;
160164
}
@@ -169,6 +173,15 @@
169173
fill: var(--bg-paper);
170174
stroke: var(--gender-female);
171175
stroke-width: 2;
176+
transition:
177+
r var(--duration-tooltip-in) var(--ease-out),
178+
fill var(--duration-tooltip-in) var(--ease-out);
179+
}
180+
181+
circle:hover,
182+
circle:focus-visible {
183+
r: 6;
184+
fill: var(--gender-female);
172185
}
173186
174187
circle.small {

website/src/lib/charts/Treemap.svelte

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,54 @@
3636
function hideTooltip() {
3737
window.dispatchEvent(new CustomEvent('darts:tooltip-hide'));
3838
}
39+
40+
function motifLines(leaf) {
41+
const w = leaf.x1 - leaf.x0;
42+
const h = leaf.y1 - leaf.y0;
43+
const pad = Math.max(8, Math.min(w, h) * 0.08);
44+
if (w < 42 || h < 42) return [];
45+
if (leaf.rank % 3 === 0) {
46+
return Array.from({ length: 5 }, (_, i) => ({
47+
x1: leaf.x0 + pad,
48+
y1: leaf.y0 + pad + i * ((h - pad * 2) / 4),
49+
x2: leaf.x1 - pad,
50+
y2: leaf.y0 + pad + i * ((h - pad * 2) / 4) + (i % 2 ? -8 : 8),
51+
}));
52+
}
53+
if (leaf.rank % 3 === 1) {
54+
return Array.from({ length: 6 }, (_, i) => ({
55+
x1: leaf.x0 + pad + i * ((w - pad * 2) / 6),
56+
y1: leaf.y1 - pad,
57+
x2: leaf.x0 + pad + i * ((w - pad * 2) / 6) + Math.min(32, w * 0.16),
58+
y2: leaf.y0 + pad,
59+
}));
60+
}
61+
return Array.from({ length: 4 }, (_, i) => ({
62+
x1: leaf.x0 + pad + i * ((w - pad * 2) / 4),
63+
y1: leaf.y0 + pad,
64+
x2: leaf.x0 + pad + i * ((w - pad * 2) / 4) + Math.min(44, w * 0.18),
65+
y2: leaf.y1 - pad,
66+
}));
67+
}
3968
</script>
4069
4170
<figure class="treemap" aria-label="Treemap of MoMA collection areas by number of works">
4271
<svg viewBox={`0 0 ${width} ${height}`}>
4372
<title>Treemap of MoMA collection areas by number of works</title>
73+
<defs>
74+
{#each leaves as leaf}
75+
<clipPath id={`leaf-clip-${leaf.rank}`}>
76+
<rect x={leaf.x0} y={leaf.y0} width={leaf.x1 - leaf.x0} height={leaf.y1 - leaf.y0} />
77+
</clipPath>
78+
{/each}
79+
</defs>
4480
{#each leaves as leaf}
4581
{@const leafWidth = leaf.x1 - leaf.x0}
4682
{@const leafHeight = leaf.y1 - leaf.y0}
4783
{@const area = leafWidth * leafHeight}
4884
<g class="leaf" style:--rank={leaf.rank}>
4985
<rect
86+
class="mark"
5087
x={leaf.x0}
5188
y={leaf.y0}
5289
width={leafWidth}
@@ -65,6 +102,18 @@
65102
tabindex="0"
66103
aria-label={`${leaf.data.medium}: ${leaf.data.n} works, ${Number(leaf.data.pct).toFixed(1)} percent`}
67104
/>
105+
<rect
106+
class="inner-frame"
107+
x={leaf.x0 + 5}
108+
y={leaf.y0 + 5}
109+
width={Math.max(0, leafWidth - 10)}
110+
height={Math.max(0, leafHeight - 10)}
111+
/>
112+
<g class="motif" clip-path={`url(#leaf-clip-${leaf.rank})`}>
113+
{#each motifLines(leaf) as line}
114+
<line x1={line.x1} y1={line.y1} x2={line.x2} y2={line.y2} />
115+
{/each}
116+
</g>
68117
{#if area >= labelArea}
69118
<text x={leaf.x0 + 10} y={leaf.y0 + 24}>
70119
<tspan>{leaf.data.medium}</tspan>
@@ -109,10 +158,12 @@
109158
width: 100%;
110159
height: auto;
111160
display: block;
112-
background: var(--bg-light);
161+
padding: var(--space-1);
162+
background: var(--bg-paper);
163+
border: 1px solid var(--rule-on-light);
113164
}
114165
115-
rect {
166+
.mark {
116167
fill: var(--seq-1);
117168
stroke: var(--bg-light);
118169
stroke-width: 1;
@@ -124,21 +175,43 @@
124175
transition: fill var(--duration-tooltip-in) var(--ease-out);
125176
}
126177
127-
rect.highlight {
178+
.mark.highlight {
128179
fill: var(--accent-primary);
129180
}
130181
131-
rect:hover,
132-
rect:focus-visible {
182+
.mark:hover,
183+
.mark:focus-visible {
133184
fill: var(--seq-2);
134185
outline: none;
135186
}
136187
137-
rect.highlight:hover,
138-
rect.highlight:focus-visible {
188+
.mark.highlight:hover,
189+
.mark.highlight:focus-visible {
139190
fill: var(--accent-primary);
140191
}
141192
193+
.inner-frame,
194+
.motif {
195+
pointer-events: none;
196+
}
197+
198+
.inner-frame {
199+
fill: none;
200+
stroke: color-mix(in srgb, var(--fg-on-light-strong) 20%, transparent);
201+
stroke-width: 1;
202+
}
203+
204+
.motif line {
205+
stroke: color-mix(in srgb, var(--fg-on-light-strong) 16%, transparent);
206+
stroke-width: 1.2;
207+
stroke-linecap: round;
208+
}
209+
210+
.leaf:has(.mark.highlight) .motif line,
211+
.leaf:has(.mark.highlight) .inner-frame {
212+
stroke: color-mix(in srgb, var(--fg-on-dark-strong) 42%, transparent);
213+
}
214+
142215
text {
143216
fill: var(--fg-on-light-strong);
144217
font-family: var(--font-ui);

0 commit comments

Comments
 (0)