diff --git a/.gitignore b/.gitignore index ec7bed2..eb6a242 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,18 @@ infra/deploy/last-docker-release.json # DB backups (may contain data) db/DataBackup/ +# Generated exports / scratch (CSV dumps, PDF text extracts) +Output/ +__pycache__/ + +# Large local book PDFs (keep workbooks under Inputs/*.xlsx) +Inputs/*.pdf + # Recovery / temp files from local restore _extracted/ _parse_*.js +tmp-*.js +tmp-*.py # Logs *.log diff --git a/Documentation/FAC.md b/Documentation/FAC.md index f079480..c9c7363 100644 --- a/Documentation/FAC.md +++ b/Documentation/FAC.md @@ -213,7 +213,9 @@ npm run dev:discover-influences # discovery only, no curated insert | `npm run dev:find-duplicates` | Duplicate / near-duplicate painting rows | | `npm run dev:audit-influence-duplicates` | Duplicate influence edges | | `npm run dev:analyze-painter-palette` | CSV ↔ artist name match report | -| `npm run dev:export-paintings` | Write `Output/paintings.csv` | +| `npm run dev:export-paintings` | Write `Output/paintings.csv` (gitignored folder) | + +**Influence workbooks** (curator import): see [influence-import.md](influence-import.md) — e.g. `Inputs/gariff_influential_painters_influences.xlsx`, `Inputs/story_of_art_influences.xlsx`. --- diff --git a/Documentation/basics.md b/Documentation/basics.md index ee3a3b8..0f91ec2 100644 --- a/Documentation/basics.md +++ b/Documentation/basics.md @@ -76,8 +76,8 @@ Gallery/ │ ├── painter-palette-lib.js │ ├── import-painter-palette.js │ └── image-fetcher.js -├── Inputs/ # External datasets (e.g. PainterPalette.csv) -├── Output/ # Generated exports (e.g. paintings.csv) +├── Inputs/ # External datasets & influence workbooks (PainterPalette, book extracts, …) +├── Output/ # Generated exports (CSV dumps, scratch extracts) — gitignored ├── data/images/ # Local portraits and paintings (+ thumbs/) ├── db/ # schema.sql, setup-admin.sql, migrate-*.sql ├── server/migrate.js # npm run dev:migrate — schema + incremental migrations @@ -246,7 +246,7 @@ Each visible movement is drawn as a **portrait-width curved stream** (~54 px str | Feature | Implementation | |---------|----------------| | Lineage layout | `client/src/data/movement-lineage.ts` — curated predecessor→successor pairs (Met / ArtStory / museum essays); multiple parents allowed | -| Vertical depth | Successor movements sit on rows below their deepest parent; sibling movements at the same depth are spread into lanes to limit overlap | +| Vertical lanes | Movements whose time spans do **not** overlap (in the current zoom) share a horizontal lane; only concurrent spans stack into extra rows (`assignTemporalLanes` in `MovementBands.tsx`). Lineage depth still drives branch curves, not exclusive vertical bands | | Branch connectors | Smooth curves from the **centre** of a parent stream to the **centre** of each child stream (siblings fan out along the parent’s length) | | Visual blending | Path-aligned SVG gradients with transparent fades at stream ends and branch junctions; streams draw on top of branches so overlap brightness stays uniform | | Filtering | A movement is drawn when its **span overlaps** the visible year range **and** it has at least one catalogued artist — artists whose lifespan falls outside the window still keep their movement visible (their portraits simply do not render). Filtered client-side after initial load | diff --git a/Documentation/data-and-images.md b/Documentation/data-and-images.md index c2dd14d..794ad3f 100644 --- a/Documentation/data-and-images.md +++ b/Documentation/data-and-images.md @@ -434,13 +434,15 @@ Re-run `import-painter-palette` after adding gallery artists or updating the CSV ## Catalog export -Export the full painting catalog as CSV: +Export helpers write under **`Output/`** (gitignored — regenerate as needed): ```bash npm run dev:export-paintings ``` -Writes **`Output/paintings.csv`** with columns `artist`, `painting`, `year` (sorted by artist, year, title). The `Output/` folder is git-ignored by convention; regenerate after catalog changes. +Writes **`Output/paintings.csv`** with columns `artist`, `painting`, `year` (sorted by artist, year, title). + +Ad-hoc **prod** dumps (movements / artists / paintings) can also be written to `Output/*.csv` for offline review. Influence workbooks ready for the curator import wizard belong under **`Inputs/`** (e.g. Gariff, Story of Art) — see [influence-import.md](influence-import.md). ## Image fetcher overrides diff --git a/Documentation/influence-import.md b/Documentation/influence-import.md index 944da4b..61610da 100644 --- a/Documentation/influence-import.md +++ b/Documentation/influence-import.md @@ -30,17 +30,30 @@ Unresolved names (artists / movements / paintings not in the DB) are **skipped** ### Presets -| Preset | Typical headers | -|--------|-----------------| -| Web sources | `Artist`, `Painting`, `Influenced by`, `Influenced`, `Reference (source + link)` | -| Story of Art | Same Title Case (+ chapter reference column) | -| Art influences | `artist`, `painting`, `influenced_by`, `influenced`, `reference` | -| Custom | Map any columns manually | +| Preset | Typical headers | Example file | +|--------|-----------------|--------------| +| Web sources | `Artist`, `Painting`, `Influenced by`, `Influenced`, `Reference (source + link)` | [`Inputs/artist_influences_web_sources.xlsx`](../Inputs/artist_influences_web_sources.xlsx) | +| Story of Art | Same Title Case (+ chapter reference column) | [`Inputs/story_of_art_influences.xlsx`](../Inputs/story_of_art_influences.xlsx) | +| Gariff influential painters | `artist`, `painting`, `influenced by`, `influenced`, `reference` | [`Inputs/gariff_influential_painters_influences.xlsx`](../Inputs/gariff_influential_painters_influences.xlsx) | +| Art influences | `artist`, `painting`, `influenced_by`, `influenced`, `reference` | [`Inputs/art_influences.xlsx`](../Inputs/art_influences.xlsx) | +| Custom | Map any columns manually | — | Token classification order: **artist → movement → painting title** (under subject artist, then global). Committed edges use `confidence=curated`, `discovered_via=import-wizard`. +### Book extract: Gariff (2008) + +[`Inputs/gariff_influential_painters_influences.xlsx`](../Inputs/gariff_influential_painters_influences.xlsx) is distilled from David Gariff et al., *The World's Most Influential Painters and the Artists They Inspired* (Herbert Press / Quarto, 2008; local PDF `Inputs/1.pdf`, not committed). + +- Columns match the import wizard (`artist` / `painting` / `influenced by` / `influenced` / `reference`). +- **`artist` and `painting`** are limited to catalog slices in `Inputs/artistslistdbdata-*.csv` and `Inputs/paintinglistdbdata-*.csv` (regenerate those from prod/dev as needed). +- **`influenced by` / `influenced`** may name artists, movements, or works from the book (including entities outside those CSVs). +- Multiple rows per subject when the book states several influences. +- Import via curator **Influences → Import** (Art influences / custom mapping) or keep as a curated source workbook. + +See also [`Inputs/story_of_art_influences.xlsx`](../Inputs/story_of_art_influences.xlsx) (Gombrich) and optional local `Inputs/janson_short_history_influences.xlsx` (same column shape). + ### Duplicate file / data guard Each successful commit stores SHA-256 fingerprints in `curator_audit_log` (`influence.import` details): diff --git a/Inputs/gariff_influential_painters_influences.xlsx b/Inputs/gariff_influential_painters_influences.xlsx new file mode 100644 index 0000000..41ee71e Binary files /dev/null and b/Inputs/gariff_influential_painters_influences.xlsx differ diff --git a/client/src/components/MovementBands.tsx b/client/src/components/MovementBands.tsx index 4340439..4f1a566 100644 --- a/client/src/components/MovementBands.tsx +++ b/client/src/components/MovementBands.tsx @@ -45,7 +45,6 @@ interface BranchSegment { const MAX_STREAM_STROKE_PX = 54; const MIN_STREAM_STROKE_PX = 28; const LANE_GAP_PX = 10; -const DEPTH_GAP_PX = 14; const CANVAS_TOP_PAD = 38; const CANVAS_BOTTOM_PAD = 24; const DEFAULT_CANVAS_HEIGHT = 360; @@ -961,51 +960,29 @@ export default function MovementBands({ const nameToId = new Map(movements.map((m) => [m.name, m.id])); const lineageParents = buildLineageParentMap(visibleMovements, nameToId); const depths = assignDepths(visibleMovements, lineageParents); - const maxDepth = Math.max(...depths.values()); const layoutHeight = Math.max(200, canvasHeight); const usableHeight = layoutHeight - CANVAS_TOP_PAD - CANVAS_BOTTOM_PAD; - const byDepth = new Map(); + // Pack all visible movements into shared horizontal lanes whenever their + // clipped time spans do not overlap (lineage depth is kept for branch curves only). + const laneIndex = assignTemporalLanes(visibleMovements, viewStart, viewEnd); + let maxLanes = 0; + const laneOccupancy = new Map(); for (const movement of visibleMovements) { - const depth = depths.get(movement.id) ?? 0; - const list = byDepth.get(depth) || []; - list.push(movement); - byDepth.set(depth, list); + const lane = laneIndex.get(movement.id) ?? 0; + maxLanes = Math.max(maxLanes, lane + 1); + laneOccupancy.set(lane, (laneOccupancy.get(lane) ?? 0) + 1); } + maxLanes = Math.max(1, maxLanes); - const laneIndex = new Map(); - const maxLanesByDepth = new Map(); - for (const [depth, group] of byDepth.entries()) { - const temporalLanes = assignTemporalLanes(group, viewStart, viewEnd); - let maxLane = 0; - for (const movement of group) { - const lane = temporalLanes.get(movement.id) ?? 0; - laneIndex.set(movement.id, lane); - maxLane = Math.max(maxLane, lane + 1); - } - maxLanesByDepth.set(depth, Math.max(1, maxLane)); - } - - const depthCount = maxDepth + 1; - const depthGaps = Math.max(0, depthCount - 1) * DEPTH_GAP_PX; - const totalLaneSlots = [...maxLanesByDepth.values()].reduce((sum, n) => sum + n, 0); const minLaneStep = MIN_STREAM_STROKE_PX + LANE_GAP_PX; - const laneStep = Math.max(minLaneStep, (usableHeight - depthGaps) / Math.max(1, totalLaneSlots)); + const laneStep = Math.max(minLaneStep, usableHeight / maxLanes); let streamStrokePx = Math.min(MAX_STREAM_STROKE_PX, laneStep - LANE_GAP_PX); streamStrokePx = Math.max(MIN_STREAM_STROKE_PX, streamStrokePx); const portraitSizePx = Math.max(28, Math.min(52, streamStrokePx * 0.96)); const streamCurveOffset = Math.min(12, Math.max(2, (laneStep - streamStrokePx) * 0.35)); - const depthBaseY = new Map(); - let yCursor = CANVAS_TOP_PAD; - for (let depth = 0; depth <= maxDepth; depth++) { - depthBaseY.set(depth, yCursor); - const bandLanes = maxLanesByDepth.get(depth) ?? 1; - yCursor += bandLanes * laneStep; - if (depth < maxDepth) yCursor += DEPTH_GAP_PX; - } - const layoutById = new Map(); const branchList: BranchSegment[] = []; const childIdsByParent = new Map(); @@ -1017,10 +994,8 @@ export default function MovementBands({ const depth = depths.get(movement.id) ?? 0; const lane = laneIndex.get(movement.id) ?? 0; - const lanesAtDepth = maxLanesByDepth.get(depth) ?? 1; - const bandTop = depthBaseY.get(depth) ?? CANVAS_TOP_PAD; - const y = bandTop + lane * laneStep + laneStep / 2; - const allowDrift = lanesAtDepth === 1 && laneStep >= 80; + const y = CANVAS_TOP_PAD + lane * laneStep + laneStep / 2; + const allowDrift = (laneOccupancy.get(lane) ?? 0) === 1 && laneStep >= 80; const yEnd = y + (allowDrift ? organicDrift(movement.id + 1000) * 0.22 : 0); const parentIds = lineageParents.get(movement.id) || [];