Skip to content

Instantly share code, notes, and snippets.

@jsundram
Last active January 13, 2026 08:45
Show Gist options
  • Select an option

  • Save jsundram/a187ff2619b90eaf68d449462b1f9795 to your computer and use it in GitHub Desktop.

Select an option

Save jsundram/a187ff2619b90eaf68d449462b1f9795 to your computer and use it in GitHub Desktop.
Periodic Table of Boccherini String Quartets
# macOS system files
.DS_Store
# Local workflow files
.gist_id
sync-gist.sh
# Visual regression testing output (baselines/ is tracked)
diffs/

Periodic Table of Boccherini String Quartets

An interactive visualization of Luigi Boccherini's complete string quartet output (1761-1804), displayed as a periodic table-inspired grid.

View it (via gisthack, see below for details):

Updating the Gist

Quick Start (First Time Setup)

# 1. Configure git to use GitHub CLI credentials (do this once)
gh auth setup-git

# 2. Save your gist ID (do this once)
gh gist list | head -1 | awk '{print $1}' > .gist_id

# 3. Create a sync script (do this once)
cat > sync-gist.sh << 'EOF'
#!/bin/bash

# Usage: ./sync-gist.sh "commit message" file1 file2 ...
# Or: ./sync-gist.sh file1 file2 ... (uses default message)

