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/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
|
||||
|
||||
@@ -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`.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
@@ -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) || [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user