vlbthambawita commited on
Commit
c24b60e
Β·
verified Β·
1 Parent(s): dfa99dc

Deploy sha:591a9f67fc1c

Browse files
Files changed (2) hide show
  1. _version.json +2 -2
  2. analysis/mimic_iv_ecg/report.html +202 -140
_version.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "tag": "",
3
- "sha": "e92eb09bd0db",
4
- "date": "2026-03-31T08:57:50Z",
5
  "deploy": "space"
6
  }
 
1
  {
2
  "tag": "",
3
+ "sha": "591a9f67fc1c",
4
+ "date": "2026-03-31T09:21:23Z",
5
  "deploy": "space"
6
  }
analysis/mimic_iv_ecg/report.html CHANGED
@@ -129,9 +129,10 @@
129
  </div>
130
  <div class="legend" style="margin-top:14px;">
131
  <div class="legend-item"><span class="legend-swatch" style="background:#1a2233;border-radius:50%;"></span> Root / Directory</div>
132
- <div class="legend-item"><span class="legend-swatch" style="background:#4f8ef7;"></span> CSV Metadata File</div>
133
  <div class="legend-item"><span class="legend-swatch" style="background:#10b981;"></span> WFDB Waveform File (.hea / .dat)</div>
134
  <div class="legend-item"><span class="legend-swatch" style="background:#94a3b8;"></span> Other File</div>
 
135
  </div>
136
  </div>
137
  </section>
@@ -5788,17 +5789,82 @@ sections.forEach(s => observer.observe(s));
5788
 
5789
  // ── Dataset Structure Tree ────────────────────────────────────────────────────
5790
  (function() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5791
  const treeData = {
5792
  name: "mimic-iv-ecg-v1.0/", type: "root", desc: "Root dataset directory (800,035 studies)",
5793
  children: [
5794
- { name: "record_list.csv", type: "csv", desc: "800,035 rows β€” all ECG record paths + subject/study IDs + ecg_time" },
5795
- { name: "machine_measurements.csv", type: "csv", desc: "789,481 rows Γ— 33 cols β€” automated interval measurements + text report phrases" },
5796
- { name: "machine_measurements_original.csv", type: "csv", desc: "789,481 rows β€” pre-processed raw measurement values" },
5797
- { name: "machine_measurements_data_dictionary.csv", type: "csv", desc: "18 rows β€” variable descriptions and units" },
5798
- { name: "waveform_note_links.csv", type: "csv", desc: "609,272 rows β€” links ECG studies to MIMIC-IV clinical notes" },
5799
- { name: "RECORDS", type: "file", desc: "1000 patient-group directory names (p1000–p1999)" },
5800
- { name: "SHA256SUMS.txt", type: "file", desc: "File integrity checksums (SHA-256)" },
5801
- { name: "LICENSE.txt", type: "file", desc: "ODC Open Database License (ODbL)" },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5802
  {
5803
  name: "files/", type: "dir", desc: "Waveform data organised in a 4-level hierarchy",
5804
  _collapsed: false,
@@ -5806,207 +5872,203 @@ sections.forEach(s => observer.observe(s));
5806
  name: "pXXXX/", type: "dir", desc: "1,000 patient-group folders (p1000–p1999)",
5807
  _collapsed: false,
5808
  children: [{
5809
- name: "pNNNNNNNN/", type: "dir", desc: "Individual patient directory (subject_id β€” 160,862 unique patients)",
 
5810
  _collapsed: false,
5811
  children: [{
5812
- name: "sNNNNNNNN/", type: "dir", desc: "Study directory (study_id β€” one per ECG acquisition)",
 
5813
  _collapsed: false,
5814
  children: [
5815
  { name: "NNNNNNNN.hea", type: "hea", desc: "WFDB header β€” 12 leads, 500 Hz, 10 s (plain text, ~600 B)" },
5816
- { name: "NNNNNNNN.dat", type: "dat", desc: "WFDB signal β€” 16-bit signed binary, 5,000 samples/lead (~120 KB)" }
5817
- ]
5818
- }]
5819
- }]
5820
- }]
5821
- }
5822
- ]
5823
  };
5824
 