# If first arg looks like a commit message (has spaces or quotes), use it
if [[ "$1" == *" "* ]] || [[ "$1" == \"*\" ]]; then
    MESSAGE="$1"
    shift
    FILES="$@"
else
    MESSAGE="Update gist"
    FILES="${@:-index.html}"
fi

# Add, commit, and push with message
git add $FILES
git commit -m "$MESSAGE"
git push
EOF
chmod +x sync-gist.sh

Daily Workflow

# 1. Make your changes to index.html, opera.json, or README.md

# 2. Check what changed
git diff                    # See all changes
git diff index.html         # See specific file changes

# 3. Update the gist with a commit message
./sync-gist.sh "Fix title alignment" index.html

# Or update multiple files
./sync-gist.sh "Update visualization and docs" index.html README.md

# Or use default message "Update gist"
./sync-gist.sh index.html

How it works

The script uses standard git commands to update the gist:

  1. git add - stages your files
  2. git commit -m "message" - commits with your message
  3. git push - pushes to the gist

Your commit messages appear in the gist history instead of just "revised".

Manual method (if you prefer)

# Standard git workflow
git add index.html
git commit -m "Your commit message"
git push

# Check status
git status
git log --oneline

Viewing Online

Get the raw file URL and use githack.com to serve it:

  1. Get the raw URL from your gist:

    https://gist.githubusercontent.com/USERNAME/GIST_ID/raw/index.html
    
  2. Replace gist.githubusercontent.com with gist.githack.com:

    https://gist.githack.com/USERNAME/GIST_ID/raw/index.html
    

Example for this gist:

https://gist.githack.com/jsundram/a187ff2619b90eaf68d449462b1f9795/raw/index.html

Why githack.com?

  • Properly serves files with correct MIME types
  • Loads external scripts (like D3.js) without CORS issues
  • No rate limiting for development use
  • Updates automatically when you update your gist

Note: htmlpreview.github.io doesn't work because it can't load external scripts like D3.js.

Data Overview (opera.json)

Structure

The data is organized as an array of opus groups, where each opus contains:

{
  "opus": 2,                    // Opus number
  "year": 1761,                 // Year of composition
  "dedication": "...",          // Optional: dedicatee
  "imslp": "...",              // Optional: IMSLP link for the opus
  "quartets": [...]            // Array of quartets in this opus
}

Each quartet contains:

{
  "number": 1,                 // Quartet number within opus
  "gerard": 159,               // Gerard catalog number
  "key": "C",                  // Key (e.g., "C", "E-flat")
  "major": false,              // true = major, false = minor
  "nickname": "...",           // Optional: nickname
  "imslp": "...",             // Optional: IMSLP link
  "mvmts": [...],             // Array of movement names
  "category": "opera grande"   // "opera grande" or "opera piccola" variants
}

Data Usage

Displayed in visualization:

  • Opus number (row header)
  • Year and Boccherini's age (row header, top)
  • Category: Opera Grande/Piccola (row header, bottom)
  • Dedication (row header, when present)
  • Gerard catalog number (card header)
  • Quartet number within opus (card header)
  • Key signature (card center, with ♭ symbol)
  • Major/minor mode (card center)
  • Movement count with color coding (card bottom)
  • Nickname (card center, when present)

Used in interactions:

  • Individual movement names (shown in hover tooltip)
  • IMSLP links (quartet and opus level, opened on click)

Notable transformations:

  • "-flat" in key names is replaced with the Unicode flat symbol (♭)
  • Boccherini's age calculated from birth year (1743)
  • Category determines row background gradient color

Unused/Metadata

All data fields are currently utilized either in the display, tooltips, or interactions. The data is comprehensive and fully integrated.

Technical Overview (index.html)

Architecture

Single-file design: All HTML, CSS, and JavaScript in one file for easy gist hosting and sharing.

Technology stack:

  • D3.js v7 for data loading and DOM manipulation
  • Pure CSS for styling (no CSS frameworks)
  • Vanilla JavaScript (no additional frameworks)

Design Decisions

1. Periodic Table Metaphor

  • Square cards (140×140px) arranged in rows by opus
  • Each opus group forms a "period" (row)
  • Inspired by chemical periodic table organization

2. Visual Alignment Strategy The design emphasizes vertical alignment across three levels:

Row Header          ↔  Quartet Cards
─────────────────────────────────────
Year (age)          ↔  Mode bar (G# / quartet #)
Opus number         ↔  Key signature
Category badge      ↔  Movement count

This creates strong visual relationships between semantically related information.

3. Color Encoding System

Mode bar (top of each card):

  • Blue (#2196F3): Major keys
  • Pink (#E91E63): Minor keys

Movement count (diverging purple-green palette):

  • 1 movement: Deep purple (#9C27B0) - rare/incomplete
  • 2 movements: Light purple (#CE93D8) - opera piccola
  • 3 movements: Blue-gray (#B0BEC5) - standard
  • 4 movements: Light green (#81C784) - substantial
  • 5 movements: Deep green (#2E7D32) - rare/complete

Category badges use the same palette:

  • Opera Piccola: Light purple (matches 2-movement works)
  • Opera Grande: Light green (matches 4-movement works)

Row background gradients:

  • Subtle gradient (12% opacity) extends from row header toward cards
  • Uses category color to create visual flow across the row

4. Layout System

Flexbox throughout:

  • Rows: display: flex with flex-wrap for cards
  • Opus labels: justify-content: space-between for vertical distribution
  • Cards: flex-direction: column for stacking elements

Height constraints:

  • Opus label and cards both fixed at 140px for alignment
  • flex-grow: 1 on middle sections (opus number, key signature) for vertical centering
  • Movement count uses margin-top: auto to anchor to bottom

5. Typography Hierarchy

Dramatic size contrast in row headers:

  • Opus number: 2.8em, weight 900 (ultra bold)
  • Year: 0.8em, weight 600
  • Age: 0.65em (parenthetical)
  • Category badge: 0.65em
  • Dedication: 0.6em, italic

Code Intricacies

1. Commented sections for easy tweaking The opus label CSS and JavaScript are heavily commented with clear section markers (e.g., === OPUS LABEL SECTION ===) to facilitate experimentation with layout and sizing.

2. Dynamic class application Category background gradients are applied dynamically:

const categoryClass = opus.quartets[0].category.includes('grande')
  ? 'grande-bg' : 'piccola-bg';

3. Unicode transformation Flat symbols are rendered using Unicode replacement:

const keyDisplay = quartet.key.replace('-flat', '♭');

4. Nested container pattern Cards use multiple nested containers for precise alignment:

  • quartet-cardmode-bar + card-content
  • card-contentkey-section + nickname + movements-count

This allows independent control of each vertical section.

5. Age calculation Boccherini's age is calculated inline from a constant:

const BOCCHERINI_BIRTH_YEAR = 1743;
const age = opus.year - BOCCHERINI_BIRTH_YEAR;

Browser Compatibility

Requires modern browser support for:

  • CSS Flexbox
  • Unicode symbols (♭)
  • ES6 JavaScript (const, arrow functions, template literals)
  • D3.js v7

Tested in Chrome, Firefox, Safari, and Edge (2023+).


Created: December 2025 Data source: Luigi Boccherini quartet catalog (G.159-249)

#!/usr/bin/env -S uv run
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "playwright>=1.40.0",
# ]
# ///
"""
Generate PDF of Boccherini String Quartets visualization
Usage:
uv run generate-pdf.py
First-time setup (install Playwright browsers):
uvx playwright install chromium
Requirements:
- uv installed (https://docs.astral.sh/uv/)
- Local server running at http://localhost:8000
- qpdf installed (for linearization/optimization)
"""
import sys
import subprocess
import os
from playwright.sync_api import sync_playwright
def generate_pdf():
"""Generate the PDF"""
print("📄 Generating Boccherini Quartets PDF...")
print(" Loading http://localhost:8000/index.html")
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
# Set viewport size for proper layout (desktop view)
# Below 1200px width, responsive layout causes wrapping
page.set_viewport_size({'width': 1400, 'height': 2000})
# Navigate to the page
page.goto('http://localhost:8000/index.html', wait_until='networkidle')
margin = {
'top': '0in',
'bottom': '0in',
'left': '0in',
'right': '0in'
}
outfile = 'boccherini-quartets-temp.pdf'
# Generate PDF with exact settings
page.pdf(
path=outfile,
format='Letter',
print_background=True, # Enable background graphics
tagged=False, # Generate tagged PDF for accessibility?
# margin=margin, # Ignored since we are using css below.
prefer_css_page_size=True # Use CSS @page size settings
)
browser.close()
print(f"✓ Temporary PDF generated: {outfile}")
# Linearize the PDF using qpdf to avoid AirPrint issues.
final_pdf = 'boccherini-quartets.pdf'
print(f"🔄 Linearizing PDF with qpdf...")
try:
subprocess.run(
['qpdf', '--linearize', outfile, final_pdf],
check=True,
capture_output=True,
text=True
)
print(f"✓ Linearized PDF saved to: {final_pdf}")
# Clean up temporary file
os.remove(outfile)
print(f"✓ Cleaned up temporary file: {outfile}")
except FileNotFoundError:
print("\n⚠️ qpdf not found.")
print("\nPlease install qpdf:")
print(" brew install qpdf (macOS)")
print(f"\nTemporary PDF available at: {outfile}")
print(f"Run manually: qpdf --linearize {outfile} {final_pdf}")
sys.exit(1)
except subprocess.CalledProcessError as e:
print(f"\n⚠️ qpdf failed: {e.stderr}")
print(f"Temporary PDF available at: {outfile}")
sys.exit(1)
if __name__ == "__main__":
try:
generate_pdf()
except Exception as e:
if "Executable doesn't exist" in str(e) or "browserType.launch" in str(e):
print("\n⚠️ Playwright browsers not installed.")
print("\nPlease run this command first:")
print(" uvx playwright install chromium")
sys.exit(1)
else:
raise
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Periodic Table of Boccherini String Quartets</title>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<style>
:root {
/* === DYNAMIC SIZING SYSTEM === */
/* Cards smoothly scale from 140px (desktop) to 80px (mobile) */
--card-height: clamp(80px, 12vw, 140px);
--top-section-height: clamp(14px, 1.7vw, 20px);
--nickname-height: clamp(10px, 1.3vw, 15px);
--bottom-section-height: clamp(14px, 1.7vw, 20px);
/* Calculated middle section height - same for both sides */
--middle-section-height: calc(
var(--card-height)
- var(--top-section-height)
- var(--nickname-height)
- var(--bottom-section-height)
);
/* Font sizes for aligned elements */
--top-font-size: 0.8em;
--middle-font-size: 2.8em;
--bottom-font-size: 0.7em;
/* Header widths */
--header-width: 900px;
/* === DEBUG MODE === */
--debug-mode: 0; /* Set to 1 to show bounding boxes, 0 to hide */
/* === OWNERSHIP INDICATOR === */
--show-ownership: 1; /* Set to 1 to show repeat signs on owned parts, 0 to hide */
}
/* Debug bounding boxes - controlled by --debug-mode */
.opus-label .year-age {
outline: calc(var(--debug-mode) * 1px) solid rgba(255, 0, 0, 0.3);
}
.opus-label .opus-number {
outline: calc(var(--debug-mode) * 1px) solid rgba(0, 255, 0, 0.3);
}
.opus-label .bottom-section {
outline: calc(var(--debug-mode) * 1px) solid rgba(0, 0, 255, 0.3);
}
.mode-bar {
outline: calc(var(--debug-mode) * 1px) solid rgba(255, 0, 0, 0.3);
}
.key-section {
outline: calc(var(--debug-mode) * 1px) solid rgba(0, 255, 0, 0.3);
}
.movements-count {
outline: calc(var(--debug-mode) * 1px) solid rgba(0, 0, 255, 0.3);
}
.nickname {
outline: calc(var(--debug-mode) * 1px) solid rgba(255, 165, 0, 0.3);
}
.opus-label .dedication {
outline: calc(var(--debug-mode) * 1px) solid rgba(128, 0, 128, 0.3);
}
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
background-color: #f5f5f5;
margin: 20px;
padding: 0;
}
.header-group {
/* Header centered over visualization at all widths */
display: flex;
flex-direction: column;
align-items: stretch; /* Children stretch to container width */
width: fit-content; /* Shrink to fit widest child (title) */
max-width: 100%; /* Don't overflow viewport */
margin: 0 auto; /* Center the group */
}
h1 {
margin: 0 0 5px 0; /* Small gap below */
text-align: center;
color: #333;
font-size: clamp(1.3em, 1.8vw, 2em); /* Scales smoothly */
}
.subtitle {
margin: 0 0 25px 0; /* Larger gap below */
text-align: right; /* Right-align to match title's right edge */
color: #666;
font-size: clamp(0.75em, 0.85vw, 0.9em); /* Scales smoothly */
font-style: italic;
}
.subtitle a {
color: #2196F3;
text-decoration: none;
}
.subtitle a:hover {
text-decoration: underline;
}
.container {
max-width: 1400px;
margin: 0 auto;
display: flex;
flex-direction: column;
align-items: center;
}
.opus-row {
display: flex;
align-items: flex-start;
margin-bottom: 10px;
}
/* Combined rows - no special CSS needed, just flex layout */
.opus-row.combined-pair {
/* Flexbox will lay out multiple label+card groups horizontally */
}
/* === OPUS LABEL SECTION (easy to tweak) === */
.opus-label {
/* Container sizing - MATCHES CARD HEIGHT, scales proportionally */
width: clamp(50px, 8vw, 95px);
height: var(--card-height);
padding: clamp(1px, 0.2vw, 2px) clamp(3px, 0.5vw, 6px) 0 clamp(5px, 0.8vw, 10px);
/* Layout - vertical alignment structure */
display: flex;
flex-direction: column;
justify-content: flex-start; /* Stack from top, no auto-spacing */
text-align: right;
position: relative;
}
/* Background gradient based on category - applied via JS */
.opus-label.grande-bg {
background: linear-gradient(to right,
transparent 0%,
rgba(129, 199, 132, 0.12) 100%); /* Light green fade */
}
.opus-label.piccola-bg {
background: linear-gradient(to right,
transparent 0%,
rgba(206, 147, 216, 0.12) 100%); /* Light purple fade */
}
/* TOP SECTION: Year and Age - ALIGNS WITH MODE BAR */
.opus-label .year-age {
display: flex;
justify-content: space-between; /* Year left, age right - periodic table aesthetic */
align-items: center;
height: var(--top-section-height);
flex-shrink: 0;
line-height: 1; /* Match mode-bar line-height */
}
.opus-label .year {
font-size: var(--top-font-size);
font-weight: 700; /* Slightly bolder for clarity */
color: #444; /* Slightly darker */
line-height: 1; /* Exact alignment */
}
.opus-label .age {
font-size: calc(var(--top-font-size) * 0.85); /* Slightly larger */
font-weight: 400; /* Regular weight */
color: #777; /* Slightly darker */
line-height: 1; /* Exact alignment */
}
/* MIDDLE SECTION: Opus number - ALIGNS WITH KEY/MODE */
.opus-label .opus-number {
font-size: var(--middle-font-size);
font-weight: 900;
color: #222;
line-height: 1;
letter-spacing: -0.03em;
height: var(--middle-section-height); /* Fixed height to match key-section */
margin-bottom: var(--nickname-height); /* Spacer to match nickname section */
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: flex-end;
}
/* BOTTOM SECTION: Dedication and category badge */
.opus-label .bottom-section {
display: flex;
flex-direction: column;
align-items: flex-end; /* Right align */
justify-content: center; /* Vertically center - matches movements-count */
flex-shrink: 0;
height: var(--bottom-section-height); /* Fixed height to match movements-count */
position: relative; /* For dedication positioning */
}
/* Dedication - absolutely positioned above opus number to preserve alignment */
.opus-label .dedication {
position: absolute; /* Remove from layout flow */
top: 22px; /* Position below year/age */
left: 0;
right: 0;
font-size: 0.6em; /* Size: smallest */
font-style: italic; /* Style: italic */
color: #999; /* Color: lighter gray */
text-align: center; /* Center align */
line-height: 1.2; /* Tighter line height for wrapping */
z-index: 1; /* Above background */
}
/* Category badge - aligns with movement count */
.opus-label .category-badge {
font-size: var(--bottom-font-size); /* Aligns with movement count */
width: 100%; /* Full width like movement count */
height: 100%; /* Full height like movement count */
display: flex; /* Use flexbox for centering */
align-items: center; /* Vertically center text */
justify-content: center; /* Horizontally center text */
font-weight: 500; /* Match movement count weight */
line-height: 1; /* Match movement count */
}
/* === END OPUS LABEL SECTION === */
.quartets-container {
display: flex;
flex-wrap: wrap;
gap: clamp(6px, 0.8vw, 10px);
margin-left: clamp(6px, 0.8vw, 10px); /* Gap between label and cards */
}
/* Spacer between opus groups in combined rows */
.opus-spacer {
width: clamp(120px, 15vw, 177px); /* Scales with viewport */
height: var(--card-height);
flex-shrink: 0;
margin: 0 clamp(6px, 0.8vw, 10px); /* Gap on both sides */
}
.quartet-card {
width: var(--card-height); /* Square cards - width matches height */
height: var(--card-height);
background: white;
border: 2px solid #ddd;
border-radius: 4px;
padding: 0;
box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
cursor: pointer;
transition: all 0.2s;
position: relative;
display: flex;
flex-direction: column;
overflow: hidden;
}
.quartet-card:hover {
transform: translateY(-3px);
box-shadow: 3px 3px 10px rgba(0,0,0,0.2);
border-color: #888;
}
/* Subtle gray wash for quartets without minuets */
.quartet-card.no-minuet {
background: rgba(0, 0, 0, 0.03);
}
.mode-bar {
height: var(--top-section-height); /* Aligns with year-age section */
width: 100%;
flex-shrink: 0;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px;
box-sizing: border-box;
}
.mode-bar.major {
background: transparent;
}
.mode-bar.minor {
background: #E91E63;
}
.card-content {
padding: 0;
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
/* No position: relative - nickname positions relative to .quartet-card instead */
}
/* Text colors for major keys (on transparent background) */
.mode-bar.major .quartet-number {
font-size: var(--top-font-size); /* Aligns with year */
color: #888; /* Lighter gray */
font-weight: 600; /* Semi-bold */
line-height: 1; /* Exact alignment */
}
.mode-bar.major .gerard-number {
font-size: var(--top-font-size); /* Aligns with year */
font-weight: 700; /* Bold for emphasis */
color: #444; /* Darker for readability */
line-height: 1; /* Exact alignment */
}
/* Text colors for minor keys (on colored background) */
.mode-bar.minor .quartet-number {
font-size: var(--top-font-size); /* Aligns with year */
color: rgba(255, 255, 255, 0.85); /* Slightly less opaque */
font-weight: 600; /* Semi-bold */
line-height: 1; /* Exact alignment */
}
.mode-bar.minor .gerard-number {
font-size: var(--top-font-size); /* Aligns with year */
font-weight: 700; /* Bold for emphasis */
color: white;
line-height: 1; /* Exact alignment */
}
/* Key section - aligns with opus number */
.key-section {
height: var(--middle-section-height); /* Fixed height to match opus-number */
margin-bottom: var(--nickname-height); /* Space for nickname below (matches opus-number margin-bottom) */
flex-shrink: 0;
display: flex;
flex-direction: column;
justify-content: center; /* Vertically center */
align-items: center; /* Horizontally center */
padding: 0 8px; /* Horizontal padding only */
}
.key-signature {
text-align: center;
font-size: calc(var(--middle-font-size) * 0.64); /* Proportional to opus number */
font-weight: bold;
color: #222;
line-height: 1;
margin: 0; /* Remove margin for proper centering */
}
.key-mode {
text-align: center;
font-size: 0.75em;
color: #666;
margin: 0; /* Remove margin for proper centering */
line-height: 1;
margin-top: 2px; /* Small space above, not below key-signature */
}
.category-badge.grande {
background-color: #81C784;
color: #333;
}
.category-badge.piccola {
background-color: #CE93D8;
color: #333;
}
/* Links container for quartet cards - holds IMSLP, QR, and Recording indicator */
.card-links {
display: grid;
grid-template-columns: 1fr 1fr 1fr; /* 3 equal columns: IMSLP (left), QR (center), Recording (right) */
align-items: center;
padding: 2px 4px;
font-size: 0.65em;
font-weight: 400;
}
.imslp-link,
.qr-link,
.recording-indicator {
color: #2196F3;
text-decoration: none;
}
.imslp-link {
justify-self: start; /* Left-aligned in its grid cell */
}
.qr-link {
justify-self: center; /* Center-aligned in its grid cell */
}
.recording-indicator {
justify-self: end; /* Right-aligned in its grid cell */
cursor: default; /* Not clickable */
}
.imslp-link:hover,
.qr-link:hover {
text-decoration: underline;
color: #1976D2;
}
/* IMSLP link in opus label - positioned absolutely to not affect layout */
.opus-label .imslp-link {
position: absolute;
bottom: 100%; /* Position above bottom-section */
left: 0; /* Left align to match quartet cells */
margin-bottom: 1px; /* Minimal space below link */
padding: 2px 4px; /* Match quartet cell link padding */
font-size: 0.65em; /* Match quartet cell link size */
font-weight: 400; /* Lighter weight */
}
.movements-count {
text-align: center;
font-size: var(--bottom-font-size); /* Aligns with category badge */
height: var(--bottom-section-height); /* Fixed height to match bottom-section */
flex-shrink: 0;
display: flex;
align-items: center; /* Vertically center text */
justify-content: center; /* Horizontally center text */
margin: 0;
padding: 0 4px; /* Small padding for repeat glyphs */
border-radius: 0; /* Square edges like top bar */
font-weight: 500;
line-height: 1; /* Match category badge */
}
/* Repeat glyphs for parts.json quartets */
.movements-count .repeat-start,
.movements-count .repeat-end {
font-size: 1.75em; /* Make glyphs taller */
line-height: 1;
opacity: var(--show-ownership); /* Controlled by CSS variable */
}
.movements-count .repeat-start {
margin-right: auto;
}
.movements-count .repeat-end {
margin-left: auto;
}
/* Movement text container - stacks text and lines */
.movements-count .mvmt-text {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
}
/* SVG movement lines */
.movements-count .mvmt-lines-svg {
display: block;
}
/* Diverging color scheme: Purple (short) -> Gray (standard) -> Green (long) */
.movements-count.mvmt-1 {
background-color: #9C27B0;
color: white;
}
.movements-count.mvmt-2 {
background-color: #CE93D8;
color: #333;
}
.movements-count.mvmt-3 {
background-color: #B0BEC5;
color: #333;
}
.movements-count.mvmt-4 {
background-color: #81C784;
color: #333;
}
.movements-count.mvmt-5 {
background-color: #2E7D32;
color: white;
}
.nickname {
position: absolute; /* Remove from layout flow to preserve alignment */
top: 22px; /* Match dedication positioning */
left: 0;
right: 0;
max-height: 13px; /* Constrain to allocated space (22px to 35px) */
overflow: hidden; /* Clip overflow to prevent overlapping key section */
font-size: 0.55em; /* Slightly smaller for tighter fit in limited space */
font-style: italic; /* Same as dedication */
color: #999; /* Same as dedication */
text-align: center; /* Center align */
line-height: 1; /* Tight line-height to maximize space */
z-index: 2; /* Above mode bar */
pointer-events: none; /* Don't block clicks on mode bar */
}
.tooltip {
position: absolute;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 10px;
border-radius: 5px;
font-size: 0.85em;
pointer-events: none;
z-index: 1000;
max-width: 300px;
line-height: 1.4;
}
.dedication {
font-size: 0.7em;
color: #666;
font-style: italic;
margin-left: 5px;
}
/* === PRINT STYLES === */
/*
* PRINTING INSTRUCTIONS:
* For best results when printing or saving as PDF:
* 1. Open print dialog (Cmd+P / Ctrl+P)
* 2. Enable "Background graphics"
* 3. Set margins to "Custom" with:
* - Top: 0.25 inches
* - Bottom: 0 inches (minimum)
* - Left: 0 inches (minimum)
* - Right: 0 inches (minimum)
* Note: Browser print dialogs override CSS @page margins,
* so custom margins must be set manually.
*/
@media print {
@page {
margin: 0; /* Reset all margins to 0 first */
margin-top: 0.25in;
margin-left: 0in;
margin-right: 0in;
margin-bottom: 0in;
size: letter portrait;
}
/* Override responsive sizing with fixed desktop values for print */
:root {
--card-height: 140px;
--top-section-height: 20px;
--nickname-height: 15px;
--bottom-section-height: 20px;
}
body {
font-size: 9pt;
background: white !important;
zoom: 0.73; /* Scale to 73% to fit on page */
}
/* Override responsive font sizes with fixed desktop values for print */
h1 {
font-size: 3.5em !important;
max-width: 100%; /* Responsive width */
}
.subtitle {
font-size: 1.5em !important;
}
/* Page break controls */
.opus-row {
page-break-inside: avoid;
break-inside: avoid;
}
/* Clean aesthetics for print */
.opus-label {
width: 95px; /* Fixed width for print (not responsive) */
box-shadow: none !important; /* Remove sticky shadow */
/* position: static; /* Remove sticky positioning */*/
}
.opus-spacer {
width: 179px;
}
.quartet-card {
box-shadow: none !important;
border: 1px solid #999;
}
/* Hide interactive elements */
.tooltip {
display: none !important;
}
/* Ensure links and recording indicator are visible */
.imslp-link,
.qr-link,
.recording-indicator {
color: #2196F3;
}
/*
ChatGPT suggests that if your table has very thin rules (≤0.25pt),
this reduces Quartz/CUPS line thinning on some printers.
*/
* {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}
/* === MOBILE RESPONSIVE (iPhone) === */
/* Mini version of desktop - scaled down, no scrolling, pinch-to-zoom */
@media (max-width: 767px) {
/* Override responsive sizing with fixed desktop values */
:root {
--card-height: 140px;
--top-section-height: 20px;
--nickname-height: 15px;
--bottom-section-height: 20px;
}
body {
margin: 0;
/* Scale to fit ~1000px content into 375px viewport */
/* Use transform instead of zoom for Safari compatibility */
transform: scale(0.37);
transform-origin: 0 0;
width: calc(100% / 0.37); /* Compensate: content laid out at ~270% then scaled down */
}
h1 {
font-size: 2em; /* Fixed desktop size */
}
.opus-label {
width: 95px; /* Fixed desktop width */
}
.quartets-container {
flex-wrap: nowrap; /* Prevent cards from wrapping */
gap: 10px;
margin-left: 10px;
}
.opus-spacer {
width: 177px;
margin: 0 10px;
}
/* Prevent text wrapping in bottom sections */
.movements-count,
.opus-label .category-badge {
white-space: nowrap;
overflow: hidden;
}
/* Tighten repeat sign spacing */
.movements-count .repeat-start {
margin-left: -4px;
}
.movements-count .repeat-end {
margin-right: -4px;
}
.card-links {
padding: 0px 2px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header-group">
<h1>Luigi Boccherini (1743–1805) – 91 String Quartets</h1>
<p class="subtitle">see also <a href="https://quartetroulette.com/Boccherini/" target="_blank">Quartet Roulette</a></p>
</div>
<div id="visualization"></div>
</div>
<script>
// Load all JSON files: peters.json, parts.json, then opera.json
Promise.all([
d3.json('peters.json'),
d3.json('parts.json'),
d3.json('opera.json')
]).then(([petersData, partsData, operaData]) => {
// Create lookup maps keyed by Gerard number
const petersMap = new Map(petersData.map(p => [p.Gerard, { number: p.number, label: p.label }]));
const glabelMap = new Map(petersData.filter(p => p.glabel != null).map(p => [p.glabel, p.actual]));
const partsMap = new Map(partsData.map(p => [p.Gerard, p.edition]));
// Enrich opera data with peters nicknames and parts editions
operaData.forEach(opus => {
opus.quartets.forEach(quartet => {
// Add nickname from peters.json if available
if (petersMap.has(quartet.gerard)) {
const peters = petersMap.get(quartet.gerard);
quartet.nickname = `Peters ${peters.number}: ${peters.label}`;
}
// Add "see also" nickname for glabel references
else if (glabelMap.has(quartet.gerard)) {
const actual = glabelMap.get(quartet.gerard);
quartet.nickname = `Peters: see ${actual}`;
}
// Add edition info from parts.json for styling
if (partsMap.has(quartet.gerard)) {
quartet.edition = partsMap.get(quartet.gerard);
}
});
});
const data = operaData; // Use enriched data
const container = d3.select('#visualization');
// Create tooltip
const tooltip = d3.select('body')
.append('div')
.attr('class', 'tooltip')
.style('opacity', 0);
// Detect touch device
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
// Track currently active tooltip card (for touch devices)
let activeCard = null;
// On touch devices, enable pointer events on tooltip and prevent click passthrough
if (isTouchDevice) {
tooltip.style('pointer-events', 'auto');
tooltip.on('click', function(event) {
// Prevent clicks on tooltip from passing through
event.stopPropagation();
});
// Close tooltip when clicking outside on touch devices
d3.select('body').on('click', function(event) {
// Only close if clicking outside tooltip and cards
if (!event.target.closest('.quartet-card') && !event.target.closest('.tooltip')) {
tooltip.style('opacity', 0);
d3.selectAll('.quartet-card').style('border-color', '#ddd');
activeCard = null;
}
});
}
// === HELPER FUNCTIONS ===
// Render opus label (left side of row)
function renderOpusLabel(parent, opus) {
// Determine category for background gradient
const categoryClass = opus.quartets.length > 0 &&
opus.quartets[0].category.includes('grande') ? 'grande-bg' : 'piccola-bg';
const opusLabel = parent.append('div')
.attr('class', `opus-label ${categoryClass}`);
// === TOP: Year + Age (aligns with mode bar) ===
const BOCCHERINI_BIRTH_YEAR = 1743;
const age = opus.year - BOCCHERINI_BIRTH_YEAR;
const yearAge = opusLabel.append('div')
.attr('class', 'year-age');
yearAge.append('span')
.attr('class', 'year')
.text(opus.year);
yearAge.append('span')
.attr('class', 'age')
.text(`(${age})`);
// === Dedication (if present) - appears above opus number ===
if (opus.dedication) {
// Update dedications for display
let dedicationDisplay = opus.dedication;
if (opus.dedication === 'Monsieur le Baron du Beine de Malchamps') {
dedicationDisplay = 'Baron de Malchamps';
} else if (opus.dedication === 'Alli Signori Diletanti di Madrid') {
dedicationDisplay = 'Diletanti di Madrid';
} else if (opus.dedication === 'Infante Luis of Spain') {
dedicationDisplay = 'Infante Luigi di Spagna';
}
opusLabel.append('div')
.attr('class', 'dedication')
.text(dedicationDisplay);
}
// === MIDDLE: Opus number (NO "Op." prefix) ===
opusLabel.append('div')
.attr('class', 'opus-number')
.text(opus.opus);
// === BOTTOM: Bottom section (IMSLP link + category badge) ===
const bottomSection = opusLabel.append('div')
.attr('class', 'bottom-section');
// IMSLP link (if present at opus level)
if (opus.imslp) {
bottomSection.append('a')
.attr('class', 'imslp-link')
.attr('href', opus.imslp)
.attr('target', '_blank')
.attr('rel', 'noopener noreferrer')
.text('imslp')
.on('click', function(event) {
event.stopPropagation();
});
}
// Category badge below dedication/link
if (opus.quartets.length > 0) {
const category = opus.quartets[0].category;
const catClass = category.includes('grande') ? 'grande' : 'piccola';
const categoryText = category.includes('grande') ? 'Grande' : 'Piccola';
bottomSection.append('div')
.attr('class', `category-badge ${catClass}`)
.text(categoryText);
}
return opusLabel;
}
// Render quartet card
function renderQuartetCard(parent, opus, quartet, tooltip, isTouchDevice) {
const showTooltip = function(event) {
// Close any previously active tooltip
if (activeCard && activeCard !== this) {
d3.select(activeCard).style('border-color', '#ddd');
}
activeCard = this;
d3.select(this).style('border-color', '#333');
const movements = quartet.mvmts.map((m, i) => `${i + 1}. ${m}`).join('<br>');
const keyFull = `${quartet.key} ${quartet.major ? 'major' : 'minor'}`;
let tooltipText = `<strong>Opus ${opus.opus} #${quartet.number || '—'} \
in ${keyFull}, G. ${quartet.gerard}</strong><br>`;
if (quartet.nickname) {
tooltipText += `<em>"${quartet.nickname}"</em><br>`;
}
tooltipText += `${quartet.category}<br><br>`;
tooltipText += `<strong>Movements:</strong><br>${movements}`;
const x = event.pageX || (event.touches && event.touches[0].pageX) || 0;
const y = event.pageY || (event.touches && event.touches[0].pageY) || 0;
// Account for body transform scale on mobile
const bodyTransform = window.getComputedStyle(document.body).transform;
let scale = 1;
if (bodyTransform && bodyTransform !== 'none') {
// transform matrix(a, b, c, d, tx, ty) - 'a' is the scale factor
const match = bodyTransform.match(/matrix\(([^,]+)/);
if (match) scale = parseFloat(match[1]);
}
tooltip.html(tooltipText)
.style('left', (x / scale + 10) + 'px')
.style('top', (y / scale - 10) + 'px')
.style('opacity', 1);
};
const hideTooltip = function() {
d3.select(this).style('border-color', '#ddd');
tooltip.style('opacity', 0);
activeCard = null;
};
// Check if quartet has any minuets
const hasMinuet = quartet.mvmts.some(m => m.includes('Minuetto'));
// TODO: remove this once the data is updated
quartet.hasRecording = d3.randomUniform()(1) < .5;
const card = parent.append('div')
.attr('class', hasMinuet ? 'quartet-card' : 'quartet-card no-minuet');
if (isTouchDevice) {
// Touch device: use click to toggle, ignore mouse events
card.on('click', function(event) {
event.stopPropagation();
if (activeCard === this) {
// Clicking same card: close tooltip
hideTooltip.call(this);
} else {
// Clicking different card: show its tooltip
showTooltip.call(this, event);
}
});
} else {
// Desktop: use hover
card.on('mouseover', showTooltip)
.on('mouseout', hideTooltip);
}
// Add colored top bar for major/minor with numbers
const modeBar = card.append('div')
.attr('class', `mode-bar ${quartet.major ? 'major' : 'minor'}`);
// Gerard catalog number in the mode bar (left)
modeBar.append('div')
.attr('class', 'gerard-number')
.text(quartet.gerard);
// Quartet number (within opus) in the mode bar (right)
modeBar.append('div')
.attr('class', 'quartet-number')
.text(quartet.number ? `#${quartet.number}` : '');
// Create content container
const content = card.append('div')
.attr('class', 'card-content');
// Nickname if exists - appears above key section
if (quartet.nickname) {
content.append('div')
.attr('class', 'nickname')
.text(`"${quartet.nickname}"`);
}
// Key section (aligns with opus number in row header)
const keySection = content.append('div')
.attr('class', 'key-section');
// Key signature (replace -flat with ♭ symbol)
const keyDisplay = quartet.key.replace('-flat', '♭');
keySection.append('div')
.attr('class', 'key-signature')
.text(keyDisplay);
// Major/minor mode
keySection.append('div')
.attr('class', 'key-mode')
.text(quartet.major ? 'major' : 'minor');
// Links container (IMSLP, QR, and Recording) - appended to card, positioned above movement bar
const linksContainer = card.append('div')
.attr('class', 'card-links');
// IMSLP link (left-aligned in grid column 1)
const imslpLink = quartet.imslp || opus.imslp;
if (imslpLink) {
linksContainer.append('a')
.attr('class', 'imslp-link')
.attr('href', imslpLink)
.attr('target', '_blank')
.attr('rel', 'noopener noreferrer')
.text('imslp')
.on('click', function(event) {
event.stopPropagation(); // Prevent card click
});
} else {
// Empty span to maintain grid structure when no IMSLP link
linksContainer.append('span');
}
// QR link (center-aligned in grid column 2) - always present
linksContainer.append('a')
.attr('class', 'qr-link')
.attr('href', `https://quartetroulette.com/boccherini-g${quartet.gerard}/`)
.attr('target', '_blank')
.attr('rel', 'noopener noreferrer')
.text('QR')
.on('click', function(event) {
event.stopPropagation(); // Prevent card click
});
// Recording indicator (right-aligned in grid column 3)
if (quartet.hasRecording) {
linksContainer.append('span')
.attr('class', 'recording-indicator')
.text('♩');
} else {
// Empty span to maintain grid structure when no recording
linksContainer.append('span');
}
// Movement count (appended to card, not content, so it's at the bottom)
const mvmtCount = quartet.mvmts.length;
const movementsDiv = card.append('div')
.attr('class', `movements-count mvmt-${mvmtCount}`);
// Add repeat glyphs for quartets in parts.json
if (quartet.edition) {
movementsDiv.append('span')
.attr('class', 'repeat-start')
.text('𝄆');//.text('x');
}
// Create movement text container
const mvmtText = movementsDiv.append('div')
.attr('class', 'mvmt-text');
// Add movement count text
mvmtText.append('span')
.attr('class', 'mvmt-count-text')
.text(`${mvmtCount} movement${mvmtCount === 1 ? '' : 's'}`);
// Create SVG for movement lines
const lineWidth = 12;
const lineSpacing = 4;
const lineGap = lineWidth + lineSpacing;
// Find all minuet positions (handles multiple minuets like G.202)
const minuetIndices = new Set(
quartet.mvmts.map((m, i) => m.includes('Minuetto') ? i : -1)
.filter(i => i >= 0)
);
const svg = mvmtText.append('svg')
.attr('class', 'mvmt-lines-svg')
.attr('width', mvmtCount * lineGap - lineSpacing)
.attr('height', 3);
// Draw lines: white for minuets, black for others
svg.selectAll('line')
.data(d3.range(mvmtCount))
.enter()
.append('line')
.attr('x1', d => d * lineGap)
.attr('y1', 1.5)
.attr('x2', d => d * lineGap + lineWidth)
.attr('y2', 1.5)
.attr('stroke', d => minuetIndices.has(d) ? 'white' : 'black')
.attr('stroke-width', 1);
if (quartet.edition) {
movementsDiv.append('span')
.attr('class', 'repeat-end')
.text('𝄇');//.text('x');
}
return card;
}
// === AUTO-FLOW CONFIGURATION ===
const excludeFromCombining = new Set([64]); // Historical significance (Boccherini's final opus)
const maxQuartetsPerRow = 4; // Limit combined rows to max 4 quartets (e.g., 1+2 or 2+2)
const maxQuartetsToConsiderForCombining = 2; // Only combine opuses with ≤2 quartets
let currentRowOpuses = [];
let currentRowQuartetCount = 0;
// === RENDERING FUNCTIONS ===
// Render a single opus on its own row
function renderSingleRow(container, opus) {
const row = container.append('div')
.attr('class', 'opus-row');
// Render opus label
renderOpusLabel(row, opus);
// Quartets container
const quartetContainer = row.append('div')
.attr('class', 'quartets-container');
// Render quartet cards
opus.quartets.forEach(quartet => {
renderQuartetCard(quartetContainer, opus, quartet, tooltip, isTouchDevice);
});
}
// Render multiple opuses on a combined row with spacers
function renderCombinedRow(container, ...opuses) {
const row = container.append('div')
.attr('class', 'opus-row combined-pair');
opuses.forEach((opus, index) => {
// Render opus label
renderOpusLabel(row, opus);
// Quartets container for this opus
const quartetContainer = row.append('div')
.attr('class', 'quartets-container');
// Render quartet cards
opus.quartets.forEach(quartet => {
renderQuartetCard(quartetContainer, opus, quartet, tooltip, isTouchDevice);
});
// Add spacer between opus groups (but not after the last one)
if (index < opuses.length - 1) {
row.append('div')
.attr('class', 'opus-spacer');
}
});
}
// === MAIN RENDERING LOOP WITH AUTO-FLOW ===
data.forEach((opus, index) => {
const quartetCount = opus.quartets.length;
if (excludeFromCombining.has(opus.opus) || quartetCount > maxQuartetsToConsiderForCombining) {
// Render accumulated row if any, then render this opus alone
if (currentRowOpuses.length > 0) {
renderCombinedRow(container, ...currentRowOpuses);
currentRowOpuses = [];
currentRowQuartetCount = 0;
}
renderSingleRow(container, opus);
} else {
// Try to add to current row
if (currentRowQuartetCount + quartetCount <= maxQuartetsPerRow) {
currentRowOpuses.push(opus);
currentRowQuartetCount += quartetCount;
} else {
// Current row full, render it and start new row
if (currentRowOpuses.length > 0) {
renderCombinedRow(container, ...currentRowOpuses);
}
currentRowOpuses = [opus];
currentRowQuartetCount = quartetCount;
}
}
});
// Render any remaining accumulated row
if (currentRowOpuses.length > 0) {
renderCombinedRow(container, ...currentRowOpuses);
}
}).catch(error => {
console.error('Error loading JSON data:', error);
d3.select('#visualization')
.append('p')
.style('color', 'red')
.text('Error loading data. Please ensure peters.json, parts.json, and opera.json are in the same directory as this HTML file.');
});
</script>
</body>
</html>
[
{
"opus": 2,
"year": 1761,
"imslp": "https://imslp.org/wiki/6_String_Quartets,_Op.2_(Boccherini,_Luigi)",
"quartets": [
{
"number": 1,
"gerard": 159,
"key": "C",
"major": false,
"imslp": "https://imslp.org/wiki/String_Quartet_in_C_minor%2C_G.159_(Boccherini%2C_Luigi)",
"mvmts": [
"Allegro comodo",
"Largo",
"Allegro"
],
"category": "opera grande"
},
{
"number": 2,
"gerard": 160,
"key": "B-flat",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_B-flat_major%2C_G.160_(Boccherini%2C_Luigi)",
"mvmts": [
"Allegro non tanto",
"Largo",
"Fuga con spirito"
],
"category": "opera grande"
},
{
"number": 3,
"gerard": 161,
"key": "D",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_D_major%2C_G.161_(Boccherini%2C_Luigi)",
"mvmts": [
"Allegro moderato",
"Largo",
"Minuetto"
],
"category": "opera grande"
},
{
"number": 4,
"gerard": 162,
"key": "E-flat",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_E-flat_major%2C_G.162_(Boccherini%2C_Luigi)",
"mvmts": [
"Allegro spiritoso",
"Adagio",
"Minuetto"
],
"category": "opera grande"
},
{
"number": 5,
"gerard": 163,
"key": "E",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_E_major%2C_G.163_(Boccherini%2C_Luigi)",
"mvmts": [
"Allegro moderato",
"Adagio",
"Allegro assai"
],
"category": "opera grande"
},
{
"number": 6,
"gerard": 164,
"key": "C",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_C_major%2C_G.164_(Boccherini%2C_Luigi)",
"mvmts": [
"Allegro con spirito",
"Largo",
"Minuetto"
],
"category": "opera grande"
}
]
},
{
"opus": 8,
"year": 1768,
"dedication": "Infante Luis of Spain",
"quartets": [
{
"number": 1,
"gerard": 165,
"key": "D",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_D_major%2C_G.165_(Boccherini%2C_Luigi)",
"mvmts": [
"Allegro assai",
"Adagio",
"Allegro Rondeau"
],
"category": "opera grande"
},
{
"number": 2,
"gerard": 166,
"key": "C",
"major": false,
"imslp": "https://imslp.org/wiki/String_Quartet_in_C_minor%2C_G.166_(Boccherini%2C_Luigi)",
"mvmts": [
"Moderato",
"Largo",
"Allegro"
],
"category": "opera grande"
},
{
"number": 3,
"gerard": 167,
"key": "E-flat",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_E-flat_major%2C_G.167_(Boccherini%2C_Luigi)",
"mvmts": [
"Largo",
"Allegro",
"Tempo di Minuetto"
],
"category": "opera grande"
},
{
"number": 4,
"gerard": 168,
"key": "G",
"major": false,
"imslp": "https://imslp.org/wiki/String_Quartet_in_G_minor%2C_G.168_(Boccherini%2C_Luigi)",
"mvmts": [
"Allegro",
"Grave",
"Allegro"
],
"category": "opera grande"
},
{
"number": 5,
"gerard": 169,
"key": "F",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_F_major%2C_G.169_(Boccherini%2C_Luigi)",
"mvmts": [
"Andantino",
"Allegro",
"Tempo di Minuetto"
],
"category": "opera grande"
},
{
"number": 6,
"gerard": 170,
"key": "A",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_A_major%2C_G.170_(Boccherini%2C_Luigi)",
"mvmts": [
"Allegro brillante",
"Amoroso",
"Allegro"
],
"category": "opera grande"
}
]
},
{
"opus": 9,
"year": 1770,
"dedication": "Alli Signori Diletanti di Madrid",
"quartets": [
{
"number": 1,
"gerard": 171,
"key": "C",
"major": false,
"imslp": "https://imslp.org/wiki/String_Quartet_in_C_minor%2C_G.171_(Boccherini%2C_Luigi)",
"mvmts": [
"Allegro",
"Larghetto",
"Minuetto",
"Presto"
],
"category": "opera grande"
},
{
"number": 2,
"gerard": 172,
"key": "D",
"major": false,
"imslp": "https://imslp.org/wiki/String_Quartet_in_D_minor%2C_G.172_(Boccherini%2C_Luigi)",
"mvmts": [
"Grave - Allegro",
"Larghetto",
"Allegretto con moto"
],
"category": "opera grande"
},
{
"number": 3,
"gerard": 173,
"key": "F",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_F_major%2C_G.173_(Boccherini%2C_Luigi)",
"mvmts": [
"Largo - Allegro",
"Largo cantabile",
"Minuetto"
],
"category": "opera grande"
},
{
"number": 4,
"gerard": 174,
"key": "E-flat",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_E-flat_major%2C_G.174_(Boccherini%2C_Luigi)",
"mvmts": [
"Adagio",
"Allegro",
"Minuetto"
],
"category": "opera grande"
},
{
"number": 5,
"gerard": 175,
"key": "D",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_D_major%2C_G.175_(Boccherini%2C_Luigi)",
"mvmts": [
"Andante con moto",
"Allegro assai",
"Rondo Allegro"
],
"category": "opera grande"
},
{
"number": 6,
"gerard": 176,
"key": "E",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_E_major%2C_G.176_(Boccherini%2C_Luigi)",
"mvmts": [
"Andante grazioso",
"Allegretto",
"Minuetto",
"Allegro assai"
],
"category": "opera grande"
}
]
},
{
"opus": 15,
"year": 1772,
"quartets": [
{
"number": 1,
"gerard": 177,
"key": "D",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_D_major%2C_G.177_(Boccherini%2C_Luigi)",
"mvmts": [
"Presto",
"Allegro rondeau"
],
"category": "opera piccola"
},
{
"number": 2,
"gerard": 178,
"key": "F",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_F_major%2C_G.178_(Boccherini%2C_Luigi)",
"mvmts": [
"Allegretto",
"Allegro Minuetto"
],
"category": "opera piccola"
},
{
"number": 3,
"gerard": 179,
"key": "E",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_E_major%2C_G.179_(Boccherini%2C_Luigi)",
"mvmts": [
"Andantino",
"Prestissimo"
],
"category": "opera piccola"
},
{
"number": 4,
"gerard": 180,
"key": "F",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_F_major%2C_G.180_(Boccherini%2C_Luigi)",
"mvmts": [
"Prestissimo",
"Minuetto"
],
"category": "opera piccola"
},
{
"number": 5,
"gerard": 181,
"key": "E-flat",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_E-flat_major%2C_G.181_(Boccherini%2C_Luigi)",
"mvmts": [
"Adagio con sordina",
"Minuetto"
],
"category": "opera piccola"
},
{
"number": 6,
"gerard": 182,
"key": "C",
"major": false,
"imslp": "https://imslp.org/wiki/String_Quartet_in_C_minor%2C_G.182_(Boccherini%2C_Luigi)",
"mvmts": [
"Larghetto",
"Minuetto. Allegro moderato"
],
"category": "opera piccola"
}
]
},
{
"opus": 22,
"year": 1775,
"imslp": "https://imslp.org/wiki/6_String_Quartets,_G.183-188_(Op.22)_(Boccherini,_Luigi)",
"quartets": [
{
"number": 1,
"gerard": 183,
"key": "C",
"major": true,
"mvmts": [
"Allegro molto",
"Tempo di Minuetto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 2,
"gerard": 184,
"key": "D",
"major": true,
"mvmts": [
"Moderato",
"Minuetto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 3,
"gerard": 185,
"key": "E-flat",
"major": true,
"mvmts": [
"Adagio",
"Rondeau Allegro"
],
"category": "quartettino ... opera piccola"
},
{
"number": 4,
"gerard": 186,
"key": "B-flat",
"major": true,
"mvmts": [
"Allegretto moderato",
"Allegro vivace"
],
"category": "quartettino ... opera piccola"
},
{
"number": 5,
"gerard": 187,
"key": "A",
"major": false,
"mvmts": [
"Allegro con molto",
"Minuetto Amoroso"
],
"category": "quartettino ... opera piccola"
},
{
"number": 6,
"gerard": 188,
"key": "C",
"major": true,
"mvmts": [
"Andantino",
"Non troppo presto"
],
"category": "quartettino ... opera piccola"
}
]
},
{
"opus": 24,
"year": 1777,
"imslp": "https://imslp.org/wiki/6_String_Quartets,_G.189-194_(Op.24)_(Boccherini,_Luigi)",
"quartets": [
{
"number": 1,
"gerard": 189,
"key": "D",
"major": true,
"mvmts": [
"Moderato",
"Grave",
"Allegro assai"
],
"category": "opera grande"
},
{
"number": 2,
"gerard": 190,
"key": "A",
"major": true,
"mvmts": [
"Larghetto",
"Allegro spiritoso",
"Minuetto Amoroso"
],
"category": "opera grande"
},
{
"number": 3,
"gerard": 191,
"key": "E-flat",
"major": true,
"mvmts": [
"Allegro moderato",
"Adagio non tanto",
"Minuetto"
],
"category": "opera grande"
},
{
"number": 4,
"gerard": 192,
"key": "C",
"major": true,
"mvmts": [
"Moderato",
"Larghetto",
"Minuetto"
],
"category": "opera grande"
},
{
"number": 5,
"gerard": 193,
"key": "C",
"major": false,
"mvmts": [
"Allegro moderato",
"Larghetto",
"Allegro molto"
],
"category": "opera grande"
},
{
"number": 6,
"gerard": 194,
"key": "G",
"major": false,
"mvmts": [
"Allegro vivo assai",
"Adagio",
"Minuetto"
],
"category": "opera grande"
}
]
},
{
"opus": 26,
"year": 1778,
"imslp": "https://imslp.org/wiki/6_String_Quartets,_G.195-200_(Op.26)_(Boccherini,_Luigi)",
"dedication": "Monsieur le Baron du Beine de Malchamps",
"quartets": [
{
"number": 1,
"gerard": 195,
"key": "B-flat",
"major": true,
"mvmts": [
"Allegro moderato",
"Minuetto con moto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 2,
"gerard": 196,
"key": "G",
"major": false,
"mvmts": [
"Larghetto",
"Minuetto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 3,
"gerard": 197,
"key": "E-flat",
"major": true,
"mvmts": [
"Allegro vivace",
"Minuetto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 4,
"gerard": 198,
"key": "A",
"major": true,
"mvmts": [
"Larghetto",
"Minuetto con moto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 5,
"gerard": 199,
"key": "F",
"major": true,
"mvmts": [
"Allegretto",
"Minuetto Allegro"
],
"category": "quartettino ... opera piccola"
},
{
"number": 6,
"gerard": 200,
"key": "F",
"major": false,
"mvmts": [
"Andante appassionato ma con lento",
"Minuetto"
],
"category": "quartettino ... opera piccola"
}
]
},
{
"opus": 32,
"year": 1780,
"imslp": "https://imslp.org/wiki/6_String_Quartets,_G.201-206_(Op.32)_(Boccherini,_Luigi)",
"quartets": [
{
"number": 1,
"gerard": 201,
"key": "E-flat",
"major": true,
"mvmts": [
"Allegretto lentarello e affettuoso",
"Minuetto",
"Grave",
"Allegro vivace assai"
],
"category": "opera grande"
},
{
"number": 2,
"gerard": 202,
"key": "E",
"major": false,
"mvmts": [
"Largo sostenuto",
"Minuetto",
"Larghetto",
"Minuetto (same!)",
"Rondeau comodo assai"
],
"category": "opera grande"
},
{
"number": 3,
"gerard": 203,
"key": "D",
"major": true,
"mvmts": [
"Allegro vivo",
"Adagio",
"Allegro vivo ma non presto"
],
"category": "opera grande"
},
{
"number": 4,
"gerard": 204,
"key": "C",
"major": true,
"mvmts": [
"Allegro bizarro",
"Larghetto",
"Allegro con brio"
],
"category": "opera grande"
},
{
"number": 5,
"gerard": 205,
"key": "G",
"major": false,
"mvmts": [
"Allegro comodo",
"Andantino",
"Minuetto con moto",
"Allegro giusto"
],
"category": "opera grande"
},
{
"number": 6,
"gerard": 206,
"key": "A",
"major": true,
"mvmts": [
"Allegro",
"Andante lentarello",
"Minuetto con moto",
"Presto assai"
],
"category": "opera grande"
}
]
},
{
"opus": 33,
"year": 1781,
"quartets": [
{
"number": 1,
"gerard": 207,
"key": "E",
"major": true,
"mvmts": [
"Allegro spiritoso",
"Rondeau. Allegretto ma con moto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 2,
"gerard": 208,
"key": "C",
"major": true,
"mvmts": [
"Allegretto",
"Minuetto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 3,
"gerard": 209,
"key": "G",
"major": true,
"mvmts": [
"Andante con moto",
"Presto assai"
],
"category": "quartettino ... opera piccola"
},
{
"number": 4,
"gerard": 210,
"key": "B-flat",
"major": true,
"mvmts": [
"Andante lentarello",
"Minuetto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 5,
"gerard": 211,
"key": "E",
"major": false,
"mvmts": [
"Allegro brillante",
"Allegro vivo assai"
],
"category": "quartettino ... opera piccola"
},
{
"number": 6,
"gerard": 212,
"key": "E-flat",
"major": true,
"mvmts": [
"Adagio",
"Minuetto. Affetuoso"
],
"category": "quartettino ... opera piccola"
}
]
},
{
"opus": 39,
"year": 1787,
"quartets": [
{
"number": null,
"gerard": 213,
"key": "A",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_A_major%2C_G.213_(Boccherini%2C_Luigi)",
"mvmts": [
"Allegro moderato",
"Minuetto",
"Grave",
"Allegro giusto"
],
"category": "opera grande"
}
]
},
{
"opus": 41,
"year": 1788,
"imslp": "https://imslp.org/wiki/2_String_Quartets,_G.214-215_(Op.41)_(Boccherini,_Luigi)",
"quartets": [
{
"number": 1,
"gerard": 214,
"key": "C",
"major": false,
"mvmts": [
"Prestissimo",
"Tempo di Minuetto",
"Andante Flebile",
"Prestissimo (repeat of second half of 1)"
],
"category": "opera grande"
},
{
"number": 2,
"gerard": 215,
"key": "C",
"major": true,
"mvmts": [
"Allegretto moderato assai",
"Minuetto. Allegro",
"Larghetto affettuoso",
"Rondeau. Allegro moderato"
],
"category": "opera grande"
}
]
},
{
"opus": 42,
"year": 1789,
"quartets": [
{
"number": 1,
"gerard": 216,
"key": "A",
"major": true,
"mvmts": [
"Allegretto moderato",
"Minuetto. Allegro"
],
"category": "quartettino ... opera piccola"
},
{
"number": 2,
"gerard": 217,
"key": "C",
"major": true,
"mvmts": [
"Andante",
"Minuetto"
],
"category": "quartettino ... opera piccola"
}
]
},
{
"opus": 43,
"year": 1790,
"quartets": [
{
"number": 1,
"gerard": 218,
"key": "A",
"major": true,
"mvmts": [
"Allegretto moderato",
"Tempo di Minuetto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 2,
"gerard": 219,
"key": "A",
"major": true,
"mvmts": [
"Allegretto con moto",
"Minuetto"
],
"category": "quartettino ... opera piccola"
}
]
},
{
"opus": 44,
"year": 1792,
"quartets": [
{
"number": 1,
"gerard": 220,
"key": "B-flat",
"major": true,
"mvmts": [
"Maestoso assai",
"Tempo di Minuetto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 2,
"gerard": 221,
"key": "E",
"major": false,
"mvmts": [
"Andante larghetto",
"Minuetto amoroso",
"Andante allegretto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 3,
"gerard": 222,
"key": "F",
"major": true,
"mvmts": [
"Lento assai",
"Allegretto con moto",
"Tempo di Minuetto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 4,
"gerard": 223,
"key": "G",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_G_major%2C_G.223_'La_Tiranna'_(Boccherini%2C_Luigi)",
"nickname": "la tiranna",
"mvmts": [
"Presto",
"Tempo di Minuetto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 5,
"gerard": 224,
"key": "D",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_D_major%2C_G.224_(Boccherini%2C_Luigi)",
"mvmts": [
"Andantino lento",
"Allegro non tanto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 6,
"gerard": 225,
"key": "E-flat",
"major": true,
"mvmts": [
"Andantino",
"Minuetto on moto"
],
"category": "quartettino ... opera piccola"
}
]
},
{
"opus": 48,
"year": 1794,
"quartets": [
{
"number": 1,
"gerard": 226,
"key": "F",
"major": true,
"mvmts": [
"Andante moderato",
"Moderato con moto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 2,
"gerard": 227,
"key": "A",
"major": true,
"mvmts": [
"Andante lento",
"Tempo di Minuetto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 3,
"gerard": 228,
"key": "B",
"major": false,
"mvmts": [
"Allegretto moderato",
"Minuetto con moto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 4,
"gerard": 229,
"key": "E-flat",
"major": true,
"mvmts": [
"Andantino lento",
"Minuetto con un poco di moto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 5,
"gerard": 230,
"key": "G",
"major": true,
"mvmts": [
"Larghetto",
"Minuetto con un poco di moto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 6,
"gerard": 231,
"key": "C",
"major": true,
"mvmts": [
"Allegro vivace",
"Tempo di Minuetto affettuoso"
],
"category": "quartettino ... opera piccola"
}
]
},
{
"opus": 52,
"year": 1795,
"imslp": "https://imslp.org/wiki/4_String_Quartets,_G.232-235_(Op.52)_(Boccherini,_Luigi)",
"quartets": [
{
"number": 1,
"gerard": 232,
"key": "C",
"major": true,
"mvmts": [
"Allegro con moto",
"Minuetto",
"Adagio",
"Finale. Allegro giusto"
],
"category": "opera grande"
},
{
"number": 2,
"gerard": 233,
"key": "D",
"major": true,
"mvmts": [
"Allegro vivace assai",
"Andantino Patetico",
"Minuetto",
"Rondeau. Allegretto"
],
"category": "opera grande"
},
{
"number": 3,
"gerard": 234,
"key": "G",
"major": true,
"mvmts": [
"Allegretto con moto",
"Minuetto",
"Adagio",
"Rondeau. Allegro giusto"
],
"category": "opera grande"
},
{
"number": 4,
"gerard": 235,
"key": "F",
"major": false,
"mvmts": [
"Allegretto appassionato",
"Minuetto con modo",
"Adagio non tanto",
"Finale. Allegro assai"
],
"category": "opera grande"
}
]
},
{
"opus": 53,
"year": 1796,
"quartets": [
{
"number": 1,
"gerard": 236,
"key": "E-flat",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_E-flat_major%2C_G.236_(Boccherini%2C_Luigi)",
"mvmts": [
"Allegro (che appena si senta)",
"Tempo di Minuetto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 2,
"gerard": 237,
"key": "D",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_D_major%2C_G.237_(Boccherini%2C_Luigi)",
"mvmts": [
"Andantino Pausato",
"Minuetto. Allegro"
],
"category": "quartettino ... opera piccola"
},
{
"number": 3,
"gerard": 238,
"key": "C",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_C_major%2C_G.238_(Boccherini%2C_Luigi)",
"mvmts": [
"Allegro vivace",
"Minuetto. Allegro"
],
"category": "quartettino ... opera piccola"
},
{
"number": 4,
"gerard": 239,
"key": "A",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_A_major%2C_G.239_(Boccherini%2C_Luigi)",
"mvmts": [
"Allegro vivace",
"Rondeau. Allegro"
],
"category": "quartettino ... opera piccola"
},
{
"number": 5,
"gerard": 240,
"key": "C",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_C_major%2C_G.240_(Boccherini%2C_Luigi)",
"mvmts": [
"Andantino lento",
"Tempo di Minuetto"
],
"category": "quartettino ... opera piccola"
},
{
"number": 6,
"gerard": 241,
"key": "E-flat",
"major": true,
"imslp": "https://imslp.org/wiki/String_Quartet_in_E-flat_major%2C_G.241_(Boccherini%2C_Luigi)",
"mvmts": [
"Andantino amoroso",
"Minuetto. Allegro"
],
"category": "quartettino ... opera piccola"
}
]
},
{
"opus": 58,
"year": 1799,
"imslp": "https://imslp.org/wiki/6_String_Quartets,_G.242-247_(Op.58)_(Boccherini,_Luigi)",
"quartets": [
{
"number": 1,
"gerard": 242,
"key": "C",
"major": true,
"mvmts": [
"Allegro",
"Larghetto",
"Allegro vivo assai"
],
"category": "opera grande"
},
{
"number": 2,
"gerard": 243,
"key": "E-flat",
"major": true,
"mvmts": [
"Allegretto lento",
"Minuetto. Allegro",
"Larghetto Malincolico",
"Finale. Allegro vivo assai"
],
"category": "opera grande"
},
{
"number": 3,
"gerard": 244,
"key": "B-flat",
"major": true,
"mvmts": [
"Larghetto",
"Allegro vivo assai",
"Rondeau. Allegro giusto"
],
"category": "opera grande"
},
{
"number": 4,
"gerard": 245,
"key": "B",
"major": false,
"mvmts": [
"Allegro molto",
"Andantino lento",
"Rondeau. Allegro"
],
"category": "opera grande"
},
{
"number": 5,
"gerard": 246,
"key": "D",
"major": true,
"nickname": "Le Cornamuse",
"mvmts": [
"Andante sostenuto",
"Allegretto Gajo",
"Andante sostenuto como primo",
"Presto"
],
"category": "opera grande"
},
{
"number": 6,
"gerard": 247,
"key": "E-flat",
"major": true,
"mvmts": [
"Allegretto moderato",
"Larghetto",
"Allegro giusto e sostenuto"
],
"category": "opera grande"
}
]
},
{
"opus": 64,
"year": 1804,
"dedication": "Luciano Bonaparte",
"quartets": [
{
"number": 1,
"gerard": 248,
"key": "F",
"major": true,
"mvmts": [
"Allegro molto",
"Adagio non tanto",
"Allegro vivo ma non presto"
],
"category": "opera grande"
},
{
"number": 2,
"gerard": 249,
"key": "D",
"major": true,
"mvmts": [
"Allegro con brio"
],
"category": "opera grande"
}
]
}
]
[
{"Gerard": 159, "edition": "Edition Moeck", "opus": "2#1"},
{"Gerard": 160, "edition": "Edition Moeck", "opus": "2#2"},
{"Gerard": 161, "edition": "Edition Moeck", "opus": "2#3"},
{"Gerard": 162, "edition": "Edition Moeck", "opus": "2#4"},
{"Gerard": 163, "edition": "Edition Moeck", "opus": "2#5"},
{"Gerard": 164, "edition": "Edition Moeck", "opus": "2#6"},
{"Gerard": 170, "edition": "Peters", "opus": "8#6"},
{"Gerard": 172, "edition": "Peters", "opus": "9#2"},
{"Gerard": 176, "edition": "Peters", "opus": "9#6"},
{"Gerard": 177, "edition": "Peters", "opus": "15#1"},
{"Gerard": 194, "edition": "Peters", "opus": "24#6"},
{"Gerard": 198, "edition": "Peters", "opus": "26#4"},
{"Gerard": 201, "edition": "Edition Steglein", "opus": "32#1"},
{"Gerard": 202, "edition": "Edition Steglein", "opus": "32#2"},
{"Gerard": 203, "edition": "Edition Steglein", "opus": "32#3"},
{"Gerard": 204, "edition": "Edition Steglein", "opus": "32#4"},
{"Gerard": 205, "edition": "Edition Steglein", "opus": "32#5"},
{"Gerard": 206, "edition": "Edition Steglein", "opus": "32#6"},
{"Gerard": 207, "edition": "Simrock", "opus": "33#1"},
{"Gerard": 208, "edition": "Simrock", "opus": "33#2"},
{"Gerard": 209, "edition": "Simrock", "opus": "33#3"},
{"Gerard": 237, "edition": "Heinrichshofen", "opus": "53#2"},
{"Gerard": 243, "edition": "Ricordi", "opus": "58#2"},
{"Gerard": 245, "edition": "Ricordi", "opus": "58#4"},
{"Gerard": 247, "edition": "Ricordi", "opus": "58#6"},
{"Gerard": 232, "edition": "Peters", "opus": "52#1"}
]
[
{"number": 1, "label": "8#5", "actual": "15#1", "Gerard": 177, "glabel": 169},
{"number": 2, "label": "32#4", "actual": "26#4", "Gerard": 198, "glabel": 204},
{"number": 3, "label": "6#6", "actual": "8#6", "Gerard": 170, "glabel": null},
{"number": 4, "label": "10#2", "actual": "9#2", "Gerard": 172, "glabel": null},
{"number": 5, "label": "27#2", "actual": "24#6", "Gerard": 194, "glabel": null},
{"number": 6, "label": "10#5", "actual": "9#6", "Gerard": 176, "glabel": null},
{"number": 7, "label": "33#5", "actual": "32#5", "Gerard": 205, "glabel": 211},
{"number": 8, "label": "33#6", "actual": "32#6", "Gerard": 206, "glabel": 212},
{"number": 9, "label": "39#1", "actual": "52#1", "Gerard": 232, "glabel": 213}
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment