Skip to content

Commit 96a30ba

Browse files
sdspiegclaude
andcommitted
Intuitive per-field color scheme for statement tag pills
Every annotated field now maps to one semantic color — 'line' in red, 'threat' in blood red, escalation in orange, rhetoric in purple, geography in blue, time in teal, values/theme in green, weapons in metallic. Tag pills render with a 3px left border + translucent background + colored text in the field's color, so the 15+ tag row on each card is scannable by field at a glance (all 'Line' pills are red, all 'Theme' pills are teal-green, etc.) rather than requiring users to read each label. New src/filterColors.ts exports FIELD_COLOR map + fieldColor(key) lookup. Removed the old value-level palette lookups (getDimValueColor + RRLS_COLORS / NTS_COLORS) from the tag rendering path — value-level coloring still used elsewhere (charts, drilldown slope chart, etc.) but made less sense here with 15+ fields per card. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2328e08 commit 96a30ba

3 files changed

Lines changed: 80 additions & 11 deletions

File tree

src/components/StatementDrilldown.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { RRLS_COLORS, NTS_COLORS, getDimValueColor } from '../colors';
21
import { confidencePillStyle } from '../confidence';
3-
import { RRLS_FILTERS, NTS_FILTERS, COLOR_KEY_FOR } from '../filterDefs';
2+
import { RRLS_FILTERS, NTS_FILTERS } from '../filterDefs';
3+
import { fieldColor } from '../filterColors';
44
import type { RRLSStatement, NTSStatement } from '../types';
55