5825
- const nodeColor = n => {
5826
- if (n.data.type === "root" || n.data.type === "dir") return "#1a2233";
5827
- if (n.data.type === "csv") return "#4f8ef7";
5828
- if (n.data.type === "hea" || n.data.type === "dat") return "#10b981";
 
 
 
 
 
 
5829
  return "#94a3b8";
5830
  };
5831
- const nodeShape = n => (n.data.type === "root" || n.data.type === "dir") ? "diamond" : "rect";
5832
 
5833
- const margin = {top: 20, right: 320, bottom: 20, left: 160};
5834
- const baseWidth = 1100;
5835
- let svgHeight = 600;
 
 
 
 
5836
 
5837
  const svg = d3.select("#ds-tree-svg")
5838
  .attr("width", baseWidth + margin.left + margin.right)
5839
- .attr("height", svgHeight + margin.top + margin.bottom)
5840
  .style("font", "13px 'Segoe UI', system-ui, sans-serif");
5841
 
5842
  const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
5843
-
5844
- // link path generator
5845
  const linkPath = d3.linkHorizontal().x(d => d.y).y(d => d.x);
5846
 
5847
- // Build hierarchy with collapse state
5848
  function buildHierarchy(data) {
5849
- const root = d3.hierarchy(data, d => d._collapsed ? null : d.children);
5850
- root.x0 = 0; root.y0 = 0;
5851
- return root;
5852
  }
5853
 
