Skip to content

D3 renderer & GUI: file:// CORS fix, branch jitter, default GeoJSON, Summary-tree labels, flat output#48

Open
plemey wants to merge 15 commits into
masterfrom
fix/inline-data-cors
Open

D3 renderer & GUI: file:// CORS fix, branch jitter, default GeoJSON, Summary-tree labels, flat output#48
plemey wants to merge 15 commits into
masterfrom
fix/inline-data-cors

Conversation

@plemey

@plemey plemey commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

This PR bundles several independent renderer/UI improvements (plus the incidental
build fixes needed to compile/verify on a modern JDK). They are independent and
could be split if a reviewer prefers.


1. Fix file:// CORS error when loading a visualization

Opening a visualization from disk (file://) failed: the browser blocked the
data.json XHR as a CORS violation. The data is now inlined into main.js
(var SPREAD3_DATA = ...) via a loadData() helper that falls back to fetching
data.json when not inlined; D3Renderer substitutes the input JSON into the
generated main.js instead of copying a separate data.json.

2. Jitter the curvature of overlapping discrete branches

Branches connecting the same two locations were drawn as identical arcs and
superimposed. generateLinesLayer groups branches by their unordered location
pair and gives each a distinct arc side (sweep flag) and radius multiplier
so they fan out. Singletons/continuous branches unchanged. New lineCurvatureJitter.

3. Load a bundled world.geojson by default

data/geoJSON_maps/world.geojson is bundled into the jar and used as the default
for "Load GeoJSON file:". Utils.getDefaultGeojsonPath() resolves it (jar →
extract to temp; IDE → data/; cached). Panels default to it only when null, so a
manual Load overrides it. Note: the field now defaults non-null (a world layer
is always included unless another file is loaded; no in-GUI "none" option yet).

4. Indicate the active GeoJSON on the Load button

So users know a default is already loaded, the "Load GeoJSON file:" button now
shows the active file's name, tagged "(default)" when it is the bundled world map;
the tag drops once another file is loaded. Full path stays in the tooltip.

5. Rename "MCC tree" → "Summary tree" in the GUI

Relabel the user-facing input-/analysis-type strings (summary trees need not be
MCC). Internal enum codes (MCC_TREE, ...) unchanged.

6. Write output directly into the selected folder

Jar mode preserved the renderers/d3/d3renderer/ prefix from jar entry names,
nesting output deeply. That prefix is stripped so index.html/main.js land
directly in the user-selected folder, matching the IDE path.

7. Fix branches painted short of the location after increasing curvature

The curvature slider recomputed each branch's stroke-dasharray (reveal mask)
with getTotalLength() measured at the start of the transition — against the
old, shorter arc. The mask was then too short for the new, longer arc, leaving the
segment near the target unpainted (branch appears to stop short). The mask is now
recomputed on the transition end event from the final morphed length.

9. Bump gson 2.3.1 -> 2.11.0 and commons-io 2.4 -> 2.16.1

Both were ~2014 releases; upgraded to current stable versions (drop-in, no API
changes for the calls used). Updated the bundled jars, the build.xml includes and
the Eclipse .classpath. Verified end-to-end: commons-io drives the renderer
file inlining and gson parses the bundled world.geojson against the new jars.

10. Migrate Joda-Time to java.time (drop the dependency)

Joda-Time (2014) is superseded by the JDK's built-in java.time. Migrated all
usages across TimeParser, DateEditor, JsonMerger and KmlRenderer
(formatter/parse/print, LocalDate.of, getMonthValue, DateLocalDate, and
KmlRenderer's interval/Duration math) and removed joda-time-2.7.jar. Patterns
use uuuu (proleptic year) not yyyy (year-of-era), so the existing
negative/zero-year (ancient date) support is preserved. Verified TimeParser
round-trips slash, decimal and negative-year inputs (e.g. -100.5-0100/06/01).

11. Modernize the Swing look & feel (FlatLaf + light/dark toggle)

The desktop app rendered in a dated Metal/Nimbus look (on macOS it tried the
abandoned, unbundled Quaqua L&F and fell back to the default). FlatLaf
(FlatLightLaf) is now the primary look & feel, set before any Swing components
are created; the previous Quaqua/Nimbus/system-default attempts are kept as
fallbacks (gated behind !lafLoaded), and the FlatLaf call is guarded with
Throwable so a missing jar degrades gracefully. A new View menu adds a
Dark theme checkbox that swaps FlatLightLaf <-> FlatDarkLaf and calls
FlatLaf.updateUI() to restyle all open windows live. Bundles flatlaf-3.7.1.jar.
Independent of the deferred d3 renderer work.

12. Default the discrete location attribute to "location" / "loc"

The "Select location attribute" combo (discrete tree panel) previously defaulted
to the first annotation name. It now selects location by default, or loc as a
second choice, when present in the tree's annotation list (otherwise the first
item is kept). The selection is applied on the EDT in the tree-load worker's done() (after the
selector is built and added), so it behaves exactly like a manual pick: the
"Setup location coordinates" component appears underneath and
settings.locationAttributeName is set, with no second selection required.

