Skip to content

Conversation

knollengewaechs
Copy link
Contributor

@knollengewaechs knollengewaechs commented Sep 5, 2025

URL of deployed dev instance (used for testing):

Steps to test:

  • Open an annotation with some skeletons.
  • In the skeleton tab, select ">More" and select "Download visible trees as CSV"
  • A zip should be downloaded, containing two CSV files, one for the nodes and one for the edges
  • Check that this is working for ND datasets
  • check that the positions are correct for transformed data
  • Due to refactoring, you should check that the segment statistics download, the timetracking CSV and timespan CSV download are still working fine

TODOs:

  • wait for answers in thread (see below)
  • export comments
  • rename to nodes.csv
  • explain columns in docs

Issues:

Context

https://scm.slack.com/archives/C5AKLAV0B/p1757060937665459


(Please delete unneeded items, merge only when none are left open)

  • Added changelog entry (create a $PR_NUMBER.md file in unreleased_changes or use ./tools/create-changelog-entry.py)
  • Added migration guide entry if applicable (edit the same file as for the changelog)
  • Updated documentation if applicable
  • Adapted wk-libs python client if relevant API parts change
  • Removed dev-only changes like prints and application.conf edits
  • Considered common edge cases
  • Needs datastore update after deployment

@knollengewaechs knollengewaechs self-assigned this Sep 5, 2025
Copy link
Contributor

coderabbitai bot commented Sep 5, 2025

📝 Walkthrough

Walkthrough

Adds a new CSV helper module, migrates CSV serialization/saving to it, updates several CSV export call sites to use the helper, enables downloading skeleton tree nodes and edges as CSV (packaged into a ZIP) from the Skeleton tab, removes the CSV row helper from utils, and updates docs and release notes.

Changes

Cohort / File(s) Summary of Changes
CSV helper module
frontend/javascripts/viewer/model/helpers/csv_helpers.ts
New module exporting transformToCSVRow, saveAsCSV, getTreeNodesAsCSV, and getTreeEdgesAsCSV; builds CSV rows for nodes and edges and provides a save helper (uses file-saver and viewer helpers).
Skeleton tab — CSV export & ZIP packaging
frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx
Added handleCSVDownload(applyTransforms: boolean); imports CSV helpers; generates nodes and edges CSVs; packages them into a ZIP and triggers download via saveAs; updated menu labels/tooltips and added annotationId to mapped state.
Segments statistics CSV
frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx
Switched CSV assembly to produce arrays of CSV rows and delegate saving to saveAsCSV/transformToCSVRow from csv_helpers; removed manual Blob/string assembly; retained data/headers logic.
Admin time tracking CSV
frontend/javascripts/admin/statistic/time_tracking_overview.tsx
Replaced manual CSV string/Blob assembly with transformToCSVRow and saveAsCSV from csv_helpers; adjusted imports and retained per-row field logic.
Utils cleanup
frontend/javascripts/libs/utils.ts
Removed exported transformToCSVRow (migrated to csv_helpers).
Docs & release notes
docs/data/export_ui.md, docs/skeleton_annotation/import_export.md, unreleased_changes/8898.md
Documentation updated to mention CSV export alongside NML and to describe CSV contents; new unreleased note added for CSV export of skeleton nodes and edges.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

frontend, documentation

Suggested reviewers

  • philippotto
  • daniel-wer

Poem

I nibble rows and edges with care,
I stitch them into CSV fare.
I zip them snug, send them on their way,
Columns neat and bright as day. 🐇📥