5854
- // Collapse/expand node
5855
- function toggle(d) {
5856
- if (!d.data.children) return;
5857
- if (d.children) {
5858
- d.data._collapsed = true;
5859
- } else {
5860
- d.data._collapsed = false;
5861
- }
5862
- }
5863
-
5864
- function setAllCollapsed(node, collapsed) {
5865
- if (node.children) node.data.children = node.children.map(c => c.data);
5866
- if (collapsed) node.data._collapsed = true;
5867
- else node.data._collapsed = false;
5868
- (node.children || node._children || []).forEach(c => setAllCollapsed(c, collapsed));
5869
- }
5870
-
5871
- let root = buildHierarchy(treeData);
5872
-
5873
  function update() {
5874
- root = buildHierarchy(treeData);
5875
 
5876
- const treeLayout = d3.tree().nodeSize([34, 220]);
 
5877
  treeLayout(root);
5878
 
5879
- // centre vertically
5880
  const nodes = root.descendants();
5881
- const minX = d3.min(nodes, d => d.x);
5882
- const maxX = d3.max(nodes, d => d.x);
5883
- svgHeight = Math.max(400, maxX - minX + margin.top + margin.bottom + 40);
5884
  svg.attr("height", svgHeight);
5885
  const offsetY = margin.top - minX;
5886
 
5887
  g.selectAll("*").remove();
5888
 
5889
- // Links
5890
- g.selectAll(".link")
5891
  .data(root.links())
5892
  .join("path")
5893
- .attr("class","link")
5894
- .attr("fill","none")
5895
- .attr("stroke","#c8d0e0")
5896
- .attr("stroke-width",1.5)
5897
  .attr("d", d => linkPath({
5898
- source: {x: d.source.x + offsetY, y: d.source.y},
5899
- target: {x: d.target.x + offsetY, y: d.target.y}
5900
  }));
5901
 
5902
- // Nodes
5903
- const node = g.selectAll(".node")
5904
  .data(nodes)
5905
  .join("g")
5906
- .attr("class","node")
5907
  .attr("transform", d => `translate(${d.y},${d.x + offsetY})`)
5908
  .style("cursor", d => d.data.children ? "pointer" : "default")
5909
  .on("click", (event, d) => {
5910
  if (!d.data.children) return;
5911
- toggle(d);
5912
  update();
5913
  })
5914
  .on("mouseenter", (event, d) => {
5915
- const icon = d.data.type === "dir" || d.data.type === "root" ? "πŸ“" :
5916
- d.data.type === "csv" ? "πŸ“„" :
5917
- d.data.type === "hea" ? "πŸ“‹" :
5918
- d.data.type === "dat" ? "πŸ’Ύ" : "πŸ“Ž";
5919
- showTip(`<b>${icon} ${d.data.name}</b><br>${d.data.desc}`, event);
5920
  })
5921
- .on("mousemove", (event) => {
5922
- tip.style("left",(event.pageX+14)+"px").style("top",(event.pageY-28)+"px");
5923
- })
5924
- .on("mouseleave", () => hideTip());
5925
 
5926
- // Shape: diamond for dirs, rect for files
5927
  node.each(function(d) {
5928
- const sel = d3.select(this);
5929
- const color = nodeColor(d);
5930
- if (d.data.type === "root" || d.data.type === "dir") {
5931
- sel.append("polygon")
5932
- .attr("points", "-9,0 0,-9 9,0 0,9")
5933
- .attr("fill", color)
5934
- .attr("stroke", "#fff")
5935
- .attr("stroke-width", 1.5);
5936
  } else {
5937
- sel.append("rect")
5938
- .attr("x", -7).attr("y", -7)
5939
- .attr("width", 14).attr("height", 14)
5940
- .attr("rx", 3)
5941
- .attr("fill", color)
5942
- .attr("stroke", "#fff")
5943
- .attr("stroke-width", 1.5);
5944
  }
5945
  });
5946
 
5947
- // Collapse indicator (+ / -)
5948
- node.filter(d => d.data.children)
5949
  .append("text")
5950
- .attr("dy", "0.35em")
5951
- .attr("text-anchor", "middle")
5952
- .attr("fill", "#fff")
5953
- .attr("font-size", "11px")
5954
- .attr("font-weight", "700")
5955
- .attr("pointer-events", "none")
5956
  .text(d => d.children ? "βˆ’" : "+");
5957
 
5958
- // Labels
5959
- node.append("text")
5960
- .attr("dy", "0.35em")
5961
- .attr("x", d => (d.data.type === "root" || d.data.type === "dir") ? 14 : 12)
5962
- .attr("text-anchor", "start")
5963
- .attr("fill", d => (d.data.type === "root" || d.data.type === "dir") ? "#1a2233" : "#334")
5964
- .attr("font-weight", d => (d.data.type === "root" || d.data.type === "dir") ? "700" : "400")
5965
- .attr("font-size", "12.5px")
5966
- .text(d => d.data.name)
5967
- .clone(true).lower()
5968
- .attr("stroke","#f4f6f9")
5969
- .attr("stroke-width",3)
5970
- .attr("stroke-linejoin","round");
5971
 
5972
- // Description labels (grey, smaller)
5973
- node.append("text")
5974
- .attr("dy", "0.35em")
5975
- .attr("x", d => (d.data.type === "root" || d.data.type === "dir") ? 14 : 12)
5976
- .attr("dx", d => {
5977
- // approximate label width (7px/char)
5978
- const nameLen = (d.data.name || "").length;
5979
- return nameLen * 7 + 6;
5980
- })
5981
- .attr("text-anchor", "start")
5982
- .attr("fill", "#888")
5983
- .attr("font-size", "11px")
5984
- .text(d => "β€” " + d.data.desc)
5985
- .clone(true).lower()
5986
- .attr("stroke","#f4f6f9")
5987
- .attr("stroke-width",3)
5988
- .attr("stroke-linejoin","round");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5989
 
5990
- // update container height
5991
  document.getElementById("ds-tree-container").style.minHeight = svgHeight + "px";
5992
  }
5993
 
5994
  update();
5995
 
 
5996
  document.getElementById("tree-expand-all").addEventListener("click", () => {
 
5997
  function expandAll(n) {
5998
- n.data._collapsed = false;
5999
- if (n.data.children) n.data.children.forEach(c => expandAll({data: c}));
 
6000
  }
6001
- expandAll({data: treeData});
6002
  update();
6003
  });
 
6004
  document.getElementById("tree-collapse-all").addEventListener("click", () => {
6005
  function collapseAll(n, depth) {
6006
- if (depth > 0) n.data._collapsed = true;
6007
- if (n.data.children) n.data.children.forEach(c => collapseAll({data: c}, depth + 1));
6008
  }
6009
- collapseAll({data: treeData}, 0);
6010
  update();
6011
  });
6012
  })();
 