13. Fix the Setup-locations window not rendering under FlatLaf

The location coordinates editor built its top button bar from a JMenuBar used
as a generic container (added to the dialog's NORTH, never set as the window's
menu bar). Under FlatLaf the menu-bar UI assumes real menus, so the panel inside
it was not painted until a hover/click forced a repaint (the previous L&F
tolerated the misuse). Replaced it with a plain JPanel -- same layout, no
behaviour change. This was the only such misuse in the codebase.

14. Narrow the default main window (1400 -> 800)

The main window opened at 1400x700, leaving a lot of empty horizontal space. The
default width is now 800 (height unchanged); the window stays resizable and the
300x200 minimum is unchanged.

Incidental: unblock the build on modern JDKs

  • build.xml: javac source/target 1.61.8.
  • Removed two dead, non-portable internal-JDK imports (com.sun.corba...Util in
    ContinuousTreeParser, com.sun.org.apache.xerces.internal...SynchronizedSymbolTable
    in MergePanel) that were unused and break/warn on newer JDKs.

Testing

  • ant build → BUILD SUCCESSFUL; jar contains patched main.js and
    geojson/world.geojson.
  • node --check passes on the template and a simulated inlined bundle.
  • Branch-jitter side/radius assignment verified via isolated logic test.
  • Default GeoJSON verified end-to-end (extract-from-jar → parseGeoJSON()).
  • Flattened output verified end-to-end (headless render() writes
    <output>/index.html + main.js with data inlined).

8. Resolve the running jar by absolute path

getRunningJarName() returned only the jar's basename, so new JarFile(name)
resolved it against the working directory — rendering only worked when the app
was launched from the jar's own directory and threw NoSuchFileException
otherwise. The absolute path is now parsed from the jar:file:/...!/... URL
(via URI, so encoded chars are handled), with a string fallback. Verified: a
headless render() now succeeds from an unrelated working directory.

Follow-up (deferred): d3 v3.5 -> v7 renderer upgrade