Pre-merge checks and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title Check ✅ Passed The PR title "Export skeleton trees as CSV" is concise and accurately describes the primary change — adding CSV export for skeleton trees (nodes and edges) — so it clearly reflects the main change set and is suitable for history scanning.
Linked Issues Check ✅ Passed This PR satisfies the coding objectives of the linked issue [#8635]: it adds a CSV export option alongside NML, exports visible trees' nodes and edges into nodes.csv and edges.csv within a ZIP, and emits node attributes as separate CSV columns (e.g., annotationId, treeId, nodeId, radius, x/y/z, rotation, additional coordinates, viewport, magnification, bitDepth, interpolation, timestamp, and comments). The implementation gathers visible trees, supports transformed coordinates and ND datasets, and provides the ZIP download flow, so the primary coding requirements are met.
Out of Scope Changes Check ✅ Passed All modified files and changes in the provided summary are related to the CSV export feature or necessary supporting work: introduction of a csv_helpers module and its reuse in other CSV exports, the ZIP-based download flow in the skeleton tab, documentation and changelog updates, and small prop/API additions (annotationId in mapStateToProps and handleCSVDownload) required to implement edges export. I do not see unrelated functional changes outside the export/refactor scope in the given summary.
Description Check ✅ Passed The PR description is directly related to the changeset: it includes a test deployment URL, step-by-step testing instructions, completed TODOs, and correctly references the linked issue (#8635), satisfying the lenient requirement for this check.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch visible-tree-csv-export

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f6b86f5 and f6c7751.

📒 Files selected for processing (1)
  • docs/skeleton_annotation/import_export.md (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/skeleton_annotation/import_export.md
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build-smoketest-push
  • GitHub Check: frontend-tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@knollengewaechs knollengewaechs marked this pull request as ready for review September 5, 2025 14:54
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx (1)

32-34: Fix CSV header typo: segmendId → segmentId

User-facing column name.

-  `segmendId,segmentName,groupId,groupName,volumeInVoxel,volumeIn${dataSourceUnit}3,boundingBoxTopLeftPositionX,boundingBoxTopLeftPositionY,boundingBoxTopLeftPositionZ,boundingBoxSizeX,boundingBoxSizeY,boundingBoxSizeZ`;
+  `segmentId,segmentName,groupId,groupName,volumeInVoxel,volumeIn${dataSourceUnit}3,boundingBoxTopLeftPositionX,boundingBoxTopLeftPositionY,boundingBoxTopLeftPositionZ,boundingBoxSizeX,boundingBoxSizeY,boundingBoxSizeZ`;
frontend/javascripts/admin/statistic/time_tracking_overview.tsx (2)

115-118: Guard is ineffective; can still crash on null list

Use a proper null/empty check before mapping.

-    if (filteredTimeEntries?.length === null) {
+    if (filteredTimeEntries == null || filteredTimeEntries.length === 0) {
       return;
     }

48-51: Fix state initialization for selectedProjectIds

This currently passes a type in value position; it won’t compile.

-  const [selectedProjectIds, setSelectedProjectIds] = useState(Array<string>);
+  const [selectedProjectIds, setSelectedProjectIds] = useState<Array<string>>([]);
🧹 Nitpick comments (5)
frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx (2)

3-3: Use shared CSV saver and drop direct file-saver import

Route CSV saving through csv_helpers for consistency and less duplication.

-import saveAs from "file-saver";
...
-import { transformToCSVRow } from "viewer/model/helpers/csv_helpers";
+import { transformToCSVRow, saveAsCSV } from "viewer/model/helpers/csv_helpers";

Also applies to: 16-16


62-99: Refactor exportStatisticsToCSV to use saveAsCSV

Simplifies logic and aligns with other exports.

-  const segmentStatisticsAsString = segmentInformation
-    .map((row) => {
-      const maybeAdditionalCoords = hasAdditionalCoords ? [row.additionalCoordinates] : [];
-      return transformToCSVRow([
-        ...maybeAdditionalCoords,
-        row.segmentId,
-        row.segmentName,
-        row.groupId,
-        row.groupName,
-        row.volumeInVoxel,
-        row.volumeInUnit3,
-        ...row.boundingBoxTopLeft,
-        ...row.boundingBoxPosition,
-      ]);
-    })
-    .join("\n");
+  const csvLines = segmentInformation.map((row) => {
+    const maybeAdditionalCoords = hasAdditionalCoords ? [row.additionalCoordinates] : [];
+    return transformToCSVRow([
+      ...maybeAdditionalCoords,
+      row.segmentId,
+      row.segmentName,
+      row.groupId,
+      row.groupName,
+      row.volumeInVoxel,
+      row.volumeInUnit3,
+      ...row.boundingBoxTopLeft,
+      ...row.boundingBoxPosition,
+    ]);
+  });
...
-  const csv = [csv_header, segmentStatisticsAsString].join("\n");
   const filename =
     groupIdToExport === -1
       ? `segmentStatistics_${tracingIdOrDatasetName}.csv`
       : `segmentStatistics_${tracingIdOrDatasetName}_group-${groupIdToExport}.csv`;
-  const blob = new Blob([csv], {
-    type: "text/plain;charset=utf-8",
-  });
-  saveAs(blob, filename);
+  saveAsCSV([csv_header], csvLines, filename);
frontend/javascripts/admin/statistic/time_tracking_overview.tsx (2)

89-114: Reuse saveAsCSV for spans export

Removes manual Blob handling and keeps exports uniform.

-    const timeEntriesAsString = timeSpans
-      .map((row) => {
-        return transformToCSVRow([
-          row.userId,
-          row.userEmail,
-          row.datasetOrganization,
-          row.datasetName,
-          row.annotationId,
-          row.annotationState,
-          row.timeSpanCreated,
-          Math.ceil(row.timeSpanTimeMillis / 1000),
-          row.taskId,
-          row.projectName,
-          row.taskTypeId,
-          row.taskTypeSummary,
-        ]);
-      })
-      .join("\n"); // rows starting on new lines
-    const csv = [TIMETRACKING_CSV_HEADER_SPANS, timeEntriesAsString].join("\n");
-    const filename = `timetracking-user-export-${userId}.csv`;
-    const blob = new Blob([csv], {
-      type: "text/plain;charset=utf-8",
-    });
-    saveAs(blob, filename);
+    const csvLines = timeSpans.map((row) =>
+      transformToCSVRow([
+        row.userId,
+        row.userEmail,
+        row.datasetOrganization,
+        row.datasetName,
+        row.annotationId,
+        row.annotationState,
+        row.timeSpanCreated,
+        Math.ceil(row.timeSpanTimeMillis / 1000),
+        row.taskId,
+        row.projectName,
+        row.taskTypeId,
+        row.taskTypeSummary,
+      ]),
+    );
+    const filename = `timetracking-user-export-${userId}.csv`;
+    saveAsCSV(TIMETRACKING_CSV_HEADER_SPANS, csvLines, filename);

119-128: CSV per-user export looks good; minor rounding consistency

Consider using the same rounding mode as spans (ceil vs round) for consistency across exports.

frontend/javascripts/viewer/model/helpers/csv_helpers.ts (1)

23-24: Remove redundant null check on state

State is non-null by type.

-      const position = (
-        applyTransform && state != null ? getNodePosition(node, state) : node.untransformedPosition
+      const position = (
+        applyTransform ? getNodePosition(node, state) : node.untransformedPosition
       ).map(Math.floor);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d29c6d5 and 31702b5.

📒 Files selected for processing (5)
  • frontend/javascripts/admin/statistic/time_tracking_overview.tsx (2 hunks)
  • frontend/javascripts/libs/utils.ts (0 hunks)
  • frontend/javascripts/viewer/model/helpers/csv_helpers.ts (1 hunks)
  • frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx (2 hunks)
  • frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (5 hunks)
💤 Files with no reviewable changes (1)
  • frontend/javascripts/libs/utils.ts
🧰 Additional context used
🧬 Code graph analysis (2)
frontend/javascripts/viewer/model/helpers/csv_helpers.ts (3)
frontend/javascripts/viewer/store.ts (2)
  • WebknossosState (571-591)
  • SkeletonTracing (162-172)
frontend/javascripts/viewer/model/accessors/skeletontracing_accessor.ts (1)
  • getNodePosition (253-255)
frontend/javascripts/viewer/model/accessors/flycam_accessor.ts (1)
  • getAdditionalCoordinatesAsString (288-298)
frontend/javascripts/admin/statistic/time_tracking_overview.tsx (1)
frontend/javascripts/viewer/model/helpers/csv_helpers.ts (2)
  • transformToCSVRow (68-74)
  • saveAsCSV (76-82)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build-smoketest-push
  • GitHub Check: frontend-tests
  • GitHub Check: backend-tests
🔇 Additional comments (7)
frontend/javascripts/admin/statistic/time_tracking_overview.tsx (1)

18-18: LGTM on moving CSV utilities to csv_helpers

Good consolidation.

frontend/javascripts/viewer/model/helpers/csv_helpers.ts (2)

76-82: LGTM on saveAsCSV

Simple, reusable, and consistent with callers.


37-45: All referenced node fields exist on Node type
The Node interface in frontend/javascripts/viewer/model/types/tree_types.ts defines untransformedPosition, additionalCoordinates, rotation, radius, viewport, mag, bitDepth, interpolation, and timestamp, matching the CSV helper’s usage.

frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (4)

65-67: LGTM on CSV helper imports with aliases

Clear intent at call sites.


783-795: LGTM: Clear NML menu labels

Good UX clarity.


796-809: LGTM: Added CSV export actions (with transformed variant)

Matches new helpers and UX pattern.


1041-1041: LGTM: Wire annotationId into props

Needed for edges CSV.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx (1)

31-33: Fix CSV header typo: “segmendId” → “segmentId”

This is user-facing and will leak into exported files.

Apply this diff:

-const getSegmentStatisticsCSVHeader = (dataSourceUnit: string) =>
-  `segmendId,segmentName,groupId,groupName,volumeInVoxel,volumeIn${dataSourceUnit}3,boundingBoxTopLeftPositionX,boundingBoxTopLeftPositionY,boundingBoxTopLeftPositionZ,boundingBoxSizeX,boundingBoxSizeY,boundingBoxSizeZ`;
+const getSegmentStatisticsCSVHeader = (dataSourceUnit: string) =>
+  `segmentId,segmentName,groupId,groupName,volumeInVoxel,volumeIn${dataSourceUnit}3,boundingBoxTopLeftPositionX,boundingBoxTopLeftPositionY,boundingBoxTopLeftPositionZ,boundingBoxSizeX,boundingBoxSizeY,boundingBoxSizeZ`;
frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (1)

884-888: Fix spinner title for CSV exports

When exporting CSV, the modal still says “Preparing NML”. Use a generic title.

Apply this diff:

-    if (this.state.isDownloading) {
-      title = "Preparing NML";
+    if (this.state.isDownloading) {
+      title = "Preparing Download";
♻️ Duplicate comments (1)
frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (1)

562-591: Robust CSV export with proper error handling and spinner reset

The try/catch/finally addresses prior feedback; errors are surfaced via toast and the spinner always resets.

Two small improvements:

  • Name the ZIP with context (helps users distinguish downloads).
  • Optional: guard annotationId nullability if it can be undefined in some flows.

Apply this diff for the filename:

       await writer.close();
-      saveAs(await blobWriter.getData(), "tree_export.zip");
+      const fileName = `visible_trees_${annotationId ?? "export"}.zip`;
+      saveAs(await blobWriter.getData(), fileName);

Is annotationId guaranteed to be set when exporting? If not, confirm exportEdgesAsCSV handles a missing ID gracefully.

🧹 Nitpick comments (2)
frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx (2)

68-81: Normalize nullable fields to empty cells in CSV

transformToCSVRow stringifies values; null/undefined become the literal strings “null”/“undefined”. Prefer empty cells. Minimal fix here for groupId.

Apply this diff:

       row.segmentId,
       row.segmentName,
-      row.groupId,
+      row.groupId ?? "",
       row.groupName,

Option (global): make transformToCSVRow map null/undefined to "" once, so all callers benefit.


83-91: Pass header as columns array to saveAsCSV (align with its API)

Currently a pre-joined header string is wrapped in an array. Passing columns keeps semantics consistent and avoids double-joining.

Apply this diff:

-  const csv_header = hasAdditionalCoords
-    ? [ADDITIONAL_COORDS_COLUMN, getSegmentStatisticsCSVHeader(voxelSize.unit)].join(",")
-    : getSegmentStatisticsCSVHeader(voxelSize.unit);
+  const baseHeader = getSegmentStatisticsCSVHeader(voxelSize.unit).split(",");
+  const csvHeader = hasAdditionalCoords
+    ? [ADDITIONAL_COORDS_COLUMN, ...baseHeader]
+    : baseHeader;
   const filename =
     groupIdToExport === -1
       ? `segmentStatistics_${tracingIdOrDatasetName}.csv`
       : `segmentStatistics_${tracingIdOrDatasetName}_group-${groupIdToExport}.csv`;
-  saveAsCSV([csv_header], segmentStatisticsAsRows, filename);
+  saveAsCSV(csvHeader, segmentStatisticsAsRows, filename);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 396a23f and 017bba8.

📒 Files selected for processing (3)
  • frontend/javascripts/admin/statistic/time_tracking_overview.tsx (2 hunks)
  • frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx (3 hunks)
  • frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/javascripts/admin/statistic/time_tracking_overview.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx (1)
frontend/javascripts/viewer/model/helpers/csv_helpers.ts (2)
  • transformToCSVRow (68-74)
  • saveAsCSV (76-82)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build-smoketest-push
  • GitHub Check: backend-tests
  • GitHub Check: frontend-tests
🔇 Additional comments (7)
frontend/javascripts/viewer/view/right-border-tabs/segments_tab/segment_statistics_modal.tsx (2)

6-6: LGTM: import source updated correctly

pluralize remains in libs/utils while CSV helpers are pulled from the new module. Looks good.


15-15: Good move to central CSV helpers

Using saveAsCSV and transformToCSVRow from a single place reduces duplication.

frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (5)

17-17: LGTM: use Zip wrapper for writing

Using libs/zipjs_wrapper for writing while keeping @zip.js/zip.js for reading ZIPs is fine.


64-67: LGTM: CSV helper aliases

Clear aliases (getTreesAsCSV/getTreeEdgesAsCSV) improve readability.


784-800: Menu copy reads well and is consistent

Renaming to include “NML” removes ambiguity; transformed variants are properly tooltiped and gated.


801-815: CSV menu entries: good UX parity with NML

CSV options mirror NML (incl. transformed gating). Nicely done.


1047-1047: LGTM: annotationId plumbed to props

Required by edges CSV export; mapping looks correct.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (1)

566-587: Great: robust try/catch/finally with user feedback

Spinner is reset on error and an error toast is shown. This addresses the earlier review note.

🧹 Nitpick comments (5)
frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (5)

559-588: Spinner title says “Preparing NML” during CSV export

While exporting CSV, the modal still shows “Preparing NML”. Make the title generic (“Preparing Download”) or set it conditionally.

Outside this block, adjust render():

// Lines ~881-885
if (this.state.isDownloading) {
  title = "Preparing Download";
} else if (this.state.isUploading) {
  title = "Importing NML";
}

559-588: Name ZIP after the annotation for consistency

Use the same base name as NML export to help users correlate files.

Apply within this block:

   try {
-      const treesCsv = getTreeNodesAsCSV(Store.getState(), skeletonTracing, applyTransforms);
-      const edgesCsv = getTreeEdgesAsCSV(annotationId, skeletonTracing);
+      const state = Store.getState();
+      const treesCsv = getTreeNodesAsCSV(state, skeletonTracing, applyTransforms);
+      const edgesCsv = getTreeEdgesAsCSV(annotationId, skeletonTracing);
@@
-      await writer.close();
-      saveAs(await blobWriter.getData(), "tree_export.zip");
+      await writer.close();
+      const zipName = getNmlName(state).replace(/\.nml$/i, "_csv.zip");
+      saveAs(await blobWriter.getData(), zipName);

559-588: Toast copy: be explicit that CSV export failed

Minor wording improvement for clarity.

-    } catch (e) {
-      Toast.error("Could not export trees. See the console for details.");
+    } catch (e) {
+      Toast.error("Failed to export CSV (nodes+edges). See console for details.");
       console.error(e);

559-588: Enable ZIP compression for large CSVs (if wrapper supports it)

CSV compresses very well; adding deflate can significantly reduce download size.

If zipjs_wrapper forwards options, consider:

// either on writer construction
const writer = new Zip.ZipWriter(blobWriter /*, { level: 9 }*/);

// or per-entry
await writer.add("nodes.csv", new Zip.TextReader(treesCsv) /*, { level: 9 }*/);
await writer.add("edges.csv", new Zip.TextReader(edgesCsv) /*, { level: 9 }*/);

Please confirm the wrapper’s API before applying.


17-17: Use single zip.js import from libs/zipjs_wrapper
Import BlobReader, BlobWriter, ZipReader, and Entry from "libs/zipjs_wrapper" in frontend/javascripts/viewer/view/right-border-tabs/trees_tab_view.tsx instead of mixing direct "@zip.js/zip.js" imports—this ensures the CSP-safe workerScripts configuration is applied project-wide and prevents duplicate code paths.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 017bba8 and bc6eb70.

📒 Files selected for processing (2)
  • frontend/javascripts/viewer/model/helpers/csv_helpers.ts (1 hunks)
  • frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/javascripts/viewer/model/helpers/csv_helpers.ts
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (1)
frontend/javascripts/viewer/model/helpers/csv_helpers.ts (2)
  • getTreeNodesAsCSV (6-52)
  • getTreeEdgesAsCSV (54-67)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: frontend-tests
  • GitHub Check: backend-tests
  • GitHub Check: build-smoketest-push
🔇 Additional comments (3)
frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (3)

64-64: CSV helpers import looks good

Types and call sites below match the helper signatures.


785-797: Menu entries/readability: labels and gating look good

  • Clear NML vs. CSV labels.
  • Transformed variants correctly gated by isSkeletonLayerTransformed.

Also applies to: 798-804, 805-813


1043-1043: Prop wiring for annotationId looks correct

Matches CSV helpers’ expectation for edges export context.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
docs/skeleton_annotation/import_export.md (5)

17-24: Clarify export contents, add missing “comments” column, and align terminology

  • Add explicit mention that node comments are included (per PR TODO “export comments (done)”).
  • Avoid implying that “rotation (x,y,z)” is a per-node attribute; if rotation refers to viewport/camera, move it under viewport. Please confirm.
  • Tidy wording (bit depth vs. bitdepth), and make ND examples concrete (e.g., t, c).
  • Make it clear that CSV export downloads a ZIP (nodes.csv + edges.csv).

Apply:

-The NML or CSV files will contain the following information:
-- All nodes with their corresponding annotation and tree
-- The node's position and rotation (x,y,z)
-    - For ND datasets: additional coordinates
-- The magnification, interpolation, radius, bitdepth and viewport information of the node
-- The time the node was added
-- All edges with their corresponding tree, source and target node
+Exports (NML or CSV) contain:
+- All nodes with their corresponding annotation and tree.
+- Node position (x, y, z). For nD datasets: additional coordinates (e.g., t, c).
+- Node properties: radius, interpolation, magnification, and bit depth.
+- Viewport information (e.g., center and rotation), if available.
+- Timestamp when the node was added.
+- Node comments.
+- All edges with their corresponding tree, source node, and target node.

20-20: Fix markdown list indentation (MD007)

Indent sub-items by 2 spaces, not 4, to satisfy markdownlint.

Minimal fix (if you keep a sub-bullet here):

-    - For ND datasets: additional coordinates
+  - For nD datasets: additional coordinates

1-1: Update title to reflect CSV support

The page title still suggests NML only.

-# Importing & Exporting Skeletons as NML Files
+# Importing & Exporting Skeletons (NML/CSV)

12-16: Document the new CSV download flow (Skeleton tab → More → Download visible trees as CSV)

Add a dedicated step for CSV export and mention the ZIP structure.

 2. If you need more fine-grained control over which trees to download, use the `Download Selected Trees` option. From the `Skeleton` Tab, click on `More` and select `Download Selected Trees` from the menu. All visible trees (checkmark in front of the name) will be downloaded as an NML file. This is especially useful if you need to only download a single tree of an otherwise much larger annotation.
   ![Skeletons can be exported and downloaded as NML files from the annotation view. Either download all or only selected trees.](../images/tracing_ui_download.jpeg)
 
-![Skeletons can be exported and downloaded as NML files from the annotation view. Either download all or only selected trees.](../images/tracing_ui_download.jpeg)
+3. To export visible trees as CSV, open the `Skeleton` tab, click `More` → `Download visible trees as CSV`. The download is a ZIP containing `nodes.csv` and `edges.csv`.

Please confirm the exact menu label (“Download visible trees as CSV”) and placement.


15-15: Remove duplicated image

The image at Line 15 is identical to Line 13.

-![Skeletons can be exported and downloaded as NML files from the annotation view. Either download all or only selected trees.](../images/tracing_ui_download.jpeg)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bc6eb70 and ce02f1e.

📒 Files selected for processing (3)
  • docs/data/export_ui.md (1 hunks)
  • docs/skeleton_annotation/import_export.md (1 hunks)
  • frontend/javascripts/viewer/model/helpers/csv_helpers.ts (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • docs/data/export_ui.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/javascripts/viewer/model/helpers/csv_helpers.ts
🧰 Additional context used
🪛 markdownlint-cli2 (0.17.2)
docs/skeleton_annotation/import_export.md

20-20: Unordered list indentation
Expected: 2; Actual: 4

(MD007, ul-indent)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build-smoketest-push
  • GitHub Check: backend-tests
  • GitHub Check: frontend-tests

Copy link
Contributor

@MichaelBuessemeyer MichaelBuessemeyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very nice, everything worked perfectly fine.

I only found three potential improvements. Afterwards, this should be ready to go.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (1)

539-559: Wrap NML export in try/catch/finally to avoid stuck spinner

If serializeToNml or saveAs throws, isDownloadingNML remains true and the modal never closes. Align with the CSV flow.

   this.setState({
-      isDownloadingNML: true,
+    isDownloadingNML: true,
   });
-  // Wait 1 second for the Modal to render
-  const [buildInfo] = await Promise.all([getBuildInfo(), Utils.sleep(1000)]);
-  const state = Store.getState();
-  const nml = serializeToNml(
-    state,
-    state.annotation,
-    skeletonTracing,
-    buildInfo,
-    applyTransforms,
-  );
-  this.setState({
-    isDownloadingNML: false,
-  });
-  const blob = new Blob([nml], {
-    type: "text/plain;charset=utf-8",
-  });
-  saveAs(blob, getNmlName(state));
+  try {
+    // Wait 1 second for the Modal to render
+    const [buildInfo] = await Promise.all([getBuildInfo(), Utils.sleep(1000)]);
+    const state = Store.getState();
+    const nml = serializeToNml(
+      state,
+      state.annotation,
+      skeletonTracing,
+      buildInfo,
+      applyTransforms,
+    );
+    const blob = new Blob([nml], { type: "text/plain;charset=utf-8" });
+    saveAs(blob, getNmlName(state));
+  } catch (e) {
+    Toast.error("Could not export NML. See the console for details.");
+    // @ts-ignore
+    console.error(e);
+  } finally {
+    this.setState({ isDownloadingNML: false });
+  }
♻️ Duplicate comments (1)
frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (1)

883-887: Addressed past feedback: shows “Preparing CSV”

This resolves the earlier review about the spinner text.

🧹 Nitpick comments (3)
frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (3)

561-571: Optional: short-circuit when no visible trees

Avoid producing header-only CSVs by checking visibility up-front.

   if (!skeletonTracing) {
     return;
   }

   this.setState({
     isDownloadingCSV: true,
   });
 
   try {
+    const hasVisible = skeletonTracing
+      .trees
+      .values()
+      .filter((t) => t.isVisible)
+      .toArray().length > 0;
+    if (!hasVisible) {
+      Toast.info("No visible trees to export.");
+      return;
+    }

787-789: Nit: keep label order consistent

NML entries use “Download Visible Trees NML”, while CSV transformed entry below uses “Download Visible Trees (Transformed) CSV”. Consider “Download Visible Trees CSV” and “Download Visible Trees CSV (Transformed)” for symmetry.


800-815: CSV actions look good; consider disabling when nothing is exportable

Great addition. Optionally set disabled when there are no visible trees to preempt a no-op.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 243939c and 1765086.

📒 Files selected for processing (2)
  • frontend/javascripts/viewer/model/helpers/csv_helpers.ts (1 hunks)
  • frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (11 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/javascripts/viewer/model/helpers/csv_helpers.ts
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (2)
frontend/javascripts/viewer/model/helpers/nml_helpers.ts (1)
  • getNmlName (116-129)
frontend/javascripts/viewer/model/helpers/csv_helpers.ts (2)
  • getTreeNodesAsCSV (6-51)
  • getTreeEdgesAsCSV (53-66)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: backend-tests
  • GitHub Check: build-smoketest-push
  • GitHub Check: frontend-tests
🔇 Additional comments (8)
frontend/javascripts/viewer/view/right-border-tabs/trees_tab/skeleton_tab_view.tsx (8)

17-17: Verify mixed ZipJS imports are intentional

You import read APIs from @zip.js/zip.js and write APIs from libs/zipjs_wrapper. If this is to work around bundling/typing quirks, add a short comment to prevent “cleanup refactors” later. Otherwise, consider consolidating to one source to avoid duplicate code paths.


64-65: Good: centralizing CSV logic via helpers

Using getTreeNodesAsCSV/getTreeEdgesAsCSV keeps the UI lean and reuses a single CSV contract.


106-108: State flags look fine

Separate flags for NML/CSV keep the modal title accurate.


323-325: Constructor defaults are correct

Both download flags initialized to false.


795-799: LGTM: transformed NML entry

Label and tooltip read clearly.


897-897: LGTM: unified downloading gate

The modal now opens for either NML or CSV.


906-906: LGTM: modal open condition

Tied to download/upload flags as intended.


1048-1049: Prop addition is correct

annotationId wired into props for edges CSV; matches helper signature.

@MichaelBuessemeyer
Copy link
Contributor

Awesome, thanks for applying the changes. Retesting went well 👍

@MichaelBuessemeyer MichaelBuessemeyer merged commit f00f61e into master Sep 22, 2025
6 of 8 checks passed
@MichaelBuessemeyer MichaelBuessemeyer deleted the visible-tree-csv-export branch September 22, 2025 08:09
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.

Export tree nodes as CSV

2 participants