66
interface DrilldownProps {
@@ -12,7 +12,6 @@ interface DrilldownProps {
1212

1313
export default function StatementDrilldown({ mode, title, statements, onClose }: DrilldownProps) {
1414
const shown = statements.slice(0, 50);
15-
const COLORS = mode === 'rrls' ? RRLS_COLORS : NTS_COLORS;
1615
const filterDefs = mode === 'rrls' ? RRLS_FILTERS : NTS_FILTERS;
1716

1817
return (
@@ -47,9 +46,9 @@ export default function StatementDrilldown({ mode, title, statements, onClose }:
4746
{filterDefs.map(f => {
4847
const val = s[f.key] as string | undefined;
4948
if (!val) return null;
50-
const c = getDimValueColor(COLORS, COLOR_KEY_FOR(f.key), val, 0);
49+
const c = fieldColor(f.key);
5150
return (
52-
<span key={f.key} className="tag" style={{ background: `${c}33`, color: c }}>
51+
<span key={f.key} className="tag" style={{ background: `${c}2a`, color: c, borderLeft: `3px solid ${c}` }}>
5352
{f.label}: {val}
5453
</span>
5554
);

src/components/Statements.tsx

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import { useEffect, useState, useMemo, useCallback, Fragment } from 'react';
22
import { load } from '../data';
3-
import { RRLS_COLORS, NTS_COLORS, getDimValueColor } from '../colors';
43
import { extractCountries } from '../countries';
54
import { confidencePillStyle } from '../confidence';
5+
import { RRLS_FILTERS, NTS_FILTERS } from '../filterDefs';
6+
import { fieldColor } from '../filterColors';
67
import ChartInfo from './ChartInfo';
78
import type { RRLSStatement, NTSStatement } from '../types';
89

910
type Mode = 'rrls' | 'nts';
1011

11-
import { RRLS_FILTERS, NTS_FILTERS, COLOR_KEY_FOR } from '../filterDefs';
12-
1312
function highlightText(text: string, query: string): React.ReactNode {
1413
if (!query) return text;
1514
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
@@ -359,15 +358,14 @@ export default function Statements() {
359358
<div className="stmt-text">{stmt.context_text_span ? highlightText(stmt.context_text_span, search) : '(no text)'}</div>
360359
{(() => {
361360
const s = stmt as unknown as Record<string, unknown>;
362-
const COLORS = mode === 'rrls' ? RRLS_COLORS : NTS_COLORS;
363361
return (
364362
<div className="stmt-tags">
365363
{filterDefs.map(f => {
366364
const val = s[f.key] as string | undefined;
367365
if (!val) return null;
368-
const c = getDimValueColor(COLORS, COLOR_KEY_FOR(f.key), val, 0);
366+
const c = fieldColor(f.key);
369367
return (
370-
<span key={f.key} className="tag" style={{ background: `${c}33`, color: c }}>
368+
<span key={f.key} className="tag" style={{ background: `${c}2a`, color: c, borderLeft: `3px solid ${c}` }}>
371369
{f.label}: {val}
372370
</span>
373371
);

src/filterColors.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Per-field color scheme for Statement Browser tag pills.
2+
//
3+
// Each annotated field maps to a single hex color, grouped into intuitive
4+
// semantic families so the 15+ tag row on every card is scan-able by
5+
// field at a glance.
6+
//
7+
// Families (rough mental model):
8+
// RED — "line" (red in red lines) and "threat" (scary, violent)
9+
// ORANGE — escalation, severity, warning
10+
// PURPLE — rhetoric, speech, statement type
11+
// ROSE — audience / who's being addressed
12+
// BLUE — geography, spatial context
13+
// TEAL/CYAN — time, duration, immediacy
14+
// GREEN — values, purpose, theme (what's being defended / discussed)
15+
// METALLIC — weapons capability, delivery systems, arms control
16+
// NEUTRAL — structure / meta (specificity, reciprocity, etc.)
17+
18+
export const FIELD_COLOR: Record<string, string> = {
19+
// Red — line + threat (the "red" in red lines + scary threats)
20+
line_type: '#e53935', // pure red
21+
line_intensity: '#c62828', // darker red
22+
threat_type: '#8e0000', // blood red
23+
threat_intensity: '#b71c1c', // crimson
24+
nts_threat_type: '#8e0000', // match threat_type
25+
26+
// Orange/amber — nature of threat, escalation, tone, consequences
27+
nature_of_threat: '#e65100', // deep orange
28+
level_of_escalation: '#ff8f00', // amber
29+
tone: '#d84315', // burnt orange (alarming speech)
30+
consequences: '#bf360c', // dark red-orange (outcomes)
31+
32+
// Purple — rhetoric, statement type
33+
rhetorical_device: '#8e24aa', // purple
34+
nts_statement_type: '#6a1b9a', // deep purple
35+
36+
// Rose — audience / target framing
37+
audience: '#d81b60', // rose
38+
39+
// Blue — geography / spatial context
40+
geopolitical_area_of_concern: '#1976d2', // blue
41+
geographical_reach: '#0d47a1', // navy
42+
context: '#455a64', // steel blue-gray
43+
44+
// Teal / cyan — time
45+
temporal_context: '#00897b', // teal
46+
timeline: '#00acc1', // cyan
47+
immediacy: '#0097a7', // darker cyan
48+
durability: '#5d4037', // brown (permanence)
49+
50+
// Green — theme / values / purpose
51+
theme: '#00796b', // teal-green
52+
underlying_values_or_interests: '#2e7d32', // forest green
53+
purpose: '#558b2f', // olive green
54+
55+
// Neutral/structural — specificity, reciprocity, conditionality, uni/multi
56+
specificity: '#546e7a', // slate
57+
conditionality: '#6d4c41', // warm brown
58+
reciprocity: '#5e35b1', // indigo-purple
59+
unilateral_vs_multilateral: '#303f9f', // deep indigo
60+
61+
// Metallic — weapons, capability, arms control
62+
capability: '#37474f', // steel
63+
delivery_system: '#263238', // gunmetal
64+
arms_control_and_testing: '#827717', // olive (arms control)
65+
};
66+
67+
// Fallback for any future filter field that hasn't been assigned yet.
68+
export const DEFAULT_FIELD_COLOR = '#607d8b';
69+
70+
export function fieldColor(key: string): string {
71+
return FIELD_COLOR[key] || DEFAULT_FIELD_COLOR;
72+
}

0 commit comments

Comments
 (0)