The renderer bundle (src/renderers/d3/d3renderer/main.js) embeds d3 v3.5.17
and jQuery v1.4.2. Upgrading is deliberately out of scope for this PR because
it is a standalone project, not an in-repo edit:

  • Source lives elsewhere. The bundle is built from
    phylogeography/d3-renderer
    (a webpack project, last touched Feb 2017). This repo contains only the built
    artifact. The app code to migrate is ~120 KB across ~11 modules
    (lines / topo / points / areas / counts / main / locations / time / ...), plus
    three vendored components (d3-legend, jquery.simple-color, d3.slider).
  • Two coupled major migrations. d3 v3 -> v7 (four majors; the app uses the
    full v3 API: d3.scale.*, d3.geo.*, d3.svg.*, d3.layout.hierarchy,
    d3.behavior.drag, callback-style d3.json) and webpack 1.9 -> 5 (config
    • loader ecosystem rewrite; some deps install via git+ssh).
  • Dead vendored pieces. d3.slider has no v4+ release (replace or rewrite);
    d3-svg-legend changed APIs.
  • No longer user-facing. The inline-data change (order in continuous tree distribution analysis #1) already removed the
    file:// CORS failure, so the remaining motivation is maintainability.

Recommended approach: a separate PR against phylogeography/d3-renderer (port the
modules onto a fresh webpack 5 + d3 v7 skeleton, replace the dead vendored
components), with visual regression testing across all four output types
(discrete, continuous, time-slicer, bayes), then drop the rebuilt bundle into
SpreaD3.

plemey and others added 2 commits June 26, 2026 09:05
The D3 renderer wrote the input JSON to a sibling data.json that main.js
fetched via d3.json(). Opened from disk (file://), that XHR is blocked by
CORS, so the visualization never loaded.

main.js now carries a `var SPREAD3_DATA = null;` hook and a loadData()
helper that uses the inlined data when present and otherwise falls back to
fetching data.json (still works over HTTP). D3Renderer replaces the marker
with the input JSON instead of copying a separate data.json.

Also unblock the build on modern JDKs (incidental, required to compile):
- build.xml: javac source/target 1.6 -> 1.8 (1.6 unsupported on JDK 19;
  StandardCharsets needs 1.7+)
- remove dead, non-portable internal-JDK imports that fail or warn on newer
  JDKs: com.sun.corba...Util (ContinuousTreeParser) and
  com.sun.org.apache.xerces.internal...SynchronizedSymbolTable (MergePanel);
  neither was used.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…impose

In discrete tree visualizations, several branches connecting the same two
locations were drawn as identical SVG arcs (same endpoints, same radius, same
sweep) and superimposed, so independent transitions between the same pair of
locations could not be visually distinguished.

generateLinesLayer now groups branches by their unordered location pair and
gives each member of a group a distinct arc side (sweep flag) and a small
radius multiplier, so they fan out: pairs split above/below the chord and
same-side members nest at different curvatures. Singleton pairs and continuous
branches (no locationId) are unchanged. The same per-branch side/multiplier is
applied in the curvature-slider redraw, so dragging the slider keeps the fan.

Controlled by a new `lineCurvatureJitter` (0 disables; larger spreads more).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@plemey plemey changed the title Inline visualization data into main.js to fix file:// CORS error Fix file:// CORS data load + jitter overlapping discrete branches Jun 26, 2026
Previously the "Load GeoJSON file:" control left settings.geojsonFilename null
until the user manually picked a file, so no base map was shown unless one was
loaded every time.

data/geoJSON_maps/world.geojson is now bundled into the jar (build.xml copies it
to geojson/world.geojson) and used as the default. Utils.getDefaultGeojsonPath()
resolves it: from a packaged jar the resource is extracted to a temp file named
world.geojson (so it can be read with a FileReader and the derived layer id stays
"world.geojson"); from the IDE the data/ folder is used directly. The result is
cached.

All four input panels (Discrete, Continuous, TimeSlicer, BayesFactors) default
settings.geojsonFilename to this path only when it is still null, so a manual
Load always overrides it; the button shows the active file as a tooltip, updated
on load. Jar grows ~3.5 -> 4.4 MB (the GeoJSON compresses well).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@plemey plemey changed the title Fix file:// CORS data load + jitter overlapping discrete branches file:// CORS fix, branch-curvature jitter, and default world GeoJSON Jun 26, 2026
plemey and others added 2 commits June 26, 2026 09:56
Relabel the user-facing input-type and analysis-type strings, since summary
trees need not be MCC trees. Internal enum codes (MCC_TREE, DISCRETE_TREE, ...)
are unchanged so behaviour and any persisted values are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In jar mode the renderer preserved the renderers/d3/d3renderer/ prefix from the
jar entry names, so output landed in <output>/renderers/d3/d3renderer/. Strip
that prefix so index.html and main.js are written directly into the user-selected
folder, matching what the IDE code path already did. index.html references
main.js by a relative path, so they only need to sit together.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@plemey plemey changed the title file:// CORS fix, branch-curvature jitter, and default world GeoJSON D3 renderer & GUI: file:// CORS fix, branch jitter, default GeoJSON, Summary-tree labels, flat output Jun 26, 2026
plemey and others added 10 commits June 26, 2026 10:14
Previously nothing indicated that world.geojson is loaded by default, so users
were unaware a base map was already selected. The "Load GeoJSON file:" button now
displays the active file's name; when it is the bundled default it is tagged
"(default)", and the tag drops once the user loads another file. The full path
remains in the tooltip. Applied to all four input panels.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The curvature slider recomputed each branch's stroke-dasharray (reveal mask)
with getTotalLength() evaluated at the start of the d3 transition, i.e. against
the old, shorter arc before it morphed to the new curvature. The mask was then
too short for the new, longer arc, leaving the segment near the target unpainted
so the branch appeared to stop short of its location (only when increasing
curvature).

Recompute the dash mask on the transition "end" event, from the final morphed
path length, so it matches the new geometry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
getRunningJarName() split the class URL on "/" and returned only the segment
containing "!" minus its last char, i.e. just the jar's basename (e.g.
"spreaD3.jar"). new JarFile(name) then resolved it against the current working
directory, so rendering only worked when the app was launched from the jar's
own directory and threw NoSuchFileException otherwise.

Parse the jar's absolute path out of the "jar:file:/path/app.jar!/..." URL (via
URI so encoded characters like spaces are handled), with a plain-string fallback.

