Commit Graph

51 Commits

Author SHA1 Message Date
Przemek Więch
980df35198 refactor: consolidate prober logic into shared helper and add test-testid attributes to core UI elements 2026-07-04 21:42:21 +02:00
Rob Ekl
621d581d87 Fix large GEDCOM files failing to load or freezing the browser (#296)
* fix(upload): bypass history.pushState size limit with in-memory GEDCOM store

Firefox caps history.pushState state at 640KB; Safari at ~512KB. The upload
handler was passing the raw GEDCOM string through navigate(..., {state}),
which calls history.pushState. A 10MB GEDCOM caused an immediate SecurityError
in Firefox and Safari, preventing the chart from ever loading.

Add a bounded in-memory store (gedcom_store.ts) capped to 1 entry — uploading
a new file evicts the previous one, preventing unbounded memory growth during
a session. navigate() now carries only a file fingerprint hash in the URL;
loadGedcom() retrieves the GEDCOM from the store by hash.

Update embedded.ts to store its injected GEDCOM under the key 'embedded'
before calling loadGedcom(), keeping the data path consistent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(fingerprint): replace 500ms full-file md5 with fast metadata + 4KB sample hash

The upload handler called md5() over the full GEDCOM string before navigating
— ~240ms for a 10MB file, blocking the main thread before the user saw
any feedback.

Replace with a hash over file.name|file.size|file.lastModified plus the first
and last 4KB of content. Runtime drops to ~1ms. The 4KB sample captures the
GEDCOM header (software name, export date, submitter info) which differs
between any two real exports, providing strong collision resistance for a local
session cache without a cryptographic guarantee.

Extract fileFingerprint to src/util/file_fingerprint.ts so it can be unit
tested independently of the React component.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* perf(parse): async GEDCOM conversion with event-loop yields and onProgress callback

convertGedcom was synchronous, blocking the main thread for the full parse
duration (~1.2s parseGedcom + ~700ms gedcomEntriesToJson in Safari). The
browser spinner could not update and GC could not run between steps.

Make convertGedcom async and insert await setTimeout(0) yields between the
four major steps (parse, convert, normalize, index). This breaks the work
into chunks, allows incremental GC, and keeps the spinner responsive.

Add onProgress?(status: string) => void to convertGedcom, prepareData,
loadGedcom, loadFromUrl, loadAndPrepareFile, and the DataSource interface.
All data source implementations (UploadedDataSource, GedcomUrlDataSource,
GoogleDriveDataSource) forward the callback down the load pipeline.
WikiTree and Embedded accept the parameter but do not use it.

Also skip the sessionStorage JSON.stringify for GEDCOM files over 512KB.
The serialized TopolaData (chartData 7.85MB + raw parse-gedcom tree 25MB
= 33MB) always exceeded the 5MB sessionStorage limit, wasting 10-20s in
Safari on a write that was guaranteed to fail.

Simplify loadGedcom to (hash, onProgress?) now that the GEDCOM string is
always retrieved from the in-memory store rather than passed as a parameter.
Remove console.log timing instrumentation and window.__topolaDebug globals
that were left in from debugging.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ui): skip redundant chart re-renders; persist progress overlay through D3 layout

Two related issues caused the chart to freeze or appear unresponsive:

1. The Chart useEffect has no dependency array, so it ran after every React
   render. Each call re-created a JsonDataProvider from all 23K individuals,
   re-ran the ancestor/descendant layout, and called getComputedTextLength for
   every visible text element (~500ms per call in Chrome, longer in Safari).
   For unrelated state changes this froze the browser 3-5 extra times per load.
   Fix: return early in renderChart() when neither initialRender nor
   resetPosition is set — the SVG is already correct.

2. The "Loading..." progress pill was mounted only in AppState.LOADING. When
   data finished parsing, React transitioned to SHOWING_CHART and unmounted the
   pill before the D3 layout (the slow part) had run. Users saw a blank screen
   with no feedback for several seconds.
   Fix: replace the state-gated pill with a persistent fixed overlay driven by
   a loadingStatus string, visible through both LOADING and the initial chart
   render. Add onFirstRender() callback to Chart; App clears loadingStatus when
   the first D3 layout completes.

Wire the onProgress callback from the load pipeline into setLoadingStatus so
users see step-by-step progress ("Step 1/4: parsing GEDCOM..." through
"Rendering chart (23,909 people)...").

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* perf(search): pre-build Lunr index during browser idle time

Building the full-text search index over 23K individuals takes ~600ms
synchronously. This was deferred to the first keystroke, freezing the input
field for up to 3 seconds in Safari with no user feedback.

Schedule the build via requestIdleCallback (5s timeout) after data loads,
with a 200ms setTimeout fallback for browsers without requestIdleCallback.
The index is typically warm before the user types. getOrBuildIndex() still
builds synchronously on the first keystroke if idle scheduling has not yet
fired (e.g. user types immediately after load).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: add coverage for gedcom_store, loadGedcom store path, and fileFingerprint

gedcom_store.spec.ts
  - Returns undefined for unknown hash
  - Stores and retrieves GEDCOM by hash
  - Evicts the previous entry when a new file is stored (bounded-to-1 behavior)
  - Returns undefined for a hash that was evicted

load_data_store.spec.ts
  - loadGedcom throws ERROR_LOADING_UPLOADED_FILE when hash is absent from store
  - loadGedcom successfully parses GEDCOM retrieved from the in-memory store
  - onProgress callback receives all four step messages in the correct order

upload_menu.spec.ts (via src/util/file_fingerprint.ts)
  - Returns a valid 32-character hex hash
  - Produces identical hashes for identical inputs
  - Differs for different filename, file size, content beyond 4KB boundary,
    and image file list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ui): clear loading pill for all chart types including Donatso

The progress pill persisted indefinitely on the Donatso chart because
DonatsoChart did not call onFirstRender — only the D3-based Chart did.

Add onFirstRender? to DonatsoChartProps and call it from the existing
useEffect on the first render, consistent with how Chart handles it.
Pass onFirstRender={() => setLoadingStatus('')} from renderChart in app.tsx.

Update visual regression snapshots for macOS (darwin) to reflect the new
progress pill and its correct dismissal across all chart types. The 12
non-Donatso failures were pre-existing snapshot staleness; chart-donatso
was the one newly introduced by this PR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix thread onProgress through EmbeddedDataSource.onMessage to loadGedcom

The _onProgress parameter accepted by loadData was never forwarded to the
onMessage event handler, silently dropping all step-progress callbacks for
embedded GEDCOM loads. The progress pill stayed at 'Loading…' for the full
parse duration instead of showing the four-step breakdown.

Thread onProgress through the event-listener closure by adding it as a
parameter to onMessage and passing it to loadGedcom.

* Make gedcom_store tests independent of execution order

Jest caches module instances, so the module-level let variables persisted
across tests. Running with --randomize or in parallel could cause test 1
('returns undefined for unknown hash') to see a hash left by a previous test.

Export clearStoredGedcom() for testing and call it in beforeEach so each test
starts from a clean slate regardless of execution order.

* Document intentional pre-animation timing of onFirstRender in Chart

onFirstRender fires when the chart SVG is in the DOM, before the D3
fade-in animation completes (~400ms). A reviewer noted this as a potential
issue, but moving it into animationPromise.then() would keep the progress
pill visible for 400ms after the chart is already visible and interactive,
creating a confusing overlap. The current timing is correct.

Add a comment explaining the design intent.

* Pass onProgress to WikiTree and Embedded data sources at call site

Both call sites in the loadData callback omitted the onProgress parameter
that all other sources (UPLOADED, GEDCOM_URL, GOOGLE_DRIVE) already forward.
WikiTree's implementation ignores it today (_onProgress), but wiring it here
ensures parity and makes future progress reporting possible without touching
app.tsx again. Embedded now receives it and threads it to loadGedcom.

* Use const-scoped handles in search useEffect to eliminate type casts

The shared 'let handle' variable was typed as a union of setTimeout and
requestIdleCallback return types, requiring unsafe casts (handle as number,
handle as ReturnType<typeof setTimeout>) in the cleanup closures. Each
cleanup closure captured the shared variable by reference rather than value.

Use a const declaration in each branch so TypeScript infers the correct
type automatically and no casts are needed. The early-return form also
makes the two branches structurally symmetric.

* Remove window event listener in EmbeddedDataSource after GEDCOM received

The listener registered in loadData was never removed, causing it to
accumulate on every loadData call. In a multi-file workflow the second
load would have two listeners firing on the same message, the first
with a stale resolve/reject pair.

Wrap resolve and reject so the listener removes itself on first
settlement, making the registration effectively once-per-load.

* Minor code fixes. Moved test to a different file

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Przemek Więch <pwiech@gmail.com>
2026-06-05 00:14:58 +02:00
Rokas
059c9d9413 Add configurable place display and shortening (#294)
* Add configurable place display and shortening

* Fix prettier formatting stuff

* Move place shortening logic to Chart
2026-06-04 20:33:08 +02:00
Przemek Więch
6ee52a1d5f Fix turning off colors 2026-05-12 17:19:13 +02:00
Przemek Więch
8202c9cd05 Improve code quality
Fix lint warnings
2026-05-11 18:06:31 +02:00
Przemek Więch
fa88d4f86b Run prettier on all code 2026-04-28 17:07:49 +02:00
Frank Buchholz
4800b04eb1 View details for different persons without rebuilding the tree (#281) 2026-04-20 17:41:03 +02:00
Przemek Więch
6088d5d599 Run prettier on all files 2026-03-04 20:12:22 +01:00
Przemek Więch
ad7eadb4fd Improve exporting PDF files.
Changed strategy from rendering the SVG to a canvas and then exporting
the canvas as an image, to directly exporting the SVG as an image.
This results in much smaller PDF files.
2026-02-20 23:46:31 +01:00
czifumasa
2a0c963789 Sidebar improvements (Collapse/Expand, Mobile view) (#215)
* Extract sidebar to new component

* Add sidebar toggle

* Fix scrollbars sometimes appearing even at maximum zoom-out
2025-09-09 10:41:45 +02:00
Przemek Więch
84a0149e0e update prettier config and run on all files 2025-02-06 00:11:24 +01:00
Przemek Więch
ae7357f27c First version of integrating donatso/family-chart #185 2025-01-16 17:56:11 +01:00
Przemek Więch
042a842f4f Upgrade d3 to newest version 2025-01-15 00:45:57 +01:00
Przemek Więch
f0bb5c1273 Upgrade to react 18 2025-01-14 00:24:40 +01:00
Kent Grigo
05443954f6 Toggle showing sex (#141)
Fixes #138
2023-02-01 15:58:18 +01:00
Kent Grigo
1fac47f78b Toggle showing IDs (#131) 2023-01-08 14:26:23 +01:00
Przemek Wiech
1bb65c0f23 Fixed build 2022-02-04 20:34:19 +01:00
Przemek Wiech
1e3cf7a24f Fixed incorrect chart positioning after transition 2022-02-04 19:03:15 +01:00
Przemek Wiech
6e8b6c7b9e Use useIntl hook for i18n 2021-11-04 16:58:11 +01:00
Przemek Wiech
e444e25177 Replaced at="large with greaterThanOrEqual="large"
Based on warnings in the console
2021-11-02 00:07:26 +01:00
Przemek Wiech
2d4ca439d5 Refactored Chart component from class-based to functional 2021-11-02 00:01:41 +01:00
Przemek Wiech
3914226042 Added settings tab with color settings (#6) 2021-10-25 13:34:03 +02:00
Przemek Wiech
e2d4b07c9e Replaced usages of Responsive with @artsy/fresnel
Responsive has been removed in newest versions of semantic-ui-react
2021-04-11 20:50:07 +02:00
Przemek Wiech
df5ae76180 Upgraded to the newest version of react-intl 2021-04-02 00:40:57 +02:00
Przemek Wiech
44e1954dda Upgraded npm package versions 2021-03-31 00:07:25 +02:00
Przemek Wiech
68b9f4baa5 lazy load jspdf to speed up page load 2020-04-29 22:29:25 +02:00
Przemek Wiech
a12515008e Fixed transitions failing 2020-04-29 00:40:39 +02:00
Przemek Wiech
5c002c5278 Updated dependencies
Replaced d3 dependency with smaller d3-* deps
2020-04-29 00:13:30 +02:00
Przemek Wiech
392548e3a1 Added freeze URL parameter to disable animations 2020-04-20 23:44:22 +02:00
Przemek Wiech
36dabfc4a1 Remove option to turn off zoom 2020-04-01 18:57:39 +02:00
Przemek Wiech
3650082386 Hide zoom buttons on small screens 2020-03-16 23:02:00 +01:00
Przemek Wiech
73dceaaae0 Changed zoom button element from <a> to <button> 2020-03-16 18:40:40 +01:00
Przemek Wiech
9e37659288 Added zoom in/out buttons 2020-03-16 17:45:36 +01:00
Przemek Wiech
09e8d19c7a Limit zooming out when the chart is small 2020-03-14 23:02:37 +01:00
Przemek Wiech
442de4c7c5 Improved zoom support 2020-03-14 16:51:07 +01:00
Przemek Wiech
67c78b5982 Added zooming capability
This feature is not enabled by default yet
and can be only enabled by adding &enabledZoom=true to the URL
2020-03-13 00:28:43 +01:00
Przemek Wiech
e4e865695b Synchronize transition animations 2020-01-26 23:34:10 +01:00
Przemek Wiech
48cfb221f3 Load more data when navigating in WikiTree 2020-01-26 14:49:30 +01:00
Przemek Wiech
c988f4180e Added fancy tree as an experimental feature 2019-11-18 22:28:06 +01:00
Przemek Wiech
9bd1720122 Added menu option to display the relatives chart 2019-05-20 22:13:18 +02:00
Przemek Wiech
25c5438a04 Use async/await for async functions. 2019-03-16 23:53:20 +01:00
Przemek Wiech
f75eee4e82 Inline images as data URLs when downloading PDF/PNG/SVG. 2019-03-11 22:26:37 +01:00
Przemek Wiech
730642fb4e Added 'Download PDF' option using jsPDF. 2019-03-03 01:17:44 +01:00
Przemek Wiech
1083708e15 Use canvas.drawImage instead of canvg for better results. 2019-03-03 00:04:34 +01:00
Przemek Wiech
1dc90e3262 Using file-saver package for better cross-browser support. 2019-03-02 13:31:16 +01:00
Przemek Wiech
e6be58c20b Added "Download PNG" option 2019-02-24 14:08:38 +01:00
Przemek Wiech
285294a3c0 Added "Download SVG" option 2019-02-22 23:36:25 +01:00
Przemek Wiech
a920147ba8 Added 'print' button to print the current chart 2019-02-21 22:38:22 +01:00
Przemek Wiech
631f8814a9 Pass locale to chart rendering library 2019-02-14 22:40:08 +01:00
Przemek Wiech
f0addd57a9 Migrated from react-scripts-ts to react-scripts 2019-02-06 22:57:38 +01:00