129
  </div>
130
  <div class="legend" style="margin-top:14px;">
131
  <div class="legend-item"><span class="legend-swatch" style="background:#1a2233;border-radius:50%;"></span> Root / Directory</div>
132
+ <div class="legend-item"><span class="legend-swatch" style="background:#4f8ef7;"></span> CSV Metadata File <em style="font-size:0.72rem;color:#888">(click to expand columns β–Ύ)</em></div>
133
  <div class="legend-item"><span class="legend-swatch" style="background:#10b981;"></span> WFDB Waveform File (.hea / .dat)</div>
134
  <div class="legend-item"><span class="legend-swatch" style="background:#94a3b8;"></span> Other File</div>
135
+ <div class="legend-item"><span class="legend-swatch" style="background:#cbd5e1;border-radius:50%;width:10px;height:10px;"></span> Column name <em style="font-size:0.72rem;color:#888">(monospace, hover for type)</em></div>
136
  </div>
137
  </div>
138
  </section>
 
5789
 
5790
  // ── Dataset Structure Tree ────────────────────────────────────────────────────
5791
  (function() {
5792
+ // Column metadata: name β†’ description shown in tooltip
5793
+ const COL_META = {
5794
+ "subject_id": "Patient identifier (integer)",
5795
+ "study_id": "ECG study identifier β€” primary key (integer)",
5796
+ "file_name": "WFDB record filename without extension (string)",
5797
+ "ecg_time": "Acquisition datetime β€” date-shifted for de-identification",
5798
+ "path": "Relative path to WFDB record within files/",
5799
+ "cart_id": "ECG cart / device identifier (string)",
5800
+ "report_0 … report_17": "18 machine-generated diagnostic phrase fields (string, often blank)",
5801
+ "bandwidth": "ECG device bandwidth setting (string)",
5802
+ "filtering": "ECG device filter setting (string)",
5803
+ "rr_interval": "RR interval β€” ms (float)",
5804
+ "p_onset": "P-wave onset β€” ms (float)",
5805
+ "p_end": "P-wave end β€” ms (float)",
5806
+ "qrs_onset": "QRS complex onset β€” ms (float)",
5807
+ "qrs_end": "QRS complex end β€” ms (float)",
5808
+ "t_end": "T-wave end β€” ms (float)",
5809
+ "p_axis": "P-wave electrical axis β€” degrees (float)",
5810
+ "qrs_axis": "QRS electrical axis β€” degrees (float)",
5811
+ "t_axis": "T-wave electrical axis β€” degrees (float)",
5812
+ "waveform_path": "Path used to link waveform to clinical note",
5813
+ "note_id": "MIMIC-IV clinical note identifier (integer)",
5814
+ "note_seq": "Note sequence number within admission (integer)",
5815
+ "charttime": "Datetime of linked clinical note",
5816
+ "variable": "Column / variable name (string)",
5817
+ "label": "Short human-readable label (string)",
5818
+ "description": "Full description of the variable (string)",
5819
+ "unit": "Measurement unit (string)",
5820
+ };
5821
+ const col = name => ({ name, type: "column", desc: COL_META[name] || "" });
5822
+
5823
+ // Shared measurement columns (machine_measurements + original)
5824
+ const measCols = [
5825
+ "subject_id","study_id","cart_id","ecg_time",
5826
+ "report_0 … report_17",
5827
+ "bandwidth","filtering",
5828
+ "rr_interval","p_onset","p_end","qrs_onset","qrs_end","t_end",
5829
+ "p_axis","qrs_axis","t_axis",
5830
+ ].map(col);
5831
+
5832
  const treeData = {
5833
  name: "mimic-iv-ecg-v1.0/", type: "root", desc: "Root dataset directory (800,035 studies)",
5834
  children: [
5835
+ {
5836
+ name: "record_list.csv", type: "csv",
5837
+ desc: "800,035 rows β€” all ECG record paths + subject/study IDs + ecg_time",
5838
+ _collapsed: true,
5839
+ children: ["subject_id","study_id","file_name","ecg_time","path"].map(col),
5840
+ },
5841
+ {
5842
+ name: "machine_measurements.csv", type: "csv",
5843
+ desc: "789,481 rows Γ— 33 cols β€” automated interval measurements + text report phrases",
5844
+ _collapsed: true,
5845
+ children: measCols,
5846
+ },
5847
+ {
5848
+ name: "machine_measurements_original.csv", type: "csv",
5849
+ desc: "789,481 rows β€” pre-processed raw measurement values (same 33 columns)",
5850
+ _collapsed: true,
5851
+ children: measCols,
5852
+ },
5853
+ {
5854
+ name: "machine_measurements_data_dictionary.csv", type: "csv",
5855
+ desc: "18 rows β€” variable descriptions and units",
5856
+ _collapsed: true,
5857
+ children: ["variable","label","description","unit"].map(col),
5858
+ },
5859
+ {
5860
+ name: "waveform_note_links.csv", type: "csv",
5861
+ desc: "609,272 rows β€” links ECG studies to MIMIC-IV clinical notes",
5862
+ _collapsed: true,
5863
+ children: ["subject_id","study_id","waveform_path","note_id","note_seq","charttime"].map(col),
5864
+ },
5865
+ { name: "RECORDS", type: "file", desc: "1000 patient-group directory names (p1000–p1999)" },
5866
+ { name: "SHA256SUMS.txt", type: "file", desc: "File integrity checksums (SHA-256)" },
5867
+ { name: "LICENSE.txt", type: "file", desc: "ODC Open Database License (ODbL)" },
5868
  {
5869
  name: "files/", type: "dir", desc: "Waveform data organised in a 4-level hierarchy",
5870
  _collapsed: false,
 
5872
  name: "pXXXX/", type: "dir", desc: "1,000 patient-group folders (p1000–p1999)",
5873
  _collapsed: false,
5874
  children: [{
5875
+ name: "pNNNNNNNN/", type: "dir",
5876
+ desc: "Individual patient directory (subject_id β€” 160,862 unique patients)",
5877
  _collapsed: false,
5878
  children: [{
5879
+ name: "sNNNNNNNN/", type: "dir",
5880
+ desc: "Study directory (study_id β€” one per ECG acquisition)",
5881
  _collapsed: false,
5882
  children: [
5883
  { name: "NNNNNNNN.hea", type: "hea", desc: "WFDB header β€” 12 leads, 500 Hz, 10 s (plain text, ~600 B)" },
5884
+ { name: "NNNNNNNN.dat", type: "dat", desc: "WFDB signal β€” 16-bit signed binary, 5,000 samples/lead (~120 KB)" },
5885
+ ],
5886
+ }],
5887
+ }],
5888
+ }],
5889
+ },
5890
+ ],
5891
  };
5892
 
5893
+ // ── Helpers ──────────────────────────────────────────────────────────────────
5894
+ const isDir = d => d.data.type === "root" || d.data.type === "dir";
5895
+ const isCSV = d => d.data.type === "csv";
5896
+ const isCol = d => d.data.type === "column";
5897
+
5898
+ const nodeColor = d => {
5899
+ if (isDir(d)) return "#1a2233";
5900
+ if (isCSV(d)) return "#4f8ef7";
5901
+ if (d.data.type === "hea" || d.data.type === "dat") return "#10b981";
5902
+ if (isCol(d)) return "#94a3b8";
5903
  return "#94a3b8";
5904
  };
 
5905
 
5906
+ // Approximate pixel width of a label string
5907
+ const labelPx = (s, mono) => (s || "").length * (mono ? 7.2 : 7.5);
5908
+
5909
+ // ── SVG setup ────────────────────────────────────────────────────────────────
5910
+ const margin = { top: 24, right: 360, bottom: 24, left: 160 };
5911
+ const baseWidth = 1100;
5912
+ let svgHeight = 600;
5913
 
5914
  const svg = d3.select("#ds-tree-svg")
5915
  .attr("width", baseWidth + margin.left + margin.right)
5916
+ .attr("height", svgHeight)
5917
  .style("font", "13px 'Segoe UI', system-ui, sans-serif");
5918
 
5919
  const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);
 
 
5920
  const linkPath = d3.linkHorizontal().x(d => d.y).y(d => d.x);
5921
 