Verified: a headless render() now succeeds from an unrelated working directory,
where it previously failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Both were ~2014 releases. These are drop-in upgrades to current stable versions
(no API changes for the calls used here). Updated the bundled jars, build.xml
zipgroupfileset includes, and the Eclipse .classpath entries.

Verified end-to-end against the new jars: commons-io still drives the renderer
file inlining (flat index.html/main.js written) and gson still parses the
bundled world.geojson.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Joda-Time is superseded by the JDK's built-in java.time (available since Java 8,
which the build now targets). Replaced all usages across TimeParser, DateEditor,
JsonMerger and KmlRenderer:

- DateTimeFormat.forPattern -> DateTimeFormatter.ofPattern
- formatter.print(d) / d.toString -> d.format(formatter)
- formatter.parseLocalDate / parseDateTime -> LocalDate.parse(s, formatter)
- new LocalDate(y, m, d) -> LocalDate.of(...); getMonthOfYear -> getMonthValue
- new LocalDate(java.util.Date) -> date.toInstant().atZone(...).toLocalDate()
- KmlRenderer interval/Duration math reworked onto LocalDate.atStartOfDay() +
  java.time.Duration (between / ofMillis)

Patterns use 'uuuu' (proleptic year), not 'yyyy' (year-of-era), because the
code supports negative/zero years for ancient dates; 'yyyy' cannot represent
them in java.time. Removed joda-time-2.7.jar and its build.xml / .classpath
references.

Verified: TimeParser round-trips slash, decimal and negative-year MRSD inputs
(e.g. -100.5 -> -0100/06/01) and the project builds without the jar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The desktop app rendered in a dated Metal/Nimbus look: on macOS it tried to load
Quaqua (abandoned, and not even bundled), so it fell back to the default L&F.

Set FlatLaf (FlatLightLaf) as the primary look & feel in Spread3UIApp, applied
before any Swing components are created. The previous Quaqua (mac) / Nimbus
(non-mac) / system-default attempts are kept, gated behind !lafLoaded, so they
only run if FlatLaf is unavailable. The FlatLaf call is guarded with Throwable so
a missing jar degrades gracefully.

Add a View menu (ViewMenuFactory) with a "Dark theme" checkbox that swaps
FlatLightLaf <-> FlatDarkLaf and calls FlatLaf.updateUI() to restyle all open
windows live. Bundle flatlaf-3.7.1.jar (build.xml + .classpath).

Verified: builds; FlatLaf loads as "FlatLaf Light"; toggle round-trips
Light -> Dark -> Light headlessly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Select location attribute" combo previously defaulted to the first
annotation name. It now selects "location" by default, or "loc" as a second
choice, when either is present in the tree's annotation list; otherwise the
existing first-item default is kept. The default is applied after the listener
is registered, so it also populates settings.locationAttributeName and advances
to the coordinates step, as a manual pick would.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…attribute

The previous change selected the default location attribute inside the
background populate block, before the selector was added to the panel. That
fired the selection listener too early (it runs resetFlags/removeChildComponents
and adds "Setup location coordinates"), so the setup component appeared next to
"Load tree file" and a manual re-selection was needed to place it correctly.

Move the default selection into the SwingWorker's done() (EDT, after the
selector is built and added). It now behaves exactly like a manual pick: the
listener adds "Setup location coordinates" underneath the selector and sets
settings.locationAttributeName, with no second selection required. Only fires
when the preferred value isn't already selected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The location coordinates editor built its top button bar from a JMenuBar used as
a generic container (BorderLayout, holding a button panel, added to the dialog's
NORTH -- never set as the window's menu bar). Under FlatLaf the menu-bar UI
assumes real menus, so the arbitrary panel inside it was not painted until a
hover/click forced a repaint; the previous Quaqua/Metal L&F tolerated the misuse.

Use a plain JPanel instead -- the correct component for a row of buttons, which
paints reliably under any look & feel. Same layout, no behaviour change; removed
the now-unused JMenuBar import. This was the only such misuse in the codebase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The main window opened at 1400x700, leaving a lot of empty horizontal space.
Set the default width to 800 (height unchanged); the window stays resizable and
the 300x200 minimum is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant