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:
co-authored by
Cursor
parent
8005252881
commit
6656f91f25
@@ -13,9 +13,18 @@ infra/deploy/last-docker-release.json
|
|||||||
# DB backups (may contain data)
|
# DB backups (may contain data)
|
||||||
db/DataBackup/
|
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
|
# Recovery / temp files from local restore
|
||||||
_extracted/
|
_extracted/
|
||||||
_parse_*.js
|
_parse_*.js
|
||||||
|
tmp-*.js
|
||||||
|
tmp-*.py
|
||||||
|
|
||||||
# Logs
|
# Logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -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:find-duplicates` | Duplicate / near-duplicate painting rows |
|
||||||
| `npm run dev:audit-influence-duplicates` | Duplicate influence edges |
|
| `npm run dev:audit-influence-duplicates` | Duplicate influence edges |
|
||||||
| `npm run dev:analyze-painter-palette` | CSV ↔ artist name match report |
|
| `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`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ Gallery/
|
|||||||
│ ├── painter-palette-lib.js
|
│ ├── painter-palette-lib.js
|
||||||
│ ├── import-painter-palette.js
|
│ ├── import-painter-palette.js
|
||||||
│ └── image-fetcher.js
|
│ └── image-fetcher.js
|
||||||
├── Inputs/ # External datasets (e.g. PainterPalette.csv)
|
├── Inputs/ # External datasets & influence workbooks (PainterPalette, book extracts, …)
|
||||||
├── Output/ # Generated exports (e.g. paintings.csv)
|
├── Output/ # Generated exports (CSV dumps, scratch extracts) — gitignored
|
||||||
├── data/images/ # Local portraits and paintings (+ thumbs/)
|
├── data/images/ # Local portraits and paintings (+ thumbs/)
|
||||||
├── db/ # schema.sql, setup-admin.sql, migrate-*.sql
|
├── db/ # schema.sql, setup-admin.sql, migrate-*.sql
|
||||||
├── server/migrate.js # npm run dev:migrate — schema + incremental migrations
|
├── 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 |
|
| Feature | Implementation |
|
||||||
|---------|----------------|
|
|---------|----------------|
|
||||||
| Lineage layout | `client/src/data/movement-lineage.ts` — curated predecessor→successor pairs (Met / ArtStory / museum essays); multiple parents allowed |
|
| 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) |
|
| 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 |
|
| 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 |
|
| 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 |
|
||||||
|
|||||||
@@ -434,13 +434,15 @@ Re-run `import-painter-palette` after adding gallery artists or updating the CSV
|
|||||||
|
|
||||||
## Catalog export
|
## Catalog export
|
||||||
|
|
||||||
Export the full painting catalog as CSV:
|
Export helpers write under **`Output/`** (gitignored — regenerate as needed):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run dev:export-paintings
|
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
|
## Image fetcher overrides
|
||||||
|
|
||||||
|
|||||||
@@ -30,17 +30,30 @@ Unresolved names (artists / movements / paintings not in the DB) are **skipped**
|
|||||||
|
|
||||||
### Presets
|
### Presets
|
||||||
|
|
||||||
| Preset | Typical headers |
|
| Preset | Typical headers | Example file |
|
||||||
|--------|-----------------|
|
|--------|-----------------|--------------|
|
||||||
| Web sources | `Artist`, `Painting`, `Influenced by`, `Influenced`, `Reference (source + link)` |
|
| 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) |
|
| Story of Art | Same Title Case (+ chapter reference column) | [`Inputs/story_of_art_influences.xlsx`](../Inputs/story_of_art_influences.xlsx) |
|
||||||
| Art influences | `artist`, `painting`, `influenced_by`, `influenced`, `reference` |
|
| Gariff influential painters | `artist`, `painting`, `influenced by`, `influenced`, `reference` | [`Inputs/gariff_influential_painters_influences.xlsx`](../Inputs/gariff_influential_painters_influences.xlsx) |
|
||||||
| Custom | Map any columns manually |
|
| 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).
|
Token classification order: **artist → movement → painting title** (under subject artist, then global).
|
||||||
|
|
||||||
Committed edges use `confidence=curated`, `discovered_via=import-wizard`.
|
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
|
### Duplicate file / data guard
|
||||||
|
|
||||||
Each successful commit stores SHA-256 fingerprints in `curator_audit_log` (`influence.import` details):
|
Each successful commit stores SHA-256 fingerprints in `curator_audit_log` (`influence.import` details):
|
||||||
|
|||||||
Binary file not shown.
@@ -45,7 +45,6 @@ interface BranchSegment {
|
|||||||
const MAX_STREAM_STROKE_PX = 54;
|
const MAX_STREAM_STROKE_PX = 54;
|
||||||
const MIN_STREAM_STROKE_PX = 28;
|
const MIN_STREAM_STROKE_PX = 28;
|
||||||
const LANE_GAP_PX = 10;
|
const LANE_GAP_PX = 10;
|
||||||
const DEPTH_GAP_PX = 14;
|
|
||||||
const CANVAS_TOP_PAD = 38;
|
const CANVAS_TOP_PAD = 38;
|
||||||
const CANVAS_BOTTOM_PAD = 24;
|
const CANVAS_BOTTOM_PAD = 24;
|
||||||
const DEFAULT_CANVAS_HEIGHT = 360;
|
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 nameToId = new Map(movements.map((m) => [m.name, m.id]));
|
||||||
const lineageParents = buildLineageParentMap(visibleMovements, nameToId);
|
const lineageParents = buildLineageParentMap(visibleMovements, nameToId);
|
||||||
const depths = assignDepths(visibleMovements, lineageParents);
|
const depths = assignDepths(visibleMovements, lineageParents);
|
||||||
const maxDepth = Math.max(...depths.values());
|
|
||||||
const layoutHeight = Math.max(200, canvasHeight);
|
const layoutHeight = Math.max(200, canvasHeight);
|
||||||
const usableHeight = layoutHeight - CANVAS_TOP_PAD - CANVAS_BOTTOM_PAD;
|
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) {
|
for (const movement of visibleMovements) {
|
||||||
const depth = depths.get(movement.id) ?? 0;
|
const lane = laneIndex.get(movement.id) ?? 0;
|
||||||
const list = byDepth.get(depth) || [];
|
maxLanes = Math.max(maxLanes, lane + 1);
|
||||||
list.push(movement);
|
laneOccupancy.set(lane, (laneOccupancy.get(lane) ?? 0) + 1);
|
||||||
byDepth.set(depth, list);
|
|
||||||
}
|
}
|
||||||
|
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 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);
|
let streamStrokePx = Math.min(MAX_STREAM_STROKE_PX, laneStep - LANE_GAP_PX);
|
||||||
streamStrokePx = Math.max(MIN_STREAM_STROKE_PX, streamStrokePx);
|
streamStrokePx = Math.max(MIN_STREAM_STROKE_PX, streamStrokePx);
|
||||||
const portraitSizePx = Math.max(28, Math.min(52, streamStrokePx * 0.96));
|
const portraitSizePx = Math.max(28, Math.min(52, streamStrokePx * 0.96));
|
||||||
const streamCurveOffset = Math.min(12, Math.max(2, (laneStep - streamStrokePx) * 0.35));
|
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 layoutById = new Map<number, MovementLayout>();
|
||||||
const branchList: BranchSegment[] = [];
|
const branchList: BranchSegment[] = [];
|
||||||
const childIdsByParent = new Map<number, number[]>();
|
const childIdsByParent = new Map<number, number[]>();
|
||||||
@@ -1017,10 +994,8 @@ export default function MovementBands({
|
|||||||
|
|
||||||
const depth = depths.get(movement.id) ?? 0;
|
const depth = depths.get(movement.id) ?? 0;
|
||||||
const lane = laneIndex.get(movement.id) ?? 0;
|
const lane = laneIndex.get(movement.id) ?? 0;
|
||||||
const lanesAtDepth = maxLanesByDepth.get(depth) ?? 1;
|
const y = CANVAS_TOP_PAD + lane * laneStep + laneStep / 2;
|
||||||
const bandTop = depthBaseY.get(depth) ?? CANVAS_TOP_PAD;
|
const allowDrift = (laneOccupancy.get(lane) ?? 0) === 1 && laneStep >= 80;
|
||||||
const y = bandTop + lane * laneStep + laneStep / 2;
|
|
||||||
const allowDrift = lanesAtDepth === 1 && laneStep >= 80;
|
|
||||||
const yEnd = y + (allowDrift ? organicDrift(movement.id + 1000) * 0.22 : 0);
|
const yEnd = y + (allowDrift ? organicDrift(movement.id + 1000) * 0.22 : 0);
|
||||||
const parentIds = lineageParents.get(movement.id) || [];
|
const parentIds = lineageParents.get(movement.id) || [];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user