Skip to content

Commit 7f8212d

Browse files
author
Oussama
committed
fix: make globe country selection reliable
1 parent c808acb commit 7f8212d

7 files changed

Lines changed: 143 additions & 12 deletions

File tree

data/build_aggregates.py

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,7 @@ def artist_story(
340340
last_decade: int | None,
341341
work_title: str | None,
342342
work_year: int | None,
343+
score: float,
343344
) -> str:
344345
"""Build a factual collection note without inventing biography."""
345346
country_phrase = (
@@ -360,12 +361,51 @@ def artist_story(
360361
work += f" ({work_year})"
361362
work += "."
362363
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}, "
364+
f"In this dataset, {artist_name} has the highest collection prominence score for {country_phrase} "
365+
f"({score:.1f}/100), with {n_works:,} credited works. MoMA's records place this artist mostly in {top_medium}, "
365366
f"with works appearing {span}.{work}"
366367
)
367368

368369

370+
def prominence_table(group: pd.DataFrame) -> pd.DataFrame:
371+
"""Rank artists within one country using a transparent collection-based proxy."""
372+
stats = (
373+
group.groupby("artist_id")
374+
.agg(
375+
artist_name=("artist_name", "first"),
376+
n_works=("artwork_id", "count"),
377+
n_departments=("department", "nunique"),
378+
n_mediums=("medium_group", "nunique"),
379+
first_decade=("decade", "min"),
380+
last_decade=("decade", "max"),
381+
cataloged_share=("cataloged", lambda values: float((values == "Y").mean())),
382+
n_on_view=("on_view", lambda values: int(values.notna().sum())),
383+
has_wiki=("has_wiki", "max"),
384+
has_ulan=("has_ulan", "max"),
385+
)
386+
.reset_index()
387+
)
388+
stats["decade_span"] = (stats["last_decade"] - stats["first_decade"]).fillna(0).clip(lower=0)
389+
max_works = max(1, int(stats["n_works"].max()))
390+
max_departments = max(1, int(stats["n_departments"].max()))
391+
max_mediums = max(1, int(stats["n_mediums"].max()))
392+
max_on_view = max(1, int(stats["n_on_view"].max()))
393+
stats["prominence_score"] = (
394+
55 * stats["n_works"].map(lambda value: math.log1p(value) / math.log1p(max_works))
395+
+ 12 * (stats["n_departments"] / max_departments)
396+
+ 8 * (stats["n_mediums"] / max_mediums)
397+
+ 8 * (stats["decade_span"].clip(upper=160) / 160)
398+
+ 7 * stats["cataloged_share"]
399+
+ 5 * stats["has_wiki"].astype(int)
400+
+ 3 * stats["has_ulan"].astype(int)
401+
+ 2 * (stats["n_on_view"] / max_on_view)
402+
).round(2)
403+
return stats.sort_values(
404+
["prominence_score", "n_works", "artist_name"],
405+
ascending=[False, False, True],
406+
)
407+
408+
369409
def load_lookup_files() -> tuple[dict[str, str], dict[str, str]]:
370410
with (DATA_DIR / "nationality_to_iso3.json").open("r", encoding="utf-8") as handle:
371411
raw_nationality_to_iso = json.load(handle)
@@ -410,6 +450,8 @@ def build_credit_rows(artworks: pd.DataFrame, nationality_to_iso: dict[str, str]
410450
"classification": clean_token(getattr(row, "Classification")) or "Unknown",
411451
"medium": clean_token(getattr(row, "Medium")) or "Unknown",
412452
"medium_group": getattr(row, "medium_group"),
453+
"cataloged": clean_token(getattr(row, "Cataloged")),
454+
"on_view": clean_token(getattr(row, "OnView")),
413455
"nationality": nationality,
414456
"iso3": iso3,
415457
"country_name": ISO3_TO_COUNTRY.get(iso3, iso3) if iso3 else None,
@@ -456,6 +498,10 @@ def main() -> None:
456498

457499
credit_rows = build_credit_rows(artworks, nationality_to_iso, regions)
458500
credits = pd.DataFrame(credit_rows)
501+
wiki_lookup = artists_raw.set_index("ConstituentID")["Wiki QID"].notna().to_dict()
502+
ulan_lookup = artists_raw.set_index("ConstituentID")["ULAN"].notna().to_dict()
503+
credits["has_wiki"] = credits["artist_id"].map(wiki_lookup).fillna(False)
504+
credits["has_ulan"] = credits["artist_id"].map(ulan_lookup).fillna(False)
459505
n_total = len(artworks)
460506
n_credits = len(credits)
461507

@@ -514,7 +560,9 @@ def main() -> None:
514560
medium_counter = Counter(group["medium_group"])
515561
sorted_group = group.sort_values(["artist_name", "title", "year"], na_position="last")
516562
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])
563+
ranked_artists = prominence_table(group)
564+
featured_rank = ranked_artists.iloc[0]
565+
featured_artist_id = int(featured_rank["artist_id"])
518566
featured_group = group[group["artist_id"] == featured_artist_id].sort_values(["year", "title"], na_position="last")
519567
featured_sample = featured_group.iloc[0]
520568
featured_medium = Counter(featured_group["medium_group"]).most_common(1)[0][0]
@@ -539,6 +587,17 @@ def main() -> None:
539587
"featured_artist_id": featured_artist_id,
540588
"featured_artist": featured_sample["artist_name"],
541589
"featured_artist_n_works": int(len(featured_group)),
590+
"featured_artist_score": float(featured_rank["prominence_score"]),
591+
"featured_artist_score_method": (
592+
"Collection prominence score: 55% log work count, 12% department breadth, "
593+
"8% medium breadth, 8% dated decade span, 7% cataloged share, "
594+
"5% Wiki QID, 3% ULAN, 2% currently-on-view records."
595+
),
596+
"featured_artist_n_departments": int(featured_rank["n_departments"]),
597+
"featured_artist_n_mediums": int(featured_rank["n_mediums"]),
598+
"featured_artist_decade_span": int(json_ready(featured_rank["decade_span"]) or 0),
599+
"featured_artist_has_wiki": bool(featured_rank["has_wiki"]),
600+
"featured_artist_has_ulan": bool(featured_rank["has_ulan"]),
542601
"featured_artist_lifespan": lifespan(
543602
featured_sample["year_birth"],
544603
featured_sample["year_death"],
@@ -558,6 +617,7 @@ def main() -> None:
558617
featured_last,
559618
featured_sample["title"],
560619
featured_work_year,
620+
float(featured_rank["prominence_score"]),
561621
),
562622
}
563623
)

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-08T21:20:30+00:00
2+
generated_at: 2026-05-08T22:24:24+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: 113,489 bytes
45+
country_summary.json: 162,166 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-08T21:20:30+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-08T22:24:24+00:00","total_artworks":144149,"total_artists":11879,"unique_nationalities":129,"top_department":"Drawings & Prints"}

website/src/lib/charts/Globe.svelte

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
let dragging = $state(false);
1818
let dragStart = null;
1919
let rotateStart = null;
20+
let pointerDownCountryIndex = null;
21+
let pointerMoved = false;
2022
let frame = 0;
2123
let reduceMotion = false;
2224
@@ -101,18 +103,27 @@
101103
dragging = true;
102104
dragStart = [event.clientX, event.clientY];
103105
rotateStart = rotation;
106+
pointerMoved = false;
107+
pointerDownCountryIndex = event.target?.dataset?.countryIndex ?? null;
104108
event.currentTarget.setPointerCapture(event.pointerId);
105109
}
106110
107111
function onPointerMove(event) {
108112
if (!dragging || !dragStart || !rotateStart) return;
109113
const dx = event.clientX - dragStart[0];
110114
const dy = event.clientY - dragStart[1];
115+
if (Math.hypot(dx, dy) > 4) pointerMoved = true;
111116
rotation = [rotateStart[0] + dx * 0.35, Math.max(-65, Math.min(65, rotateStart[1] - dy * 0.35))];
112117
}
113118
114-
function onPointerUp() {
119+
function onPointerUp(event) {
120+
const countryIndex = pointerDownCountryIndex;
121+
const shouldSelect = dragging && !pointerMoved && countryIndex !== null;
115122
dragging = false;
123+
pointerDownCountryIndex = null;
124+
if (event.currentTarget.hasPointerCapture?.(event.pointerId))
125+
event.currentTarget.releasePointerCapture(event.pointerId);
126+
if (shouldSelect) centerCountry(countries[Number(countryIndex)]);
116127
}
117128
118129
onMount(async () => {
@@ -150,16 +161,17 @@
150161
onpointerup={onPointerUp}
151162
onpointercancel={onPointerUp}
152163
>
153-
{#each countries as country}
164+
{#each countries as country, index}
154165
{@const iso3 = worldAtlasIdToIso3[String(country.id).padStart(3, '0')]}
155166
<path
156167
d={path(country)}
168+
data-country-index={index}
169+
data-iso3={iso3}
157170
fill={fillFor(iso3)}
158171
class:selected={selectedCountry === iso3}
159172
class:clickable={Boolean(iso3 && countByIso[iso3])}
160173
onpointermove={(event) => showTooltip(event, country)}
161174
onpointerleave={hideTooltip}
162-
onclick={() => centerCountry(country)}
163175
onkeydown={(event) => {
164176
if (event.key === 'Enter' || event.key === ' ') {
165177
event.preventDefault();

website/src/lib/scenes/Scene2Globe.svelte

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
selectedSummary?.featured_story ??
2929
`${selectedSummary?.sample_artist} appears in the selected country's records with the representative work ${selectedSummary?.sample_work_title}.`,
3030
);
31+
let selectedScore = $derived(selectedSummary?.featured_artist_score ?? null);
3132
3233
function computeTopThreeShare(rows, decadeRange) {
3334
const counts = {};
@@ -116,7 +117,7 @@
116117
</div>
117118
</dl>
118119
<article class="artist-label">
119-
<p class="eyebrow">Most represented artist in this slice</p>
120+
<p class="eyebrow">Collection prominence pick</p>
120121
<div class="artist-heading">
121122
<span class="monogram" aria-hidden="true">{artistInitials(selectedArtistName)}</span>
122123
<div>
@@ -133,14 +134,38 @@
133134
</div>
134135
</div>
135136
<p class="story">{selectedStory}</p>
137+
{#if selectedScore !== null}
138+
<dl class="score-card" aria-label="Collection prominence score components">
139+
<div>
140+
<dt>Score</dt>
141+
<dd>{selectedScore.toFixed(1)}/100</dd>
142+
</div>
143+
<div>
144+
<dt>Works</dt>
145+
<dd>{selectedSummary.featured_artist_n_works.toLocaleString()}</dd>
146+
</div>
147+
<div>
148+
<dt>Breadth</dt>
149+
<dd>
150+
{selectedSummary.featured_artist_n_departments} departments · {selectedSummary.featured_artist_n_mediums}
151+
media
152+
</dd>
153+
</div>
154+
</dl>
155+
{/if}
136156
<p class="representative">
137157
<span>Representative record</span>
138158
<em>{selectedWorkTitle}</em>
139159
{#if selectedWorkYear}
140160
<span>{selectedWorkYear}</span>
141161
{/if}
142162
</p>
143-
<p class="caveat">This is a collection-count proxy, not a fame ranking.</p>
163+
<p class="caveat">
164+
This is a transparent collection-based proxy, not a true fame ranking.
165+
{#if selectedSummary.featured_artist_score_method}
166+
{selectedSummary.featured_artist_score_method}
167+
{/if}
168+
</p>
144169
{#if selectedArtistUrl}
145170
<a class="moma-link" href={selectedArtistUrl} target="_blank" rel="noopener">Open artist on MoMA</a>
146171
{/if}

website/src/lib/scenes/scene2Globe.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,34 @@
207207
line-height: var(--type-body-line);
208208
}
209209

210+
.artist-label .score-card {
211+
display: grid;
212+
grid-template-columns: repeat(3, minmax(0, 1fr));
213+
gap: var(--space-1);
214+
margin: var(--space-2) 0 0;
215+
padding: var(--space-1) 0 0;
216+
border-top: 1px solid color-mix(in srgb, var(--fg-on-dark-mute) 28%, transparent);
217+
}
218+
219+
.artist-label .score-card div {
220+
display: block;
221+
}
222+
223+
.artist-label .score-card dt {
224+
color: var(--fg-on-dark-mute);
225+
font-family: var(--font-ui);
226+
font-size: var(--type-small-size);
227+
text-transform: uppercase;
228+
}
229+
230+
.artist-label .score-card dd {
231+
margin: 0.125rem 0 0;
232+
color: var(--fg-on-dark-strong);
233+
font-family: var(--font-mono);
234+
font-size: var(--type-small-size);
235+
font-variant-numeric: tabular-nums;
236+
}
237+
210238
.artist-label .representative {
211239
display: grid;
212240
gap: 0.125rem;
@@ -289,6 +317,12 @@
289317
top: auto;
290318
bottom: var(--space-2);
291319
width: auto;
320+
max-height: calc(100svh - var(--space-4));
321+
overflow: auto;
322+
}
323+
324+
.artist-label .score-card {
325+
grid-template-columns: 1fr;
292326
}
293327
}
294328

0 commit comments

Comments
 (0)