5922
+ // ── Hierarchy builder ──────────────────────────────────────────────────────���──
5923
  function buildHierarchy(data) {
5924
+ return d3.hierarchy(data, d => d._collapsed ? null : d.children);
 
 
5925
  }
5926
 
5927
+ // ── Render ────────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5928
  function update() {
5929
+ const root = buildHierarchy(treeData);
5930
 
5931
+ // Use tighter row spacing for column nodes; standard for others
5932
+ const treeLayout = d3.tree().nodeSize([26, 220]);
5933
  treeLayout(root);
5934
 
 
5935
  const nodes = root.descendants();
5936
+ const minX = d3.min(nodes, d => d.x);
5937
+ const maxX = d3.max(nodes, d => d.x);
5938
+ svgHeight = Math.max(420, maxX - minX + margin.top + margin.bottom + 40);
5939
  svg.attr("height", svgHeight);
5940
  const offsetY = margin.top - minX;
5941
 
5942
  g.selectAll("*").remove();
5943
 
5944
+ // ── Links ──────────────────────────────────────────────────────────────────
5945
+ g.selectAll(".lnk")
5946
  .data(root.links())
5947
  .join("path")
5948
+ .attr("fill", "none")
5949
+ .attr("stroke", d => isCol(d.target) ? "#dde3ef" : "#c8d0e0")
5950
+ .attr("stroke-width", d => isCol(d.target) ? 1 : 1.5)
5951
+ .attr("stroke-dasharray", d => isCol(d.target) ? "3,3" : null)
5952
  .attr("d", d => linkPath({
5953
+ source: { x: d.source.x + offsetY, y: d.source.y },
5954
+ target: { x: d.target.x + offsetY, y: d.target.y },
5955
  }));
5956
 
5957
+ // ── Node groups ────────────────────────────────────────────────────────────
5958
+ const node = g.selectAll(".nd")
5959
  .data(nodes)
5960
  .join("g")
 
5961
  .attr("transform", d => `translate(${d.y},${d.x + offsetY})`)
5962
  .style("cursor", d => d.data.children ? "pointer" : "default")
5963
  .on("click", (event, d) => {
5964
  if (!d.data.children) return;
5965
+ d.data._collapsed = !d.data._collapsed;
5966
  update();
5967
  })
5968
  .on("mouseenter", (event, d) => {
5969
+ const icons = { root:"πŸ“", dir:"πŸ“", csv:"πŸ“„", hea:"πŸ“‹", dat:"πŸ’Ύ", column:"⬑", file:"πŸ“Ž" };
5970
+ showTip(`<b>${icons[d.data.type]||"πŸ“Ž"} ${d.data.name}</b><br>${d.data.desc}`, event);
 
 
 
5971
  })
5972
+ .on("mousemove", event => tip.style("left",(event.pageX+14)+"px").style("top",(event.pageY-28)+"px"))
5973
+ .on("mouseleave", () => hideTip());
 
 
5974
 
5975
+ // ── Node shapes ────────────────────────────────────────────────────────────
5976
  node.each(function(d) {
5977
+ const s = d3.select(this), c = nodeColor(d);
5978
+ if (isCol(d)) {
5979
+ // Small filled circle for column nodes
5980
+ s.append("circle").attr("r", 4).attr("fill", c).attr("stroke","#fff").attr("stroke-width",1);
5981
+ } else if (isDir(d)) {
5982
+ s.append("polygon").attr("points","-9,0 0,-9 9,0 0,9")
5983
+ .attr("fill",c).attr("stroke","#fff").attr("stroke-width",1.5);
 
5984
  } else {
5985
+ s.append("rect").attr("x",-7).attr("y",-7).attr("width",14).attr("height",14)
5986
+ .attr("rx",3).attr("fill",c).attr("stroke","#fff").attr("stroke-width",1.5);
 
 
 
 
 
5987
  }
5988
  });
5989
 
5990
+ // ── Expand/collapse +/βˆ’ badge (non-column expandable nodes only) ───────────
5991
+ node.filter(d => d.data.children && !isCol(d))
5992
  .append("text")
5993
+ .attr("dy","0.35em").attr("text-anchor","middle")
5994
+ .attr("fill","#fff").attr("font-size","11px").attr("font-weight","700")
5995
+ .attr("pointer-events","none")
 
 
 
5996
  .text(d => d.children ? "βˆ’" : "+");
5997
 
5998
+ // ── Labels ─────────────────────────────────────────────────────────────────
5999
+ node.each(function(d) {
6000
+ const s = d3.select(this);
6001
+ const col_ = isCol(d);
6002
+ const xOff = col_ ? 8 : isDir(d) ? 14 : 12;
6003
+ const fSize = col_ ? "11px" : "12.5px";
6004
+ const fFam = col_ ? "'Courier New', Courier, monospace" : null;
6005
+ const fColor = col_ ? "#475569" : isDir(d) ? "#1a2233" : "#334";
6006
+ const fWeight= (!col_ && isDir(d)) ? "700" : "400";
6007
+ const name = d.data.name;
 
 
 
6008
 
6009
+ // Readability halo
6010
+ s.append("text")
6011
+ .attr("dy","0.35em").attr("x",xOff).attr("text-anchor","start")
6012
+ .attr("stroke","#f4f6f9").attr("stroke-width", col_ ? 2 : 3)
6013
+ .attr("stroke-linejoin","round").attr("fill","none")
6014
+ .attr("font-size",fSize).attr("font-family",fFam||null)
6015
+ .text(name);
6016
+
6017
+ // Name label
6018
+ s.append("text")
6019
+ .attr("dy","0.35em").attr("x",xOff).attr("text-anchor","start")
6020
+ .attr("fill",fColor).attr("font-weight",fWeight)
6021
+ .attr("font-size",fSize).attr("font-family",fFam||null)
6022
+ .text(name);
6023
+
6024
+ if (col_) return; // column nodes: name only, no extra labels
6025
+
6026
+ const nameW = labelPx(name, false);
6027
+
6028
+ // Description (grey, smaller)
6029
+ s.append("text")
6030
+ .attr("dy","0.35em").attr("x",xOff).attr("dx", nameW + 6)
6031
+ .attr("text-anchor","start").attr("fill","#888").attr("font-size","11px")
6032
+ .text("β€” " + d.data.desc)
6033
+ .clone(true).lower()
6034
+ .attr("stroke","#f4f6f9").attr("stroke-width",3)
6035
+ .attr("stroke-linejoin","round").attr("fill","none");
6036
+
6037
+ // "β–Ύ N cols" badge on collapsed CSV nodes
6038
+ if (isCSV(d) && d.data._collapsed) {
6039
+ const n = d.data.children.length;
6040
+ const descW = labelPx("β€” " + d.data.desc, false);
6041
+ s.append("text")
6042
+ .attr("dy","0.35em").attr("x",xOff).attr("dx", nameW + 6 + descW + 10)
6043
+ .attr("text-anchor","start")
6044
+ .attr("fill","#4f8ef7").attr("font-size","10.5px").attr("font-weight","600")
6045
+ .text(`[β–Ύ ${n} cols]`);
6046
+ }
6047
+ });
6048
 
 
6049
  document.getElementById("ds-tree-container").style.minHeight = svgHeight + "px";
6050
  }
6051
 
6052
  update();
6053
 
6054
+ // ── Toolbar buttons ───────────────────────────────────────────────────────────
6055
  document.getElementById("tree-expand-all").addEventListener("click", () => {
6056
+ // Expand dirs/files but NOT CSV column children (too noisy)
6057
  function expandAll(n) {
6058
+ if (n.type === "csv") return; // leave CSV column children collapsed
6059
+ n._collapsed = false;
6060
+ if (n.children) n.children.forEach(expandAll);
6061
  }
6062
+ expandAll(treeData);
6063
  update();
6064
  });
6065
+
6066
  document.getElementById("tree-collapse-all").addEventListener("click", () => {
6067
  function collapseAll(n, depth) {
6068
+ if (depth > 0) n._collapsed = true;
6069
+ if (n.children) n.children.forEach(c => collapseAll(c, depth + 1));
6070
  }
6071
+ collapseAll(treeData, 0);
6072
  update();
6073
  });
6074
  })();