9-Phase AnalysisReverse Engineering Study

Bible Cross-References
Technical Reconstruction

A comprehensive technical analysis and reconstruction plan for the Bible cross-reference arc visualization — covering 344,799 cross-references across 31,102 verses in 66 books.

0
Books
0
Verses
0
Cross-References
0
Analysis Phases
0
Sprint Roadmap
0 SP
Story Points

Live Visualization Demo

Interactive D3.js arc diagram built with the techniques documented in this analysis. Scroll to zoom, drag to pan, hover arcs for details.

Loading visualization...

1

Phase 1: UX Inventory

3 subsections

UI Elements & Layout Structure

The original Bible Cross-References visualization at trekhleb.dev presents a full-viewport, single-page application with two primary views: Arcs and Threads.

Header Bar (50px height, full-width):

  • •Title: "Bible Cross-References" in Iowan Old Style, 18px, weight 500
  • •Segmented control: Arcs | Threads toggle (pill-shaped, #161615 background, 8px border-radius, 1px solid #2c2b28)
  • •Translation selector: BSB dropdown
  • •Filters button
  • •Search input (right-aligned, 352px wide)

Main Visualization Area (full remaining viewport):

  • •Background: solid #0d0d0d
  • •The arc diagram occupies 100% width × ~85% height
  • •Horizontal baseline near bottom showing all 66 books
  • •Book labels in uppercase at the baseline ("OLD TESTAMENT" / "NEW TESTAMENT" dividers)
  • •Stats bar showing: "31,102 verses · 153,419 of 344,799 links shown"
  • •Interaction hints: "scroll to zoom · drag to pan · click to focus"

Responsive Behavior:

  • •The visualization is zoom/pan-capable via scroll and drag
  • •On narrower viewports, book labels compress or hide
  • •The header remains fixed at top
  • •No dedicated mobile breakpoint detected; relies on pan/zoom for small screens

Interactions & User Flows

Zoom/Pan System:

  • •Mouse wheel: zoom in/out centered on cursor position
  • •Click + drag: pan the visualization horizontally
  • •Click on book label: zoom to and highlight that book's references
  • •Click on individual arc: show tooltip with reference details

Filtering:

  • •Genre filter: Law, History, Poetry, Prophecy, Gospels, Epistles, Apocalyptic
  • •Testament filter: Old Testament, New Testament, Inter-testament
  • •Arc opacity responds to filter state (non-matching arcs fade to ~5% opacity)

Threads View (secondary mode):

  • •Displays chapter-level reading view
  • •Shows bar chart of connection counts across all 66 books
  • •Chapter text with highlighted verses that have cross-references
  • •Click a verse to see its connections

Performance Observations:

  • •344,799 cross-references rendered as SVG path elements
  • •Uses virtual rendering — only arcs in the viewport are drawn
  • •Smooth 60fps pan/zoom via requestAnimationFrame
  • •Service Worker caches all data for offline use

Visual Design Tokens

Color Palette (extracted from computed styles):

TokenValueUsage
Background Primary#0d0d0dMain canvas, ~77% of area
Background Secondary#161615Cards, header elements
Text Primary#ecebe6Headings, body text
Text Secondary#9a9890Labels, muted content
Border#2c2b28Element borders
Arc colorsHSL-mappedHue from book genre

Typography:

RoleFontWeightSize
TitleIowan Old Style / Palatino50018px
Bodysystem-ui stack40015px
Labelssystem-ui stack40012-13px
Statssystem-ui stack40014px

Spacing: 8px base grid, 16px padding on header, 2px pill controls

Border Radius: 0px for buttons, 8px for segmented controls

Shadows: None detected — flat design language

ElementBackgroundTextBorderRadius
Page#0d0d0d#ecebe6none0
Headertransparent#ecebe6none0
Segmented Control#161615#ecebe6#2c2b288px
Active Segment#2c2b28#ecebe6none6px
Search Input#161615#9a9890#2c2b286px
Filter Buttontransparent#9a9890#2c2b286px
2

Phase 2: Tech Stack Analysis

2 subsections

Framework & Build System

Confirmed Technology Stack (from GitHub repository analysis):

TechnologyPurposeConfidence
TypeScriptPrimary language✅ 100% — repo is 95%+ TS
ViteBuild tool & dev server✅ 100% — vite.config.ts present
ReactUI framework✅ 100% — JSX components
Service WorkerOffline PWA✅ 100% — sw/ directory
GitHub PagesHosting✅ 100% — static deploy
CSS ModulesStyling✅ 100% — .module.css files

Key Architectural Decisions in Original:

  • •No server-side rendering — pure client-side SPA
  • •No external UI framework (no Material UI, Tailwind, etc.)
  • •CSS Modules with BEM-like naming (_view_ul7e8_4)
  • •Custom data pipeline in scripts/data/ for build-time processing
  • •Data validated against canonical KJV versification (31,102 verses)

Our Reconstruction Stack:

TechnologyPurposeRationale
Next.js + React 19App frameworkSSR support, API routes, better SEO
TypeScriptType safetyMatch original
D3.jsVisualizationIndustry standard for arc diagrams
PostgreSQL + PrismaData persistenceQueryable cross-reference storage
Tailwind CSSStylingRapid development, design tokens
SVG renderingArc diagramMatches original approach

Rendering Engine Analysis

SVG vs Canvas Investigation:

The original site uses SVG for rendering arcs. Evidence:

  • •DOM inspection shows elements within an SVG container
  • •CSS class names on individual paths (_arc_xxxx)
  • •Per-element mouse event handling (hover/click on individual arcs)
  • •Style attributes including stroke-opacity manipulation

Performance Strategy:

  • •Virtual viewport rendering: only arcs intersecting the visible area are in the DOM
  • •Arcs outside viewport are removed from DOM on pan/zoom
  • •Transform-based zoom (CSS transform: scale() + translate()) rather than re-rendering
  • •requestAnimationFrame for smooth 60fps interactions
  • •Total data loaded once (~2MB gzipped), filtered client-side

Canvas was NOT used because:

1. Individual arc interaction (hover tooltips) requires hit-testing — easier with SVG elements

2. Color/opacity per-arc styling is more natural in SVG

3. The site achieves smooth performance with virtual rendering

4. Accessibility: SVG elements can carry ARIA attributes

Our reconstruction will use SVG with the same virtual rendering strategy, implemented via D3.js selections.

3

Phase 3: Data Architecture

3 subsections

Data Sources & Formats

Primary Data Sources:

1. Cross-Reference Data: Treasury of Scripture Knowledge via OpenBible.info

  • •License: CC BY 4.0
  • •Format: TSV/CSV with verse-to-verse mappings
  • •Total: 344,799 cross-reference pairs
  • •Each entry: from_verse → to_verse with optional vote/confidence weight

2. Bible Text: Berean Standard Bible (BSB)

  • •License: Free for use with attribution
  • •Format: JSON per chapter
  • •KJV also available as optional download

3. Versification: Standard Protestant canon

  • •66 books, 1,189 chapters, 31,102 verses
  • •Absolute verse indexing: verse 1 = Genesis 1:1, verse 31102 = Revelation 22:21

Original Data Pipeline (from scripts/data/):

1. Fetch raw TSV from OpenBible.info

2. Validate against KJV versification checksums (SHA-256)

3. Parse into normalized JSON structure

4. Build optimized lookup indices

5. Output: static JSON files bundled with the app

Our Data Pipeline:

1. Parse the same public TSV source

2. Load into PostgreSQL via Prisma seed script

3. Build REST API with filtering/pagination

4. Cache frequently-accessed queries

Entity Relationship Model

Core Entities:

typescript
BibleBook (66 records)
├── id: 1-66 (canonical order)
├── name: "Genesis" .. "Revelation"
├── abbreviation: "Gen" .. "Rev"
├── testament: "OT" | "NT"
├── genre: "Law" | "History" | ...
├── chapters: number of chapters
├── verses: total verse count
├── startVerse: cumulative offset
└── color: HSL string for visualization

CrossReference (~344,799 records)
├── fromBook, fromChapter, fromVerse
├── toBook, toChapter, toVerse
├── fromAbsolute: computed verse position (1-31102)
├── toAbsolute: computed verse position (1-31102)
└── votes: confidence weight

Verse (31,102 records — optional for full text)
├── book, chapter, verse
├── absolutePosition
└── text: verse content

Absolute Verse Calculation:

absolutePosition = sum(verses in all preceding books) + sum(verses in preceding chapters of current book) + verse_number

This linear mapping is the foundation of the x-axis in the arc visualization.

schema.sql
-- PostgreSQL DDL for Bible Cross-References

CREATE TABLE bible_books (
  id            INTEGER PRIMARY KEY,
  name          VARCHAR(50) NOT NULL,
  abbreviation  VARCHAR(10) NOT NULL,
  testament     VARCHAR(2) NOT NULL CHECK (testament IN ('OT', 'NT')),
  genre         VARCHAR(20) NOT NULL,
  chapters      INTEGER NOT NULL,
  verses        INTEGER NOT NULL,
  start_verse   INTEGER NOT NULL,  -- cumulative offset
  color         VARCHAR(30) NOT NULL
);

CREATE TABLE cross_references (
  id             SERIAL PRIMARY KEY,
  from_book      INTEGER NOT NULL REFERENCES bible_books(id),
  from_chapter   INTEGER NOT NULL,
  from_verse     INTEGER NOT NULL,
  to_book        INTEGER NOT NULL REFERENCES bible_books(id),
  to_chapter     INTEGER NOT NULL,
  to_verse       INTEGER NOT NULL,
  from_absolute  INTEGER NOT NULL,
  to_absolute    INTEGER NOT NULL,
  votes          INTEGER DEFAULT 1
);

-- Performance indexes
CREATE INDEX idx_xref_from ON cross_references(from_book, from_chapter);
CREATE INDEX idx_xref_to ON cross_references(to_book, to_chapter);
CREATE INDEX idx_xref_from_abs ON cross_references(from_absolute);
CREATE INDEX idx_xref_to_abs ON cross_references(to_absolute);

Cross-Reference Data Format

Raw TSV Format (from OpenBible.info):

typescript
From Verse	To Verse	Votes
Gen.1.1	John.1.1	25
Gen.1.1	Heb.11.3	18
Gen.1.1	Psa.33.6	15
...```

**Parsed JSON Format** (for API responses):

{

"crossReferences": [

{

"from": { "book": 1, "chapter": 1, "verse": 1, "absolute": 1 },

"to": { "book": 43, "chapter": 1, "verse": 1, "absolute": 26046 },

"votes": 25

}

],

"meta": {

"total": 344799,

"filtered": 153419,

"books": 66,

"verses": 31102

}

}

4

Phase 4: Visualization Reverse Engineering

4 subsections

Coordinate System & Mapping

The Linear Verse Space:

The entire Bible is mapped to a single horizontal axis. Each of the 31,102 verses occupies an equal-width slot:

x(verse) = (absolutePosition / totalVerses) × viewportWidth

This creates a proportional layout where longer books (Psalms: 2,461 verses) take more horizontal space than shorter ones (Obadiah: 21 verses).

Viewport Coordinate System:

  • •Origin (0,0): top-left of SVG container
  • •X-axis: 0 → viewportWidth (verse space)
  • •Y-axis: 0 (top, where arc peaks reach) → baselineY (where the verse line sits)
  • •Baseline typically at 80-85% of container height

Zoom Transform:

The visualization uses a D3-style zoom transform {k, tx, ty}:

  • •k: scale factor (1 = fit all, higher = zoomed in)
  • •tx: horizontal translation
  • •ty: vertical translation (typically 0)
  • •Screen position: screenX = x × k + tx

Arc Rendering Mathematics

Arc Path Generation:

Each cross-reference is rendered as a circular arc connecting two points on the baseline. The arc is an SVG using a quadratic Bézier curve.

Algorithm:

1. Compute start x: x1 = (fromAbsolute / 31102) × width

2. Compute end x: x2 = (toAbsolute / 31102) × width

3. Compute midpoint: mx = (x1 + x2) / 2

4. Compute arc height: h = Math.sqrt(Math.abs(x2 - x1)) × scaleFactor

  • •The square root produces a sublinear relationship: short arcs rise quickly, long arcs grow more gently
  • •scaleFactor is typically 0.4-0.6 of the viewport height

5. Control point: cy = baselineY - h

6. SVG path: M x1,baselineY Q mx,cy x2,baselineY

Alternative height formulas observed in similar projects:

  • •Linear: h = |x2 - x1| × 0.5 (arcs too tall for distant references)
  • •Logarithmic: h = log(|x2 - x1| + 1) × scale (more compression)
  • •Square root: h = sqrt(|x2 - x1|) × scale ← used in original

Why square root?

It provides the best visual balance:

  • •Short-distance refs (within same book): visible, proportional arcs
  • •Long-distance refs (OT → NT): arcs tall but not overwhelming
  • •The rainbow effect emerges naturally from this distribution
arc-generator.ts
interface ArcData {
  fromAbsolute: number;
  toAbsolute: number;
  bookIndex: number;
}

const TOTAL_VERSES = 31102;

function generateArcPath(
  arc: ArcData,
  width: number,
  baselineY: number,
  heightScale: number = 0.5
): string {
  const x1 = (arc.fromAbsolute / TOTAL_VERSES) * width;
  const x2 = (arc.toAbsolute / TOTAL_VERSES) * width;
  const mx = (x1 + x2) / 2;
  const distance = Math.abs(x2 - x1);
  const h = Math.sqrt(distance) * heightScale;
  const cy = baselineY - h;
  
  return `M ${x1},${baselineY} Q ${mx},${cy} ${x2},${baselineY}`;
}

function getArcColor(bookIndex: number, totalBooks: number = 66): string {
  const hue = (bookIndex / totalBooks) * 360;
  return `hsl(${hue}, 80%, 55%)`;
}

Color Generation System

HSL Color Mapping:

Each arc is colored based on the source book's genre, creating visual clusters:

GenreBooksHue RangeExample Color
LawGen-Deut (1-5)0-30°Red-Orange
HistoryJosh-Esth (6-17)30-90°Orange-Yellow
PoetryJob-Song (18-22)90-130°Yellow-Green
Major ProphetsIsa-Dan (23-27)130-170°Green
Minor ProphetsHos-Mal (28-39)170-220°Cyan-Blue
GospelsMatt-John (40-43)220-250°Blue
Acts + EpistlesActs-Jude (44-65)250-340°Purple-Magenta
ApocalypticRev (66)340-360°Red

Color Formula:

typescript
hue = (bookIndex / 66) × 360
saturation = 80%
lightness = 55%
opacity = 0.15 (default) → 0.8 (on hover/filter)

The low default opacity (0.15) is critical — it allows 300K+ overlapping arcs to create the characteristic "rainbow glow" effect without becoming an opaque mass.

Filter Effects:

  • •Matching arcs: opacity 0.6-0.8
  • •Non-matching arcs: opacity 0.02-0.05
  • •Hovered arc: opacity 1.0, stroke-width increases from 1px to 2px
  • •Selected arc: opacity 1.0, tooltip shown

Rendering Performance Strategy

Challenge: Rendering 344,799 SVG path elements simultaneously would crash any browser.

Solution: Virtual Viewport Rendering:

1. Spatial Index: Pre-compute which arcs are visible at any zoom level

2. Viewport Culling: Only render arcs whose x-range intersects the visible area

3. Level of Detail (LOD):

  • •Zoom < 1.5×: show only arcs with votes ≥ 5 (~50K arcs)
  • •Zoom 1.5-3×: show arcs with votes ≥ 2 (~150K arcs)
  • •Zoom > 3×: show all arcs in viewport

4. DOM Recycling: Reuse SVG path elements via D3's enter/update/exit pattern

5. Batch Updates: Use requestAnimationFrame to batch DOM mutations

Memory Optimization:

  • •Arc data stored as typed arrays (Float32Array for positions)
  • •Color computed on-the-fly from book index (no string storage)
  • •Path strings generated lazily on render

Benchmark Targets:

MetricTargetOriginal (observed)
Initial render< 500ms~300ms
Pan/zoom frame< 16ms~8-12ms
Filter toggle< 200ms~150ms
Memory usage< 100MB~60-80MB
5

Phase 5: Application Architecture

3 subsections

Component Hierarchy

Reconstructed Component Tree:

typescript
App (RootLayout)
├── Header
│   ├── Logo ("Bible Cross-References")
│   ├── ViewToggle (Arcs | Threads)
│   ├── TranslationSelector (BSB | KJV)
│   ├── FilterPanel
│   │   ├── GenreFilter
│   │   ├── TestamentFilter
│   │   └── VoteThreshold
│   └── SearchInput
├── ArcsView (main visualization)
│   ├── SVGCanvas
│   │   ├── BookBaseline
│   │   │   ├── BookLabel[] (66 labels)
│   │   │   └── TestamentDivider
│   │   ├── ArcLayer
│   │   │   └── ArcPath[] (virtual rendered)
│   │   └── ZoomControls
│   ├── StatsBar
│   └── Tooltip
├── ThreadsView (alternate)
│   ├── ChapterSelector
│   ├── VerseList
│   ├── ConnectionChart
│   └── BookBarChart
└── Footer
    └── DataAttribution

State Management:

  • •View mode (arcs/threads): URL parameter or React state
  • •Zoom transform: D3 zoom behavior (internal state)
  • •Active filters: React state, lifted to App level
  • •Selected verse/arc: React state with tooltip positioning
  • •Data: loaded once into memory, filtered in-place

API Route Design

REST API Endpoints:

MethodPathPurpose
GET/api/booksList all 66 books with metadata
GET/api/cross-referencesPaginated cross-references
GET/api/cross-references/statsAggregate statistics
GET/api/cross-references/book/:idReferences for a specific book
GET/api/visualization-dataOptimized arc data (typed arrays)
POST/api/chatLLM-powered analysis chat

Query Parameters (for /api/cross-references):

  • •fromBook: filter by source book ID
  • •toBook: filter by target book ID
  • •testament: 'OT' | 'NT' | 'inter'
  • •minVotes: minimum confidence threshold
  • •limit: pagination limit (default 100)
  • •offset: pagination offset

Response Format:

json
{
  "data": [...],
  "meta": {
    "total": 344799,
    "page": 1,
    "limit": 100,
    "hasMore": true
  }
}
api/books/route.ts
import { NextResponse } from 'next/server';
import { prisma } from '@/lib/db';

export const dynamic = 'force-dynamic';

export async function GET() {
  try {
    const books = await prisma.bibleBook.findMany({
      orderBy: { id: 'asc' },
    });
    return NextResponse.json({
      data: books,
      meta: { total: books?.length ?? 0 },
    });
  } catch (error: any) {
    console.error('Failed to fetch books:', error);
    return NextResponse.json(
      { error: 'Failed to fetch books' },
      { status: 500 }
    );
  }
}

State Management Design

State Architecture (Zustand recommended):

typescript
interface VisualizationStore {
  // View state
  viewMode: 'arcs' | 'threads';
  setViewMode: (mode: 'arcs' | 'threads') => void;
  
  // Filter state
  filters: {
    genres: string[];
    testament: 'all' | 'OT' | 'NT' | 'inter';
    minVotes: number;
    searchQuery: string;
  };
  setFilter: (key: string, value: any) => void;
  
  // Zoom state (managed by D3, mirrored here for UI)
  zoomTransform: { k: number; x: number; y: number };
  setZoomTransform: (t: { k: number; x: number; y: number }) => void;
  
  // Selection
  selectedArc: CrossReference | null;
  hoveredBook: number | null;
  
  // Data
  books: BibleBook[];
  crossReferences: CrossReference[];
  isLoading: boolean;
}

Why Zustand over Redux/Context:

  • •Minimal boilerplate (no providers needed)
  • •Direct D3 ↔ React state bridging
  • •Selector-based re-rendering (only components using changed state re-render)
  • •Simple async actions for API calls
6

Phase 6: Component Catalog

3 subsections

ArcCanvas Component

The primary visualization component — renders the SVG arc diagram.

components/arc-canvas.tsx
'use client';
import { useRef, useEffect, useState, useCallback } from 'react';
import * as d3 from 'd3';

interface ArcCanvasProps {
  data: {
    fromAbsolute: number;
    toAbsolute: number;
    bookIndex: number;
    votes: number;
  }[];
  books: {
    id: number;
    name: string;
    abbreviation: string;
    startVerse: number;
    verses: number;
    color: string;
  }[];
  width: number;
  height: number;
  filters?: {
    genres?: string[];
    testament?: string;
    minVotes?: number;
  };
  onArcHover?: (arc: any | null) => void;
  onArcClick?: (arc: any) => void;
}

const TOTAL_VERSES = 31102;

export function ArcCanvas({
  data,
  books,
  width,
  height,
  filters,
  onArcHover,
  onArcClick,
}: ArcCanvasProps) {
  const svgRef = useRef<SVGSVGElement>(null);
  const baselineY = height * 0.82;
  const heightScale = height * 0.5;

  useEffect(() => {
    if (!svgRef?.current || !data?.length) return;
    const svg = d3.select(svgRef.current);
    
    // Clear previous
    svg.selectAll('.arc-group').remove();
    
    const g = svg.append('g').attr('class', 'arc-group');
    
    // Zoom behavior
    const zoom = d3.zoom<SVGSVGElement, unknown>()
      .scaleExtent([1, 50])
      .on('zoom', (event: any) => {
        g.attr('transform', event?.transform);
      });
    
    svg.call(zoom);
    
    // Render arcs
    const arcs = g.selectAll('.arc')
      .data(data ?? [])
      .enter()
      .append('path')
      .attr('class', 'arc')
      .attr('d', (d: any) => {
        const x1 = ((d?.fromAbsolute ?? 0) / TOTAL_VERSES) * width;
        const x2 = ((d?.toAbsolute ?? 0) / TOTAL_VERSES) * width;
        const mx = (x1 + x2) / 2;
        const dist = Math.abs(x2 - x1);
        const h = Math.sqrt(dist) * (heightScale / Math.sqrt(width));
        return `M ${x1},${baselineY} Q ${mx},${baselineY - h} ${x2},${baselineY}`;
      })
      .attr('fill', 'none')
      .attr('stroke', (d: any) => {
        const hue = ((d?.bookIndex ?? 0) / 66) * 360;
        return `hsl(${hue}, 80%, 55%)`;
      })
      .attr('stroke-width', 1)
      .attr('stroke-opacity', 0.15)
      .on('mouseenter', function(event: any, d: any) {
        d3.select(this)
          .attr('stroke-opacity', 1)
          .attr('stroke-width', 2);
        onArcHover?.(d);
      })
      .on('mouseleave', function() {
        d3.select(this)
          .attr('stroke-opacity', 0.15)
          .attr('stroke-width', 1);
        onArcHover?.(null);
      })
      .on('click', (event: any, d: any) => {
        onArcClick?.(d);
      });
    
    // Book labels on baseline
    g.selectAll('.book-label')
      .data(books ?? [])
      .enter()
      .append('text')
      .attr('class', 'book-label')
      .attr('x', (d: any) => {
        const start = ((d?.startVerse ?? 0) / TOTAL_VERSES) * width;
        const bookWidth = ((d?.verses ?? 0) / TOTAL_VERSES) * width;
        return start + bookWidth / 2;
      })
      .attr('y', baselineY + 16)
      .attr('text-anchor', 'middle')
      .attr('fill', '#9a9890')
      .attr('font-size', '8px')
      .text((d: any) => d?.abbreviation ?? '');
    
    // Baseline
    g.append('line')
      .attr('x1', 0)
      .attr('y1', baselineY)
      .attr('x2', width)
      .attr('y2', baselineY)
      .attr('stroke', '#2c2b28')
      .attr('stroke-width', 1);
    
    return () => { svg.selectAll('.arc-group').remove(); };
  }, [data, books, width, height, baselineY, heightScale, onArcHover, onArcClick]);

  return (
    <svg
      ref={svgRef}
      width={width}
      height={height}
      style={{ background: '#0d0d0d' }}
    />
  );
}

Header & Navigation Components

Header Component — matches original styling:

PropTypePurpose
viewMode'arcs''threads'Current view
onViewChange(mode) => voidToggle handler
onSearch(query) => voidSearch handler
onFilterOpen() => voidOpens filter panel

Segmented Control — the Arcs/Threads toggle:

  • •Container: #161615 bg, 8px radius, 1px #2c2b28 border
  • •Active segment: #2c2b28 bg, 6px radius
  • •Inactive segment: transparent
  • •Transition: 150ms ease-out on background-color
  • •Text: 15px system-ui, #ecebe6
components/segmented-control.tsx
'use client';
import { useState } from 'react';

interface SegmentedControlProps {
  options: { value: string; label: string }[];
  value: string;
  onChange: (value: string) => void;
}

export function SegmentedControl({ options, value, onChange }: SegmentedControlProps) {
  return (
    <div className="flex rounded-lg border border-[#2c2b28] bg-[#161615] p-[2px]">
      {(options ?? []).map((opt: any) => (
        <button
          key={opt?.value ?? ''}
          onClick={() => onChange?.(opt?.value)}
          className={`px-4 py-1 text-sm transition-all duration-150 rounded-md ${
            value === opt?.value
              ? 'bg-[#2c2b28] text-[#ecebe6]'
              : 'text-[#9a9890] hover:text-[#ecebe6]'
          }`}
        >
          {opt?.label ?? ''}
        </button>
      ))}
    </div>
  );
}

Filter & Search Components

FilterPanel Component:

typescript
interface FilterPanelProps {
  filters: {
    genres: string[];
    testament: 'all' | 'OT' | 'NT' | 'inter';
    minVotes: number;
  };
  onChange: (filters: FilterPanelProps['filters']) => void;
  isOpen: boolean;
  onClose: () => void;
}

Genre Checkboxes: Law, History, Poetry, Major Prophets, Minor Prophets, Gospels, Acts, Epistles, Apocalyptic

Testament Radio: All, Old Testament, New Testament, Inter-testament

Vote Threshold Slider: 1-50 (controls minimum votes for displayed arcs)

Search Component:

  • •Debounced input (300ms)
  • •Searches book names, chapter:verse references
  • •Results highlight matching arcs on the visualization
  • •Keyboard shortcut: / to focus
7

Phase 7: Build Plan (7-Sprint Roadmap)

3 subsections

Sprint Overview

7-Sprint Development Roadmap (2-week sprints):

SprintFocusKey DeliverablesStory Points
1FoundationProject setup, DB schema, data pipeline21
2Data LayerAPI routes, seed script, data validation18
3Core VisualizationSVG canvas, arc rendering, baseline34
4InteractionsZoom/pan, hover, click, tooltips26
5Filtering & SearchGenre/testament filters, search, URL state21
6Threads ViewChapter reader, connection chart, book bars26
7Polish & DeployPerformance optimization, PWA, accessibility18
Total164 SP

Team Composition (recommended):

  • •1 Lead Developer (full-stack)
  • •1 D3/Visualization Specialist
  • •1 Designer (part-time, Sprints 1, 6-7)

Critical Path: Sprint 1 → 2 → 3 → 4 (core visualization must be complete before filtering/threads)

Sprint Task Breakdown

Sprint 1: Foundation (Week 1-2)

  • •Initialize Next.js project with TypeScript
  • •Configure Tailwind CSS with brand tokens
  • •Set up PostgreSQL + Prisma schema
  • •Write data pipeline: fetch OpenBible.info TSV
  • •Parse and validate against KJV versification
  • •Seed database with 66 books + verse metadata
  • •Basic layout: header, main content area

Sprint 2: Data Layer (Week 3-4)

  • •Build REST API routes (books, cross-references, stats)
  • •Implement pagination and filtering on API
  • •Load ~344K cross-references into database
  • •Build optimized data fetching for visualization
  • •Add response caching headers
  • •Write API integration tests

Sprint 3: Core Visualization (Week 5-6)

  • •Implement SVG canvas with D3.js
  • •Arc path generation with quadratic Bézier curves
  • •Book baseline with labels
  • •Color mapping by genre/book index
  • •Virtual viewport rendering (cull off-screen arcs)
  • •Stats bar (verse count, link count, connectivity %)

Sprint 4: Interactions (Week 7-8)

  • •D3 zoom/pan behavior
  • •Arc hover effects (opacity, stroke-width)
  • •Arc click → tooltip with reference details
  • •Book label click → zoom to book
  • •Smooth animated transitions
  • •Keyboard navigation support

Sprint 5: Filtering & Search (Week 9-10)

  • •Genre filter checkboxes
  • •Testament filter radio buttons
  • •Vote threshold slider
  • •Search input with debounce
  • •URL query parameter sync
  • •Filter animation (fade non-matching arcs)

Sprint 6: Threads View (Week 11-12)

  • •Chapter selector component
  • •Verse list with highlighted cross-references
  • •Book connection bar chart
  • •Click-to-navigate between connected passages
  • •Smooth view transitions (Arcs ↔ Threads)

Sprint 7: Polish & Deploy (Week 13-14)

  • •Performance audit (Lighthouse, bundle analysis)
  • •Service worker for offline support
  • •Accessibility audit (WCAG 2.1 AA)
  • •Responsive design tweaks
  • •Deploy to production
  • •Documentation and README

Dependency Graph

Task Dependencies:

typescript
DB Schema ──→ Seed Script ──→ API Routes ──→ Data Fetching
                                    │
                                    ▼
SVG Canvas ──→ Arc Rendering ──→ Virtual Rendering ──→ Zoom/Pan
     │              │                                     │
     │              ▼                                     ▼
     │         Color System ──────────────────→ Filter Animation
     │                                              │
     ▼                                              ▼
Book Baseline ──→ Book Labels ──→ Book Click ──→ Search
                                                    │
                                                    ▼
                                              Threads View

External Dependencies:

PackageVersionPurpose
d3^7.0Visualization core
@types/d3^7.0TypeScript types
zustand^5.0State management
prisma^6.0Database ORM
next^16.0Application framework
tailwindcss^3.4Styling
framer-motion^12.0UI animations
8

Phase 8: Code Generation

3 subsections

Data Loading Pipeline

Complete TypeScript script for loading the Treasury of Scripture Knowledge cross-reference data into PostgreSQL.

scripts/load-cross-references.ts
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// Bible books with verse counts and cumulative offsets
const BOOKS = [
  { id: 1, name: 'Genesis', abbr: 'Gen', testament: 'OT', genre: 'Law', chapters: 50, verses: 1533 },
  { id: 2, name: 'Exodus', abbr: 'Exod', testament: 'OT', genre: 'Law', chapters: 40, verses: 1213 },
  { id: 3, name: 'Leviticus', abbr: 'Lev', testament: 'OT', genre: 'Law', chapters: 27, verses: 859 },
  { id: 4, name: 'Numbers', abbr: 'Num', testament: 'OT', genre: 'Law', chapters: 36, verses: 1288 },
  { id: 5, name: 'Deuteronomy', abbr: 'Deut', testament: 'OT', genre: 'Law', chapters: 34, verses: 959 },
  // ... remaining 61 books follow same pattern
  { id: 66, name: 'Revelation', abbr: 'Rev', testament: 'NT', genre: 'Apocalyptic', chapters: 22, verses: 404 },
];

// Compute cumulative start positions
let offset = 1;
const booksWithOffset = BOOKS.map(b => {
  const book = { ...b, startVerse: offset };
  offset += b.verses;
  return book;
});

async function loadBooks() {
  for (const book of booksWithOffset) {
    const hue = Math.round((book.id / 66) * 360);
    await prisma.bibleBook.upsert({
      where: { id: book.id },
      update: {},
      create: {
        id: book.id,
        name: book.name,
        abbreviation: book.abbr,
        testament: book.testament,
        genre: book.genre,
        chapters: book.chapters,
        verses: book.verses,
        startVerse: book.startVerse,
        color: `hsl(${hue}, 80%, 55%)`,
      },
    });
  }
  console.log(`Loaded ${booksWithOffset.length} books`);
}

async function parseTSV(url: string): Promise<any[]> {
  const response = await fetch(url);
  const text = await response.text();
  return text.split('\n')
    .filter((line: string) => line?.trim() && !line?.startsWith('From'))
    .map((line: string) => {
      const [from, to, votes] = (line ?? '').split('\t');
      return { from, to, votes: parseInt(votes ?? '1', 10) || 1 };
    });
}

async function main() {
  await loadBooks();
  console.log('Books loaded. Ready for cross-reference import.');
  await prisma.$disconnect();
}

main().catch(console.error);

D3.js Visualization Engine

Complete D3.js arc visualization implementation with zoom, pan, and filtering.

lib/visualization-engine.ts
import * as d3 from 'd3';

export interface VisConfig {
  container: SVGSVGElement;
  width: number;
  height: number;
  baselineRatio?: number;  // default 0.82
  heightScale?: number;    // default 0.5
  defaultOpacity?: number; // default 0.15
}

export interface ArcDatum {
  fromAbsolute: number;
  toAbsolute: number;
  bookIndex: number;
  votes: number;
}

export interface BookDatum {
  id: number;
  name: string;
  abbreviation: string;
  startVerse: number;
  verses: number;
  color: string;
}

const TOTAL_VERSES = 31102;

export class BibleArcEngine {
  private svg: d3.Selection<SVGSVGElement, unknown, null, undefined>;
  private arcGroup: d3.Selection<SVGGElement, unknown, null, undefined>;
  private labelGroup: d3.Selection<SVGGElement, unknown, null, undefined>;
  private config: Required<VisConfig>;
  private zoom: d3.ZoomBehavior<SVGSVGElement, unknown>;
  
  constructor(config: VisConfig) {
    this.config = {
      ...config,
      baselineRatio: config?.baselineRatio ?? 0.82,
      heightScale: config?.heightScale ?? 0.5,
      defaultOpacity: config?.defaultOpacity ?? 0.15,
    };
    
    this.svg = d3.select(this.config.container);
    this.arcGroup = this.svg.append('g').attr('class', 'arcs');
    this.labelGroup = this.svg.append('g').attr('class', 'labels');
    
    this.zoom = d3.zoom<SVGSVGElement, unknown>()
      .scaleExtent([0.5, 100])
      .on('zoom', (event: any) => {
        this.arcGroup.attr('transform', event?.transform);
        this.labelGroup.attr('transform', event?.transform);
      });
    
    this.svg.call(this.zoom);
  }
  
  renderArcs(data: ArcDatum[]): void {
    const { width, baselineRatio, heightScale, defaultOpacity } = this.config;
    const by = this.config.height * baselineRatio;
    const hs = this.config.height * heightScale / Math.sqrt(width);
    
    this.arcGroup.selectAll('.arc')
      .data(data ?? [])
      .join('path')
      .attr('class', 'arc')
      .attr('d', (d: ArcDatum) => {
        const x1 = ((d?.fromAbsolute ?? 0) / TOTAL_VERSES) * width;
        const x2 = ((d?.toAbsolute ?? 0) / TOTAL_VERSES) * width;
        const mx = (x1 + x2) / 2;
        const dist = Math.abs(x2 - x1);
        const h = Math.sqrt(dist) * hs;
        return `M ${x1},${by} Q ${mx},${by - h} ${x2},${by}`;
      })
      .attr('fill', 'none')
      .attr('stroke', (d: ArcDatum) =>
        `hsl(${((d?.bookIndex ?? 0) / 66) * 360}, 80%, 55%)`
      )
      .attr('stroke-width', 1)
      .attr('stroke-opacity', defaultOpacity);
  }
  
  renderBaseline(books: BookDatum[]): void {
    const { width } = this.config;
    const by = this.config.height * this.config.baselineRatio;
    
    // Baseline
    this.labelGroup.append('line')
      .attr('x1', 0).attr('y1', by)
      .attr('x2', width).attr('y2', by)
      .attr('stroke', '#2c2b28').attr('stroke-width', 1);
    
    // Book sections + labels
    this.labelGroup.selectAll('.book-section')
      .data(books ?? [])
      .join('g')
      .attr('class', 'book-section')
      .each(function(d: any) {
        const g = d3.select(this);
        const x = ((d?.startVerse ?? 0) / TOTAL_VERSES) * width;
        const w = ((d?.verses ?? 0) / TOTAL_VERSES) * width;
        
        g.append('rect')
          .attr('x', x).attr('y', by)
          .attr('width', w).attr('height', 4)
          .attr('fill', d?.color ?? '#333');
        
        if (w > 15) {
          g.append('text')
            .attr('x', x + w / 2).attr('y', by + 16)
            .attr('text-anchor', 'middle')
            .attr('fill', '#9a9890')
            .attr('font-size', '7px')
            .text(d?.abbreviation ?? '');
        }
      });
  }
  
  destroy(): void {
    this.svg.selectAll('*').remove();
  }
}

Docker & Deployment Configuration

Complete Docker setup for the reconstructed application.

Dockerfile
# Multi-stage build for production
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN yarn build

FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]
docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/bible_xref
      - NODE_ENV=production
    depends_on:
      - db
  
  db:
    image: postgres:16-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=bible_xref
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  pgdata:
9

Phase 9: Validation Plan

3 subsections

Visual Fidelity Metrics

Comparison Metrics (rebuilt vs. original):

MetricMethodTarget
Arc count accuracyCount rendered arcs at default zoom±0.1% of 153,419
Color distributionSample 1000 arcs, compare hue valuesΔE < 5 (perceptual)
Arc height distributionMeasure 100 arcs, compare heightsR² > 0.95
Zoom rangeTest min/max zoom levelsMatch 1× to 50×
Pan smoothnessFrame rate during pan at 10× zoom≥ 55 fps
Filter accuracyApply genre filter, count visible arcsExact match
Book label positionsScreenshot comparison at 1× zoom< 2px offset
Baseline alignmentOverlay screenshotsPixel-perfect

Automated Testing Suite:

typescript
describe('Arc Visualization', () => {
  it('should render correct number of arcs', () => {
    expect(svg.querySelectorAll('.arc').length).toBeGreaterThan(0);
  });
  
  it('should color arcs by book index', () => {
    const arc = svg.querySelector('.arc');
    const stroke = arc.getAttribute('stroke');
    expect(stroke).toMatch(/^hsl\(/);
  });
  
  it('should maintain 60fps during pan', async () => {
    const frames = await measureFrameRate(panInteraction);
    expect(frames.average).toBeGreaterThan(55);
  });
});

Data Integrity Checks

Data Validation Rules:

1. Book Count: Exactly 66 books loaded

2. Verse Count: Exactly 31,102 total verses across all books

3. Cross-Reference Count: ~344,799 references (or subset matching source)

4. Reference Validity: All fromBook and toBook values in range [1, 66]

5. Absolute Position: All values in range [1, 31102]

6. No Self-References: fromAbsolute ≠ toAbsolute

7. No Duplicates: Unique (fromAbsolute, toAbsolute) pairs

8. Genre Coverage: All 7+ genres represented in book metadata

9. Testament Distribution: ~23,145 OT verses + ~7,957 NT verses = 31,102

SQL Validation Queries:

sql
-- Verify total verses
SELECT SUM(verses) AS total FROM bible_books;
-- Expected: 31102

-- Verify no orphan references
SELECT COUNT(*) FROM cross_references cr
WHERE NOT EXISTS (SELECT 1 FROM bible_books b WHERE b.id = cr.from_book);
-- Expected: 0

-- Verify absolute position range
SELECT MIN(from_absolute), MAX(from_absolute),
       MIN(to_absolute), MAX(to_absolute)
FROM cross_references;
-- Expected: 1, 31102, 1, 31102

-- Cross-reference density per book
SELECT b.name, COUNT(*) as refs
FROM cross_references cr
JOIN bible_books b ON b.id = cr.from_book
GROUP BY b.name ORDER BY refs DESC;

Performance Benchmarks

Lighthouse Targets:

CategoryTarget Score
Performance≥ 85
Accessibility≥ 90
Best Practices≥ 95
SEO≥ 90

Core Web Vitals Targets:

MetricTargetMeasurement
LCP (Largest Contentful Paint)< 2.5sFirst meaningful paint of visualization
FID (First Input Delay)< 100msTime to respond to first zoom/pan
CLS (Cumulative Layout Shift)< 0.1No layout shifts after initial render
TTFB (Time to First Byte)< 600msServer response time

Bundle Size Targets:

BundleMax Size (gzipped)
Main JS< 200KB
D3.js (tree-shaken)< 80KB
CSS< 15KB
Cross-reference data< 2MB
Total initial load< 500KB

Memory Targets:

  • •Idle: < 50MB
  • •Full data loaded: < 100MB
  • •During zoom/pan: < 120MB peak

Technical reconstruction analysis based on the publicly accessible Bible Cross-References by Oleksii Trekhleb. Cross-reference data from the Treasury of Scripture Knowledge via OpenBible.info (CC BY 4.0).