Document Gariff influence workbook and pack timeline lanes by time span.

Add Inputs/gariff_influential_painters_influences.xlsx for curator import, document Inputs/Output conventions, and let non-overlapping movements share horizontal lanes on the timeline.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-07-27 10:32:59 +03:00
co-authored by Cursor
parent 8005252881
commit 6656f91f25
7 changed files with 50 additions and 49 deletions
+9
View File
@@ -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
+3 -1
View File
@@ -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`.
---
+3 -3
View File
@@ -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 parents 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 |
+4 -2
View File
@@ -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
+19 -6
View File
@@ -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):
Binary file not shown.
+12 -37
View File
@@ -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<number, ArtMovement[]>();
// 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<number, number>();
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<number, number>();
const maxLanesByDepth = new Map<number, number>();
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<number, number>();
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<number, MovementLayout>();
const branchList: BranchSegment[] = [];
const childIdsByParent = new Map<number, number[]>();
@@ -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) || [];