Music Graph Project: UI Enhancements

3 minute read

Phase 10 focused on UI improvements - We wanted to display the data that we have have in the database but have been hiding from the user. We created a side panel that pulls out when you click on band or a genre. It will show things like parent genres or in case of a band it will show all of the genres that band belongs too.This was a great Javascript learning experience for me. Claude will tell you the rest


Screenshots

sidepanel1

sidepanel2

The Problem: Hidden Data

Since Phase 6, the application has stored multiple relationships:

  • Bands have a primary genre (shown in graph) and additional genres (stored but hidden)
  • Genres have a primary parent (shown in graph) and additional parents (stored but hidden)

Users could only see primary relationships in the graph. All that rich “also belongs to” data was invisible unless you looked at the admin panel.

The Solution: Detail Panel

Clicking any node now opens a slide-in panel showing all relationships:

For Bands:

  • Primary genre (clickable)
  • All genres the band belongs to

For Genres:

  • Type badge (root/intermediate/leaf)
  • Primary parent
  • All parent genres
  • Child genres
  • All bands in this genre

Clicking any link in the panel navigates the graph to that entity and updates the panel - you can explore the entire graph without leaving the visualization.

Design Decision: Generic vs Entity-Specific

We had two implementation options:

Option 1 (Chosen): Generic Panel System One renderer that handles any entity type based on a data structure:

var entityData = {
    bands: {
        'pantera': {
            name: 'Pantera',
            fields: [
                { label: 'Primary Genre', value: 'Groove Metal', link: 'groove-metal' },
                { label: 'All Genres', values: [...], links: [...] }
            ]
        }
    }
};

Option 2: Entity-Specific Functions Separate showBandDetails() and showGenreDetails() functions with hardcoded HTML.

We chose the generic approach because:

Change needed Generic Entity-Specific
New entity type Data only New function + HTML
New field on entity Data only Edit function + HTML
New field type One else-if Edit ALL functions

This matters for future growth. When we add band descriptions, Wikipedia links, or Spotify embeds, we add them to the data structure and (maybe) one rendering case - not scattered across multiple functions.

Template Inheritance: DRY Templates

Phase 10 also tackled template duplication. We had 9 templates, each repeating:

  • HTML boilerplate
  • Navbar (15 lines)
  • Flash messages
  • Footer

Before: Copy-Paste Everywhere

Every template file started with ~30 lines of identical boilerplate.

After: Jinja2 Inheritance

<!-- base.html -->
<nav class="navbar">...</nav>
{% block content %}{% endblock %}
<footer>...</footer>

<!-- login.html -->
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<form>...</form>
{% endblock %}

Results:

  • login.html: 50 → 26 lines
  • admin.html: 115 → 83 lines
  • Navbar change: 1 file instead of 9
  • Footer now appears on all pages automatically

This is the same inheritance pattern used in Python classes or Terraform modules - define a base, override specific parts.

The JavaScript Learning

This phase hit my #1 learning priority: JavaScript. Key concepts that clicked:

DOM Manipulation:

// Get element, modify its CSS classes
document.getElementById('panel').classList.add('open');

CSS Does the Animation:

.detail-panel {
    right: -400px;  /* Hidden */
    transition: right 0.3s ease;
}
.detail-panel.open {
    right: 0;  /* Visible */
}

JavaScript just toggles the class. CSS handles the smooth slide-in. This separation of concerns (JS = state, CSS = appearance) was new to me.

Building HTML Dynamically:

entity.fields.forEach(function(field) {
    html += '<div class="field">';
    html += '<label>' + field.label + '</label>';
    // ... render based on field type
    html += '</div>';
});
document.getElementById('panel-content').innerHTML = html;

Loop through data, build string, inject into DOM. Simple pattern, but powerful.

UX Refinement: Toggle Behavior

After initial implementation, clicking a genre twice kept the panel open showing the same data. Expected behavior: click to open, click again to close.

The fix was tracking state:

var currentPanelEntity = null;

function showDetailPanel(entityType, entityId) {
    var entityKey = entityType + ':' + entityId;

    if (currentPanelEntity === entityKey) {
        closeDetailPanel();  // Same entity = close
        return;
    }
    // ... show panel
    currentPanelEntity = entityKey;
}

Small change, big UX improvement. This came from actually using the feature and noticing what felt wrong.

What’s Deferred

Issue #16 (graph scalability / progressive disclosure) remains open. The current user (Aidan) likes the chaotic floating graph. When scale becomes painful, we’ll revisit - but not before it’s actually a problem.

What’s Next

Phase 11 will focus on infrastructure modernization:

  • Remote Terraform state
  • Packer golden images
  • Automated dev database sync from production

The “lite” approach continues to work well - ship what’s needed, defer what isn’t.


This post is part of my Music Graph Project series, documenting the journey of building a full-stack application with AI assistance.