From 3c4df420232ec027efde850f5ad31533fa47b9d1 Mon Sep 17 00:00:00 2001 From: Mnikley Date: Fri, 10 Jul 2026 09:47:47 +0200 Subject: [PATCH 1/2] feat(styling): add node and edge opacity control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A per-element Opacity slider (0-1) in the styling panel for nodes and edges, folding into the color's alpha so it composes with colors that already carry transparency. Threaded through every node program (SVG texture, border-circle, native circle/square) and the edge stroke. - Scale-by-property (∿) supported via the numeric scale picker - Round-trips through JSON and a new Excel `Opacity` column, documented in the template readme tab - Backward compatible: absent opacity defaults to 1 (fully opaque) - Release: bump to 1.15.5, changelog entry Tests: opacity->alpha across all node programs, edge-color composition, clamp/identity cases, and Excel + scale-picker round-trips. --- CHANGELOG.md | 8 + package.json | 2 +- src/config.js | 6 +- src/graph/graph_model.js | 13 +- src/graph/style.js | 2 + src/managers/io.js | 805 +------------------------- src/managers/ui_style_div.js | 22 + src/utilities/numeric_scale_picker.js | 8 + templates/simple-template.xlsx | Bin 17405 -> 16444 bytes tests/excel-style-roundtrip.test.js | 29 + tests/graph-model.test.js | 70 +++ tests/numeric-scale-picker.test.js | 34 ++ 12 files changed, 206 insertions(+), 793 deletions(-) create mode 100644 tests/numeric-scale-picker.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 35af9e3..0fa8d1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.15.5 β€” 2026-07-10 + +Saved graph files load unchanged; older files (and files saved by earlier versions) default the new opacity to fully opaque, so their appearance is identical. + +### Features + +* **Node and edge opacity.** The styling panel (under 🎨) now has an **Opacity** slider for both nodes and edges (1 = opaque, 0 = invisible). It folds into the element's color alpha, so it composes with a color that already carries transparency, and β€” like the other numeric style knobs β€” it can be **mapped by data** via the ∿ button to scale opacity from a numeric property. Opacity round-trips through both JSON and the Excel `Opacity` column (documented in the template's readme tab). Being an alpha effect, low opacity composites toward the background: light colors (e.g. the default slate edges) approach white sooner than saturated ones. + ## 1.15.4 β€” 2026-07-01 Saved graph files load unchanged; older files default the two new per-view settings to the previous behavior (OR, non-strict). diff --git a/package.json b/package.json index 9d3a8ca..c36c78a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graph-lens-lite", - "version": "1.15.4", + "version": "1.15.5", "main": "src/package/electron_app.js", "description": "Visualise and explore property graphs in a lightweight desktop app.", "homepage": "https://github.com/Delta4AI/GraphLensLite", diff --git a/src/config.js b/src/config.js index 21aae4e..606d70a 100644 --- a/src/config.js +++ b/src/config.js @@ -1,11 +1,11 @@ /** * Defaults for the graph, layouts and UI */ -const VERSION = "1.15.4"; +const VERSION = "1.15.5"; const DEFAULTS = { NODE: { - FILL_COLOR: "#C33D35", SIZE: 20, LINE_WIDTH: 1, TYPE: "hexagon", STROKE_COLOR: null, + FILL_COLOR: "#C33D35", SIZE: 20, LINE_WIDTH: 1, TYPE: "hexagon", STROKE_COLOR: null, OPACITY: 1, BADGE: { FONT_SIZE: 8, COLOR: "#C33D35", SCALE_WITH_NODE: false }, @@ -32,7 +32,7 @@ const DEFAULTS = { }, }, EDGE: { - COLOR: "#403C5390", LINE_WIDTH: 0.75, LINE_DASH: 0, TYPE: "line", + COLOR: "#403C5390", LINE_WIDTH: 0.75, LINE_DASH: 0, TYPE: "line", OPACITY: 1, ARROWS: { START: false, END: false, START_SIZE: 8, START_TYPE: "arrow", END_SIZE: 8, END_TYPE: "arrow", // null fill β†’ marker inherits the edge stroke color; null border β†’ no border (transparent). diff --git a/src/graph/graph_model.js b/src/graph/graph_model.js index 3a1c2f4..1463b8e 100644 --- a/src/graph/graph_model.js +++ b/src/graph/graph_model.js @@ -52,8 +52,8 @@ function nodeAttributesFromStyle(style = {}, type = undefined) { // G6 size is a diameter (or [w, h]); sigma size is a radius. attrs.size = (Array.isArray(style.size) ? style.size[0] : style.size) / 2; } - if (style.fill !== undefined) attrs.color = style.fill; - if (style.stroke !== undefined) attrs.borderColor = style.stroke; + if (style.fill !== undefined) attrs.color = applyHexOpacity(style.fill, style.opacity); + if (style.stroke !== undefined) attrs.borderColor = applyHexOpacity(style.stroke, style.opacity); if (style.lineWidth !== undefined) attrs.borderSize = style.lineWidth; if (style.label === false) { @@ -142,8 +142,11 @@ function badgeScaleFactor(style) { * - native circle/square (color=fill, fillColor/image cleared, borderRatio 0) */ function nodeProgramAttributes(style = {}, type) { - const fill = style.fill ?? DEFAULTS.NODE.FILL_COLOR; - const stroke = style.stroke ?? null; + // Opacity folds into the fill/stroke alpha here so it reaches every program: + // the baked SVG texture (shape), the border rings (borderCircle) and the + // native circle/square fill alike. applyHexOpacity is null/opaque-safe. + const fill = applyHexOpacity(style.fill ?? DEFAULTS.NODE.FILL_COLOR, style.opacity); + const stroke = applyHexOpacity(style.stroke ?? null, style.opacity); const lineWidth = style.lineWidth ?? 0; const hasBorder = Boolean(stroke) && lineWidth > 0; const size = Array.isArray(style.size) ? style.size[0] : style.size; @@ -412,7 +415,7 @@ function edgeMarkerHaloAttributes(style) { function edgeAttributesFromStyle(style = {}, type = undefined) { const attrs = {}; if (style.lineWidth !== undefined) attrs.size = style.lineWidth; - if (style.stroke !== undefined) attrs.color = style.stroke; + if (style.stroke !== undefined) attrs.color = applyHexOpacity(style.stroke, style.opacity); if (style.label === false) { attrs.label = null; } else if (style.label && style.labelText !== undefined) { diff --git a/src/graph/style.js b/src/graph/style.js index f60b3cc..c98d98a 100644 --- a/src/graph/style.js +++ b/src/graph/style.js @@ -47,6 +47,7 @@ class GraphStyleManager { fill : src.fill ?? d.FILL_COLOR, stroke : src.stroke ?? d.STROKE_COLOR, lineWidth : src.lineWidth ?? d.LINE_WIDTH, + opacity : src.opacity ?? d.OPACITY, badge : src.badge ?? false, badges : src.badges ?? [], @@ -111,6 +112,7 @@ class GraphStyleManager { lineWidth : src.lineWidth ?? d.LINE_WIDTH, lineDash : src.lineDash ?? d.LINE_DASH, stroke : src.stroke ?? d.COLOR, + opacity : src.opacity ?? d.OPACITY, halo : src.halo ?? d.HALO.ENABLED, haloStroke : src.haloStroke ?? d.HALO.COLOR, diff --git a/src/managers/io.js b/src/managers/io.js index a401fc2..6224a2e 100644 --- a/src/managers/io.js +++ b/src/managers/io.js @@ -28,790 +28,7 @@ function writeExportScale(scale) { } // @formatter:off -let excelData = { - s: { - readme: { - d: [ - [0, 0, 'Color Codes Explained'], - [0, 1, 'Description'], - [1, 0, 'Required'], - [1, 1, 'Strictly required property'], - [2, 0, 'Optional'], - [2, 1, 'Optional property; Column name can not be re-used for user-defined data'], - [3, 0, 'User Data [group]'], - [ - 3, - 1, - 'Add custom properties in new columns. Use [brackets] for grouping (e.g., "Temperature [Celsius] [Physics]"). The last [bracket] becomes the group name.', - ], - [5, 0, 'Data Types Explained'], - [5, 1, 'Description'], - [6, 0, 'text'], - [6, 1, 'any text'], - [7, 0, 'number'], - [7, 1, 'whole decimal numbers or floating point numbers'], - [8, 0, 'boolean'], - [8, 1, 'true or TRUE or 1, false or FALSE or 0'], - [9, 0, 'RGBA'], - [9, 1, 'RGBA hex color code (e.g. #C33D3580 for 50% opacity) (last 2 digits are optional)'], - [10, 0, 'value 1 | value 2'], - [ - 10, - 1, - 'List of categorical values of which one must be matched (excluding optional information in brackets)', - ], - [11, 0, 'any'], - [ - 11, - 1, - 'Categorical view when text is given, slider-based view when numerical input is given', - ], - [13, 0, 'Node Properties Explained'], - [13, 1, 'Description'], - [13, 2, 'Default Value'], - [13, 3, 'Type'], - [14, 0, 'ID'], - [14, 1, 'Unique identifier for a node'], - [14, 2, '-'], - [14, 3, 'text (unique)'], - [15, 0, 'Label'], - [15, 1, 'Label of the node; if no Label is given, the ID is displayed per default'], - [15, 2, '-'], - [15, 3, 'text'], - [16, 0, 'Description'], - [16, 1, 'Description of the node, displayed in the tooltip text'], - [16, 2, '-'], - [16, 3, 'text'], - [17, 0, 'Shape'], - [17, 1, 'The shape of the node'], - [17, 2, 'hexagon'], - [17, 3, 'circle (●) | diamond (β—†) | hexagon (β¬’) | rect (β– ) | triangle (β–²) | star (β˜…)'], - [18, 0, 'Size'], - [18, 1, 'The size of the node'], - [18, 2, '20'], - [18, 3, 'number'], - [19, 0, 'Fill Color'], - [ - 19, - 1, - 'The fill color of the node in RGBA format (e.g. #FF000080 for a red node with 50% opacity)', - ], - [19, 2, '#C33D35'], - [19, 3, 'rgba'], - [20, 0, 'Border Size'], - [20, 1, 'The stroke width'], - [20, 2, '1'], - [20, 3, 'number'], - [21, 0, 'Border Color'], - [21, 1, 'The stroke color of the node in RGBA format'], - [21, 2, '-'], - [21, 3, 'rgba'], - [22, 0, 'Label Font Size'], - [22, 1, 'Label font size'], - [22, 2, '12'], - [22, 3, 'number'], - [23, 0, 'Label Placement'], - [23, 1, 'Label position relative to the main shape of the node'], - [23, 2, 'bottom'], - [ - 23, - 3, - 'left | right | top | bottom | left-top | left-bottom | right-top | right-bottom | top-left | top-right | bottom-left | bottom-right | center', - ], - [24, 0, 'Label Color'], - [24, 1, 'The color of the nodes label'], - [24, 2, '-'], - [24, 3, 'rgba'], - [25, 0, 'Label Background Color'], - [25, 1, 'Label background fill color'], - [25, 2, '-'], - [25, 3, 'rgba'], - [26, 0, 'X Coordinate'], - [26, 1, 'The x coordinate of the node'], - [26, 2, '-'], - [26, 3, 'number'], - [27, 0, 'Y Coordinate'], - [27, 1, 'The y coordinate of the node'], - [27, 2, '-'], - [27, 3, 'number'], - [28, 0, 'property A [group A]'], - [28, 1, 'User-defined custom node properties'], - [28, 2, '-'], - [28, 3, 'any'], - [30, 0, 'Edge Properties Explained'], - [30, 1, 'Description'], - [30, 2, 'Default Value'], - [30, 3, 'Type'], - [31, 0, 'Source ID'], - [31, 1, 'The ID of the source node'], - [31, 2, '-'], - [31, 3, 'text'], - [32, 0, 'Target ID'], - [32, 1, 'The ID of the target node'], - [32, 2, '-'], - [32, 3, 'text'], - [33, 0, 'Label'], - [ - 33, - 1, - 'Label of the edge; if no Label is given, the edge is only visible as line without any text', - ], - [33, 2, '-'], - [33, 3, 'text'], - [34, 0, 'Description'], - [34, 1, 'Description of the edge, displayed in the tooltip text'], - [34, 2, '-'], - [34, 3, 'text'], - [35, 0, 'Type'], - [35, 1, 'The edge type'], - [35, 2, 'line'], - [35, 3, 'line | cubic | quadratic | polyline'], - [36, 0, 'Line Width'], - [36, 1, 'The border width of the edge'], - [36, 2, '0.75'], - [36, 3, 'number'], - [37, 0, 'Line Dash'], - [37, 1, 'The dash offset of the edge line'], - [37, 2, '0'], - [37, 3, 'number'], - [38, 0, 'Color'], - [38, 1, 'The stroke color of the edge in RGBA format'], - [38, 2, '#403C5390'], - [38, 3, 'rgba'], - [39, 0, 'Label Font Size'], - [39, 1, 'The font size of the edges label'], - [39, 2, 'center'], - [39, 3, 'number'], - [40, 0, 'Label Placement'], - [40, 1, 'The position of the label relative to the edge'], - [40, 2, '#000000'], - [40, 3, 'start | center | end'], - [41, 0, 'Label Auto Rotate'], - [41, 1, 'Whether to automatically rotate the label to match the edge’s direction'], - [41, 2, '1'], - [41, 3, 'boolean'], - [42, 0, 'Label Offset X'], - [42, 1, 'The offset of the label on the X-Axis'], - [42, 2, '0'], - [42, 3, 'number'], - [43, 0, 'Label Offset Y'], - [43, 1, 'The offset of the label on the Y-Axis'], - [43, 2, '0'], - [43, 3, 'number'], - [44, 0, 'Label Color'], - [44, 1, 'The color of the edges label text'], - [44, 2, '-'], - [44, 3, 'rgba'], - [45, 0, 'Label Background Color'], - [45, 1, 'The color for the edge label’s background'], - [45, 2, '#E4E3EA'], - [45, 3, 'rgba'], - [46, 0, 'Start Arrow'], - [46, 1, 'Whether to display the start arrow on the edge'], - [46, 2, { formula: 'FALSE()' }], - [46, 3, 'boolean'], - [47, 0, 'Start Arrow Size'], - [47, 1, 'The size of the start arrow'], - [47, 2, '8'], - [47, 3, 'number'], - [48, 0, 'Start Arrow Type'], - [48, 1, 'The type of the start arrow'], - [48, 2, 'triangle'], - [48, 3, 'triangle | circle | diamond | vee | rect | triangleRect | simple'], - [49, 0, 'End Arrow'], - [49, 1, 'Whether to display the end arrow on the edge'], - [49, 2, { formula: 'FALSE()' }], - [49, 3, 'boolean'], - [50, 0, 'End Arrow Size'], - [50, 1, 'The size of the end arrow'], - [50, 2, '8'], - [50, 3, 'number'], - [51, 0, 'End Arrow Type'], - [51, 1, 'The type of the end arrow'], - [51, 2, 'triangle'], - [51, 3, 'triangle | circle | diamond | vee | rect | triangleRect | simple'], - ], - st: { - A1: 0, - B1: 0, - C1: 1, - D1: 2, - A2: 3, - B2: 2, - C2: 1, - D2: 2, - A3: 4, - B3: 2, - C3: 1, - D3: 2, - A4: 5, - B4: 2, - C4: 1, - D4: 2, - A5: 2, - B5: 2, - C5: 1, - D5: 2, - A6: 0, - B6: 0, - C6: 6, - D6: 7, - A7: 8, - B7: 2, - C7: 1, - D7: 2, - A8: 8, - B8: 2, - C8: 1, - D8: 2, - A9: 8, - B9: 2, - C9: 1, - D9: 2, - A10: 8, - B10: 2, - C10: 1, - D10: 2, - A11: 9, - B11: 2, - C11: 1, - D11: 2, - A12: 8, - B12: 2, - C12: 1, - D12: 2, - A13: 2, - B13: 2, - C13: 1, - D13: 2, - A14: 10, - B14: 10, - C14: 11, - D14: 10, - A15: 3, - B15: 2, - C15: 1, - D15: 2, - A16: 4, - B16: 2, - C16: 1, - D16: 2, - A17: 4, - B17: 2, - C17: 1, - D17: 2, - A18: 4, - B18: 2, - C18: 1, - D18: 12, - A19: 4, - B19: 2, - C19: 1, - D19: 2, - A20: 4, - B20: 2, - C20: 1, - D20: 2, - A21: 4, - B21: 2, - C21: 1, - D21: 2, - A22: 4, - B22: 2, - C22: 1, - D22: 2, - A23: 4, - B23: 2, - C23: 1, - D23: 2, - A24: 4, - B24: 2, - C24: 1, - D24: 12, - A25: 4, - B25: 2, - C25: 1, - D25: 2, - A26: 4, - B26: 2, - C26: 1, - D26: 2, - A27: 4, - B27: 2, - C27: 1, - D27: 2, - A28: 4, - B28: 2, - C28: 1, - D28: 2, - A29: 5, - B29: 2, - C29: 1, - D29: 2, - A30: 2, - B30: 2, - C30: 1, - D30: 2, - A31: 10, - B31: 10, - C31: 11, - D31: 10, - A32: 3, - B32: 2, - C32: 1, - D32: 2, - A33: 3, - B33: 2, - C33: 1, - D33: 2, - A34: 4, - B34: 2, - C34: 1, - D34: 2, - A35: 4, - B35: 2, - C35: 1, - D35: 2, - A36: 4, - B36: 2, - C36: 1, - D36: 12, - A37: 4, - B37: 2, - C37: 1, - D37: 2, - A38: 4, - B38: 2, - C38: 1, - D38: 2, - A39: 4, - B39: 2, - C39: 1, - D39: 2, - A40: 4, - B40: 2, - C40: 1, - D40: 2, - A41: 4, - B41: 2, - C41: 1, - D41: 12, - A42: 4, - B42: 2, - C42: 13, - D42: 2, - A43: 4, - B43: 2, - C43: 1, - D43: 2, - A44: 4, - B44: 2, - C44: 1, - D44: 2, - A45: 4, - B45: 2, - C45: 1, - D45: 2, - A46: 4, - B46: 2, - C46: 13, - D46: 2, - A47: 4, - B47: 2, - C47: 13, - D47: 2, - A48: 4, - B48: 2, - C48: 1, - D48: 2, - A49: 4, - B49: 2, - C49: 13, - D49: 12, - A50: 4, - B50: 2, - C50: 13, - D50: 2, - A51: 4, - B51: 2, - C51: 1, - D51: 14, - A52: 4, - B52: 2, - C52: 13, - D52: 12, - }, - dim: [52, 4], - }, - nodes: { - d: [ - [0, 0, 'ID'], - [0, 1, 'Label'], - [0, 2, 'Description'], - [0, 3, 'Shape'], - [0, 4, 'Size'], - [0, 5, 'Fill Color'], - [0, 6, 'Border Color'], - [0, 7, 'Feature X [group A]'], - [0, 8, 'Feature Y [nm] [group A]'], - [0, 9, 'Feature Z [group B]'], - [1, 0, 'A'], - [1, 1, 'Node 1'], - [1, 2, 'The first node'], - [1, 3, 'circle'], - [1, 4, '60'], - [1, 5, '#403C53'], - [1, 6, '#C33D35'], - [1, 7, '1'], - [1, 8, 'foo'], - [1, 9, '1'], - [2, 0, 'B'], - [2, 1, 'Node 2'], - [2, 2, 'The second node'], - [2, 7, '0.5'], - [2, 8, 'foo'], - [2, 9, '2'], - [3, 0, 'C'], - [3, 1, 'Node 3'], - [3, 2, 'The third node'], - [3, 7, '1.1'], - [3, 8, 'foo'], - [3, 9, '1'], - [4, 0, 'D'], - [4, 1, 'Node 4'], - [4, 2, 'The fourth node'], - [4, 7, '1.3'], - [4, 8, 'bar'], - [4, 9, '0'], - [5, 0, 'E'], - [5, 7, '0'], - [5, 8, 'bar'], - [5, 9, '-1'], - [6, 0, 'F'], - [6, 1, 'Lonely Node'], - [6, 7, '-1'], - ], - st: { - A1: 3, - B1: 4, - C1: 4, - D1: 4, - E1: 4, - F1: 4, - G1: 4, - H1: 5, - I1: 5, - J1: 5, - A2: 15, - B2: 16, - C2: 16, - D2: 16, - E2: 16, - F2: 16, - G2: 16, - H2: 17, - I2: 17, - J2: 17, - A3: 15, - B3: 16, - C3: 16, - D3: 16, - E3: 16, - F3: 16, - G3: 16, - H3: 17, - I3: 17, - J3: 17, - A4: 15, - B4: 16, - C4: 16, - D4: 16, - E4: 16, - F4: 16, - G4: 16, - H4: 17, - I4: 17, - J4: 17, - A5: 15, - B5: 16, - C5: 16, - D5: 16, - E5: 16, - F5: 16, - G5: 16, - H5: 17, - I5: 17, - J5: 17, - A6: 15, - B6: 16, - C6: 16, - D6: 16, - E6: 16, - F6: 16, - G6: 16, - H6: 17, - I6: 17, - J6: 17, - A7: 15, - B7: 16, - C7: 16, - D7: 16, - E7: 16, - F7: 16, - G7: 16, - H7: 17, - I7: 17, - J7: 17, - }, - dim: [7, 10], - }, - edges: { - d: [ - [0, 0, 'Source ID'], - [0, 1, 'Target ID'], - [0, 2, 'Color'], - [0, 3, 'Line Width'], - [0, 4, 'Label'], - [0, 5, 'Feature EX [group X]'], - [0, 6, 'Feature EY [group X]'], - [0, 7, 'Feature EZ [group Y]'], - [1, 0, 'A'], - [1, 1, 'B'], - [1, 2, '#FF0000'], - [1, 3, '0.75'], - [1, 4, 'foo'], - [1, 5, '1'], - [1, 6, 'Dummy Category 1'], - [1, 7, '1'], - [2, 0, 'A'], - [2, 1, 'C'], - [2, 5, '0.5'], - [2, 6, 'Dummy Category 2'], - [2, 7, '2'], - [3, 0, 'C'], - [3, 1, 'D'], - [3, 5, '1.1'], - [3, 6, 'Dummy Category 3'], - [3, 7, '1'], - [4, 0, 'D'], - [4, 1, 'E'], - [4, 5, '1.3'], - [4, 6, 'Dummy Category 4'], - [4, 7, '0'], - ], - st: { - A1: 3, - B1: 3, - C1: 4, - D1: 4, - E1: 4, - F1: 5, - G1: 5, - H1: 5, - A2: 15, - B2: 15, - C2: 16, - D2: 16, - E2: 16, - F2: 17, - G2: 17, - H2: 17, - A3: 15, - B3: 15, - C3: 16, - D3: 16, - E3: 16, - F3: 17, - G3: 17, - H3: 17, - A4: 15, - B4: 15, - C4: 16, - D4: 16, - E4: 16, - F4: 17, - G4: 17, - H4: 17, - A5: 15, - B5: 15, - C5: 16, - D5: 16, - E5: 16, - F5: 17, - G5: 17, - H5: 17, - }, - dim: [5, 8], - }, - }, - st: { - 0: { - f: { b: 1, sz: 12, n: 'Arial' }, - fill: { fg: 'E4E3EA', bg: 'FEFFE1' }, - b: { - t: ['thin', '000000'], - b: ['thin', '000000'], - l: ['thin', '000000'], - r: ['thin', '000000'], - }, - a: { h: 'general', v: 'bottom' }, - nf: 'General', - }, - 1: { - f: { sz: 10, n: 'Arial' }, - fill: { p: 'none' }, - a: { h: 'left', v: 'bottom' }, - nf: 'General', - }, - 2: { - f: { sz: 10, n: 'Arial' }, - fill: { p: 'none' }, - a: { h: 'general', v: 'bottom' }, - nf: 'General', - }, - 3: { - f: { b: 1, sz: 10, n: 'Arial' }, - fill: { fg: 'FF9A9A', bg: 'FF8080' }, - b: { - t: ['thin', '000000'], - b: ['thin', '000000'], - l: ['thin', '000000'], - r: ['thin', '000000'], - }, - a: { h: 'general', v: 'bottom' }, - nf: 'General', - }, - 4: { - f: { b: 1, sz: 10, n: 'Arial' }, - fill: { fg: 'FEFFE1', bg: 'FFFFFF' }, - b: { - t: ['thin', '000000'], - b: ['thin', '000000'], - l: ['thin', '000000'], - r: ['thin', '000000'], - }, - a: { h: 'general', v: 'bottom' }, - nf: 'General', - }, - 5: { - f: { b: 1, sz: 10, n: 'Arial' }, - fill: { fg: '81D41A', bg: '969696' }, - b: { - t: ['thin', '000000'], - b: ['thin', '000000'], - l: ['thin', '000000'], - r: ['thin', '000000'], - }, - a: { h: 'general', v: 'bottom' }, - nf: 'General', - }, - 6: { - f: { b: 1, sz: 12, n: 'Arial' }, - fill: { p: 'none' }, - a: { h: 'left', v: 'bottom' }, - nf: 'General', - }, - 7: { - f: { b: 1, sz: 12, n: 'Arial' }, - fill: { p: 'none' }, - a: { h: 'general', v: 'bottom' }, - nf: 'General', - }, - 8: { - f: { b: 1, sz: 10, n: 'Arial' }, - fill: { fg: 'E4E3EA', bg: 'FEFFE1' }, - b: { - t: ['thin', '000000'], - b: ['thin', '000000'], - l: ['thin', '000000'], - r: ['thin', '000000'], - }, - a: { h: 'general', v: 'bottom' }, - nf: 'General', - }, - 9: { - f: { b: 1, i: 1, sz: 10, n: 'Arial' }, - fill: { fg: 'E4E3EA', bg: 'FEFFE1' }, - b: { - t: ['thin', '000000'], - b: ['thin', '000000'], - l: ['thin', '000000'], - r: ['thin', '000000'], - }, - a: { h: 'general', v: 'bottom' }, - nf: 'General', - }, - 10: { - f: { b: 1, sz: 12, n: 'Arial' }, - fill: { fg: 'E4E3EA', bg: 'FEFFE1' }, - b: { t: ['thin', '000000'], b: ['thin', '000000'] }, - a: { h: 'general', v: 'bottom' }, - nf: 'General', - }, - 11: { - f: { b: 1, sz: 12, n: 'Arial' }, - fill: { fg: 'E4E3EA', bg: 'FEFFE1' }, - b: { t: ['thin', '000000'], b: ['thin', '000000'] }, - a: { h: 'left', v: 'bottom' }, - nf: 'General', - }, - 12: { - f: { i: 1, sz: 10, n: 'Arial' }, - fill: { p: 'none' }, - a: { h: 'general', v: 'bottom' }, - nf: 'General', - }, - 13: { - f: { sz: 10, n: 'Arial' }, - fill: { p: 'none' }, - a: { h: 'left', v: 'bottom' }, - nf: '"TRUE";"TRUE";"FALSE"', - }, - 14: { - f: { u: 1, sz: 10, n: 'Arial' }, - fill: { p: 'none' }, - a: { h: 'general', v: 'bottom' }, - nf: 'General', - }, - 15: { - f: { sz: 10, n: 'Arial' }, - fill: { fg: 'FF9A9A', bg: 'FF8080' }, - b: { - t: ['thin', '000000'], - b: ['thin', '000000'], - l: ['thin', '000000'], - r: ['thin', '000000'], - }, - a: { h: 'general', v: 'bottom' }, - nf: 'General', - }, - 16: { - f: { sz: 10, n: 'Arial' }, - fill: { fg: 'FEFFE1', bg: 'FFFFFF' }, - b: { - t: ['thin', '000000'], - b: ['thin', '000000'], - l: ['thin', '000000'], - r: ['thin', '000000'], - }, - a: { h: 'general', v: 'bottom' }, - nf: 'General', - }, - 17: { - f: { sz: 10, n: 'Arial' }, - fill: { fg: '81D41A', bg: '969696' }, - b: { - t: ['thin', '000000'], - b: ['thin', '000000'], - l: ['thin', '000000'], - r: ['thin', '000000'], - }, - a: { h: 'general', v: 'bottom' }, - nf: 'General', - }, - }, - sc: 18, -}; +let excelData = {"s":{"readme":{"d":[[0,0,"Color Codes Explained"],[0,1,"Description"],[1,0,"Required"],[1,1,"Strictly required property"],[2,0,"Optional"],[2,1,"Optional property; Column name can not be re-used for user-defined data"],[3,0,"User Data [group]"],[3,1,"Add custom properties in new columns. Use [brackets] for grouping (e.g., \"Temperature [Celsius] [Physics]\"). The last [bracket] becomes the group name."],[5,0,"Data Types Explained"],[5,1,"Description"],[6,0,"text"],[6,1,"any text"],[7,0,"number"],[7,1,"integers or floating-point numbers"],[8,0,"boolean"],[8,1,"true or TRUE or 1, false or FALSE or 0"],[9,0,"RGBA"],[9,1,"RGBA hex color code (e.g. #C33D3580 for 50% opacity) (last 2 digits are optional)"],[10,0,"value 1 | value 2"],[10,1,"List of categorical values of which one must be matched (excluding optional information in brackets)"],[11,0,"any"],[11,1,"Categorical view when text is given, slider-based view when numerical input is given"],[13,0,"Node Properties Explained"],[13,1,"Description"],[13,2,"Default Value"],[13,3,"Type"],[14,0,"ID"],[14,1,"Unique identifier for a node"],[14,2,"-"],[14,3,"text (unique)"],[15,0,"Label"],[15,1,"Label of the node; if no Label is given, the ID is displayed per default"],[15,2,"-"],[15,3,"text"],[16,0,"Description"],[16,1,"Description of the node, displayed in the tooltip text"],[16,2,"-"],[16,3,"text"],[17,0,"Shape"],[17,1,"The shape of the node"],[17,2,"hexagon"],[17,3,"circle (●) | diamond (β—†) | hexagon (β¬’) | rect (β– ) | triangle (β–²) | star (β˜…)"],[18,0,"Size"],[18,1,"The size of the node"],[18,2,"20"],[18,3,"number"],[19,0,"Fill Color"],[19,1,"The fill color of the node in RGBA format (e.g. #FF000080 for a red node with 50% opacity)"],[19,2,"#C33D35"],[19,3,"rgba"],[20,0,"Border Size"],[20,1,"The stroke width"],[20,2,"1"],[20,3,"number"],[21,0,"Border Color"],[21,1,"The stroke color of the node in RGBA format"],[21,2,"-"],[21,3,"rgba"],[22,0,"Opacity"],[22,1,"The opacity of the node, applied to its fill and border (1 = opaque, 0 = transparent). Multiplies into the color’s own alpha, so it composes with an RGBA fill/border color."],[22,2,"1"],[22,3,"number"],[23,0,"Label Font Size"],[23,1,"Label font size"],[23,2,"12"],[23,3,"number"],[24,0,"Label Placement"],[24,1,"Label position relative to the main shape of the node"],[24,2,"bottom"],[24,3,"left | right | top | bottom | left-top | left-bottom | right-top | right-bottom | top-left | top-right | bottom-left | bottom-right | center"],[25,0,"Label Color"],[25,1,"The color of the nodes label"],[25,2,"-"],[25,3,"rgba"],[26,0,"Label Background Color"],[26,1,"Label background fill color"],[26,2,"-"],[26,3,"rgba"],[27,0,"X Coordinate"],[27,1,"The x coordinate of the node"],[27,2,"-"],[27,3,"number"],[28,0,"Y Coordinate"],[28,1,"The y coordinate of the node"],[28,2,"-"],[28,3,"number"],[29,0,"property A [group A]"],[29,1,"User-defined custom node properties"],[29,2,"-"],[29,3,"any"],[31,0,"Edge Properties Explained"],[31,1,"Description"],[31,2,"Default Value"],[31,3,"Type"],[32,0,"Source ID"],[32,1,"The ID of the source node"],[32,2,"-"],[32,3,"text"],[33,0,"Target ID"],[33,1,"The ID of the target node"],[33,2,"-"],[33,3,"text"],[34,0,"Label"],[34,1,"Label of the edge; if no Label is given, the edge is only visible as line without any text"],[34,2,"-"],[34,3,"text"],[35,0,"Description"],[35,1,"Description of the edge, displayed in the tooltip text"],[35,2,"-"],[35,3,"text"],[36,0,"Type"],[36,1,"The edge type"],[36,2,"line"],[36,3,"line | cubic | quadratic | polyline"],[37,0,"Line Width"],[37,1,"The border width of the edge"],[37,2,"0.75"],[37,3,"number"],[38,0,"Line Dash"],[38,1,"The dash offset of the edge line"],[38,2,"0"],[38,3,"number"],[39,0,"Color"],[39,1,"The stroke color of the edge in RGBA format"],[39,2,"#403C5390"],[39,3,"rgba"],[40,0,"Opacity"],[40,1,"The opacity of the edge (1 = opaque, 0 = transparent). Multiplies into the color’s own alpha, so it composes with an RGBA edge color."],[40,2,"1"],[40,3,"number"],[41,0,"Label Font Size"],[41,1,"The font size of the edges label"],[41,2,"12"],[41,3,"number"],[42,0,"Label Placement"],[42,1,"The position of the label relative to the edge"],[42,2,"center"],[42,3,"start | center | end"],[43,0,"Label Auto Rotate"],[43,1,"Whether to automatically rotate the label to match the edge’s direction"],[43,2,{"formula":"FALSE()"}],[43,3,"boolean"],[44,0,"Label Offset X"],[44,1,"The offset of the label on the X-Axis"],[44,2,"4"],[44,3,"number"],[45,0,"Label Offset Y"],[45,1,"The offset of the label on the Y-Axis"],[45,2,"0"],[45,3,"number"],[46,0,"Label Color"],[46,1,"The color of the edges label text"],[46,2,"#000000"],[46,3,"rgba"],[47,0,"Label Background Color"],[47,1,"The color for the edge label’s background"],[47,2,"-"],[47,3,"rgba"],[48,0,"Start Arrow"],[48,1,"Whether to display the start arrow on the edge"],[48,2,{"formula":"FALSE()"}],[48,3,"boolean"],[49,0,"Start Arrow Size"],[49,1,"The size of the start arrow"],[49,2,"8"],[49,3,"number"],[50,0,"Start Arrow Type"],[50,1,"The type of the start arrow"],[50,2,"arrow"],[50,3,"arrow | rect | diamond | circle | tee | triangle | vee | triangleRect | simple | square"],[51,0,"Start Arrow Color"],[51,1,"The fill color of the start arrow; inherits the edge color if unset"],[51,2,"-"],[51,3,"rgba"],[52,0,"Start Arrow Border Color"],[52,1,"The border color of the start arrow; no border if unset"],[52,2,"-"],[52,3,"rgba"],[53,0,"Start Arrow Border Size"],[53,1,"The border width of the start arrow (0 = auto, ~20% of the marker)"],[53,2,"0"],[53,3,"number"],[54,0,"End Arrow"],[54,1,"Whether to display the end arrow on the edge"],[54,2,{"formula":"FALSE()"}],[54,3,"boolean"],[55,0,"End Arrow Size"],[55,1,"The size of the end arrow"],[55,2,"8"],[55,3,"number"],[56,0,"End Arrow Type"],[56,1,"The type of the end arrow"],[56,2,"arrow"],[56,3,"arrow | rect | diamond | circle | tee | triangle | vee | triangleRect | simple | square"],[57,0,"End Arrow Color"],[57,1,"The fill color of the end arrow; inherits the edge color if unset"],[57,2,"-"],[57,3,"rgba"],[58,0,"End Arrow Border Color"],[58,1,"The border color of the end arrow; no border if unset"],[58,2,"-"],[58,3,"rgba"],[59,0,"End Arrow Border Size"],[59,1,"The border width of the end arrow (0 = auto, ~20% of the marker)"],[59,2,"0"],[59,3,"number"]],"st":{"A1":0,"B1":0,"C1":1,"D1":2,"A2":3,"B2":2,"C2":1,"D2":2,"A3":4,"B3":2,"C3":1,"D3":2,"A4":5,"B4":2,"C4":1,"D4":2,"A5":2,"B5":2,"C5":1,"D5":2,"A6":0,"B6":0,"C6":6,"D6":7,"A7":8,"B7":2,"C7":1,"D7":2,"A8":8,"B8":2,"C8":1,"D8":2,"A9":8,"B9":2,"C9":1,"D9":2,"A10":8,"B10":2,"C10":1,"D10":2,"A11":9,"B11":2,"C11":1,"D11":2,"A12":8,"B12":2,"C12":1,"D12":2,"A13":2,"B13":2,"C13":1,"D13":2,"A14":10,"B14":10,"C14":11,"D14":10,"A15":3,"B15":2,"C15":1,"D15":2,"A16":4,"B16":2,"C16":1,"D16":2,"A17":4,"B17":2,"C17":1,"D17":2,"A18":4,"B18":2,"C18":1,"D18":12,"A19":4,"B19":2,"C19":1,"D19":2,"A20":4,"B20":2,"C20":1,"D20":2,"A21":4,"B21":2,"C21":1,"D21":2,"A22":4,"B22":2,"C22":1,"D22":2,"A23":4,"B23":2,"C23":1,"D23":2,"A24":4,"B24":2,"C24":1,"D24":2,"A25":4,"B25":2,"C25":1,"D25":12,"A26":4,"B26":2,"C26":1,"D26":2,"A27":4,"B27":2,"C27":1,"D27":2,"A28":4,"B28":2,"C28":1,"D28":2,"A29":4,"B29":2,"C29":1,"D29":2,"A30":5,"B30":2,"C30":1,"D30":2,"A31":2,"B31":2,"C31":1,"D31":2,"A32":10,"B32":10,"C32":11,"D32":10,"A33":3,"B33":2,"C33":1,"D33":2,"A34":3,"B34":2,"C34":1,"D34":2,"A35":4,"B35":2,"C35":1,"D35":2,"A36":4,"B36":2,"C36":1,"D36":2,"A37":4,"B37":2,"C37":1,"D37":12,"A38":4,"B38":2,"C38":1,"D38":2,"A39":4,"B39":2,"C39":1,"D39":2,"A40":4,"B40":2,"C40":1,"D40":2,"A41":4,"B41":2,"C41":1,"D41":2,"A42":4,"B42":2,"C42":1,"D42":2,"A43":4,"B43":2,"C43":1,"D43":12,"A44":4,"B44":2,"C44":13,"D44":2,"A45":4,"B45":2,"C45":1,"D45":2,"A46":4,"B46":2,"C46":1,"D46":2,"A47":4,"B47":2,"C47":1,"D47":2,"A48":4,"B48":2,"C48":13,"D48":2,"A49":4,"B49":2,"C49":13,"D49":2,"A50":4,"B50":2,"C50":1,"D50":2,"A51":4,"B51":2,"C51":13,"D51":12,"A52":4,"B52":2,"C52":13,"D52":12,"A53":4,"B53":2,"C53":13,"D53":12,"A54":4,"B54":2,"C54":1,"D54":12,"A55":4,"B55":2,"C55":13,"D55":2,"A56":4,"B56":2,"C56":1,"D56":14,"A57":4,"B57":2,"C57":13,"D57":12,"A58":4,"B58":2,"C58":13,"D58":12,"A59":4,"B59":2,"C59":13,"D59":12,"A60":4,"B60":2,"C60":1,"D60":12},"dim":[60,4]},"nodes":{"d":[[0,0,"ID"],[0,1,"Label"],[0,2,"Description"],[0,3,"Shape"],[0,4,"Size"],[0,5,"Fill Color"],[0,6,"Border Color"],[0,7,"Feature X [group A]"],[0,8,"Feature Y [nm] [group A]"],[0,9,"Feature Z [group B]"],[1,0,"A"],[1,1,"Node 1"],[1,2,"The first node"],[1,3,"circle"],[1,4,"60"],[1,5,"#403C53"],[1,6,"#C33D35"],[1,7,"1"],[1,8,"foo"],[1,9,"1"],[2,0,"B"],[2,1,"Node 2"],[2,2,"The second node"],[2,7,"0.5"],[2,8,"foo"],[2,9,"2"],[3,0,"C"],[3,1,"Node 3"],[3,2,"The third node"],[3,7,"1.1"],[3,8,"foo"],[3,9,"1"],[4,0,"D"],[4,1,"Node 4"],[4,2,"The fourth node"],[4,7,"1.3"],[4,8,"bar"],[4,9,"0"],[5,0,"E"],[5,7,"0"],[5,8,"bar"],[5,9,"-1"],[6,0,"F"],[6,1,"Lonely Node"],[6,7,"-1"]],"st":{"A1":3,"B1":4,"C1":4,"D1":4,"E1":4,"F1":4,"G1":4,"H1":5,"I1":5,"J1":5,"A2":15,"B2":16,"C2":16,"D2":16,"E2":16,"F2":16,"G2":16,"H2":17,"I2":17,"J2":17,"A3":15,"B3":16,"C3":16,"D3":16,"E3":16,"F3":16,"G3":16,"H3":17,"I3":17,"J3":17,"A4":15,"B4":16,"C4":16,"D4":16,"E4":16,"F4":16,"G4":16,"H4":17,"I4":17,"J4":17,"A5":15,"B5":16,"C5":16,"D5":16,"E5":16,"F5":16,"G5":16,"H5":17,"I5":17,"J5":17,"A6":15,"B6":16,"C6":16,"D6":16,"E6":16,"F6":16,"G6":16,"H6":17,"I6":17,"J6":17,"A7":15,"B7":16,"C7":16,"D7":16,"E7":16,"F7":16,"G7":16,"H7":17,"I7":17,"J7":17},"dim":[7,10]},"edges":{"d":[[0,0,"Source ID"],[0,1,"Target ID"],[0,2,"Color"],[0,3,"Line Width"],[0,4,"Label"],[0,5,"Feature EX [group X]"],[0,6,"Feature EY [group X]"],[0,7,"Feature EZ [group Y]"],[1,0,"A"],[1,1,"B"],[1,2,"#FF0000"],[1,3,"0.75"],[1,4,"foo"],[1,5,"1"],[1,6,"Dummy Category 1"],[1,7,"1"],[2,0,"A"],[2,1,"C"],[2,5,"0.5"],[2,6,"Dummy Category 2"],[2,7,"2"],[3,0,"C"],[3,1,"D"],[3,5,"1.1"],[3,6,"Dummy Category 3"],[3,7,"1"],[4,0,"D"],[4,1,"E"],[4,5,"1.3"],[4,6,"Dummy Category 4"],[4,7,"0"]],"st":{"A1":3,"B1":3,"C1":4,"D1":4,"E1":4,"F1":5,"G1":5,"H1":5,"A2":15,"B2":15,"C2":16,"D2":16,"E2":16,"F2":17,"G2":17,"H2":17,"A3":15,"B3":15,"C3":16,"D3":16,"E3":16,"F3":17,"G3":17,"H3":17,"A4":15,"B4":15,"C4":16,"D4":16,"E4":16,"F4":17,"G4":17,"H4":17,"A5":15,"B5":15,"C5":16,"D5":16,"E5":16,"F5":17,"G5":17,"H5":17},"dim":[5,8]}},"st":{"0":{"f":{"b":1,"sz":12,"n":"Arial"},"fill":{"fg":"E4E3EA","bg":"FEFFE1"},"b":{"t":["thin","000000"],"b":["thin","000000"],"l":["thin","000000"],"r":["thin","000000"]},"a":{"v":"bottom"}},"1":{"f":{"sz":10,"n":"Arial"},"fill":{"p":"none"},"a":{"h":"left","v":"bottom"}},"2":{"f":{"sz":10,"n":"Arial"},"fill":{"p":"none"},"a":{"v":"bottom"}},"3":{"f":{"b":1,"sz":10,"n":"Arial"},"fill":{"fg":"FF9A9A","bg":"FF8080"},"b":{"t":["thin","000000"],"b":["thin","000000"],"l":["thin","000000"],"r":["thin","000000"]},"a":{"v":"bottom"}},"4":{"f":{"b":1,"sz":10,"n":"Arial"},"fill":{"fg":"FEFFE1","bg":"FFFFFF"},"b":{"t":["thin","000000"],"b":["thin","000000"],"l":["thin","000000"],"r":["thin","000000"]},"a":{"v":"bottom"}},"5":{"f":{"b":1,"sz":10,"n":"Arial"},"fill":{"fg":"81D41A","bg":"969696"},"b":{"t":["thin","000000"],"b":["thin","000000"],"l":["thin","000000"],"r":["thin","000000"]},"a":{"v":"bottom"}},"6":{"f":{"b":1,"sz":12,"n":"Arial"},"fill":{"p":"none"},"a":{"h":"left","v":"bottom"}},"7":{"f":{"b":1,"sz":12,"n":"Arial"},"fill":{"p":"none"},"a":{"v":"bottom"}},"8":{"f":{"b":1,"sz":10,"n":"Arial"},"fill":{"fg":"E4E3EA","bg":"FEFFE1"},"b":{"t":["thin","000000"],"b":["thin","000000"],"l":["thin","000000"],"r":["thin","000000"]},"a":{"v":"bottom"}},"9":{"f":{"b":1,"i":1,"sz":10,"n":"Arial"},"fill":{"fg":"E4E3EA","bg":"FEFFE1"},"b":{"t":["thin","000000"],"b":["thin","000000"],"l":["thin","000000"],"r":["thin","000000"]},"a":{"v":"bottom"}},"10":{"f":{"b":1,"sz":12,"n":"Arial"},"fill":{"fg":"E4E3EA","bg":"FEFFE1"},"b":{"t":["thin","000000"],"b":["thin","000000"]},"a":{"v":"bottom"}},"11":{"f":{"b":1,"sz":12,"n":"Arial"},"fill":{"fg":"E4E3EA","bg":"FEFFE1"},"b":{"t":["thin","000000"],"b":["thin","000000"]},"a":{"h":"left","v":"bottom"}},"12":{"f":{"i":1,"sz":10,"n":"Arial"},"fill":{"p":"none"},"a":{"v":"bottom"}},"13":{"f":{"sz":10,"n":"Arial"},"fill":{"p":"none"},"a":{"h":"left","v":"bottom"},"nf":"\"TRUE\";\"TRUE\";\"FALSE\""},"14":{"f":{"u":1,"sz":10,"n":"Arial"},"fill":{"p":"none"},"a":{"v":"bottom"}},"15":{"f":{"sz":10,"n":"Arial"},"fill":{"fg":"FF9A9A","bg":"FF8080"},"b":{"t":["thin","000000"],"b":["thin","000000"],"l":["thin","000000"],"r":["thin","000000"]},"a":{"v":"bottom"}},"16":{"f":{"sz":10,"n":"Arial"},"fill":{"fg":"FEFFE1","bg":"FFFFFF"},"b":{"t":["thin","000000"],"b":["thin","000000"],"l":["thin","000000"],"r":["thin","000000"]},"a":{"v":"bottom"}},"17":{"f":{"sz":10,"n":"Arial"},"fill":{"fg":"81D41A","bg":"969696"},"b":{"t":["thin","000000"],"b":["thin","000000"],"l":["thin","000000"],"r":["thin","000000"]},"a":{"v":"bottom"}}},"sc":18}; // @formatter:on // The following constants define the columns in the Excel template for mapping node and edge properties @@ -903,6 +120,16 @@ const EXCEL_NODE_PROPERTIES = [ return n.style.stroke; }, }, + { + column: 'Opacity', + type: 'num', + apply: (n, v) => { + n.style.opacity = v; + }, + get: (n) => { + return n.style.opacity; + }, + }, { column: 'Label Font Size', type: 'num', @@ -1053,6 +280,16 @@ const EXCEL_EDGE_PROPERTIES = [ return e.style.stroke; }, }, + { + column: 'Opacity', + type: 'num', + apply: (e, v) => { + e.style.opacity = v; + }, + get: (e) => { + return e.style.opacity; + }, + }, { column: 'Label Font Size', type: 'num', diff --git a/src/managers/ui_style_div.js b/src/managers/ui_style_div.js index 4b32e53..1d14549 100644 --- a/src/managers/ui_style_div.js +++ b/src/managers/ui_style_div.js @@ -133,6 +133,9 @@ function createStyleDiv(cache) { case "Node Border Color": await cache.gcm.updateNodes({style: {stroke: value}}, commands); break; + case "Node Opacity": + await cache.gcm.updateNodes({style: {opacity: value}}, commands); + break; case "Node Label Color": await cache.gcm.updateNodes({style: {labelFill: value}}, commands); break; @@ -151,6 +154,9 @@ function createStyleDiv(cache) { case "Edge Width": await cache.gcm.updateEdges({style: {lineWidth: value}}, commands); break; + case "Edge Opacity": + await cache.gcm.updateEdges({style: {opacity: value}}, commands); + break; case "Edge Dash": await cache.gcm.updateEdges({style: {lineDash: value}}, commands); break; @@ -877,6 +883,14 @@ function createStyleDiv(cache) { createColorControls(rowFive, "Node Border Color", cache.DEFAULTS.NODE.STROKE_COLOR, cache.DEFAULTS.STYLES.NODE_BORDER_COLORS); + const rowFiveOpacity = createNewRow(nodeDiv); + appendLabel(rowFiveOpacity, "Opacity", + "Transparency of the selected nodes (1 = opaque, 0 = invisible). Composes with the fill/border color's own alpha."); + createNumericalSlider(rowFiveOpacity, "Node Opacity", cache.DEFAULTS.NODE.OPACITY, + {min: 0, max: 1, step: 0.05}, + "Transparency of the selected nodes (1 = opaque, 0 = invisible). Composes with the fill/border color's own alpha.", + true); + appendHorizontalRule(nodeDiv); const rowSix = createNewRow(nodeDiv); @@ -944,6 +958,14 @@ function createStyleDiv(cache) { appendLabel(rowFour, "Color", "Define the selected edges color."); createColorControls(rowFour, "Edge Color", cache.DEFAULTS.EDGE.COLOR, cache.DEFAULTS.STYLES.EDGE_COLORS); + const rowFourOpacity = createNewRow(edgeDiv); + appendLabel(rowFourOpacity, "Opacity", + "Transparency of the selected edges (1 = opaque, 0 = invisible). Composes with the edge color's own alpha."); + createNumericalSlider(rowFourOpacity, "Edge Opacity", cache.DEFAULTS.EDGE.OPACITY, + {min: 0, max: 1, step: 0.05}, + "Transparency of the selected edges (1 = opaque, 0 = invisible). Composes with the edge color's own alpha.", + true); + appendHorizontalRule(edgeDiv); const rowFive = createNewRow(edgeDiv); diff --git a/src/utilities/numeric_scale_picker.js b/src/utilities/numeric_scale_picker.js index b286a1a..cc3de22 100644 --- a/src/utilities/numeric_scale_picker.js +++ b/src/utilities/numeric_scale_picker.js @@ -138,6 +138,10 @@ class NumericScalePicker { this.minOutput = defaults.LABEL.FONT_SIZE; this.maxOutput = 30; break; + case 'Node Opacity': + this.minOutput = 0.2; + this.maxOutput = defaults.OPACITY; + break; default: this.minOutput = 0; this.maxOutput = 100; @@ -161,6 +165,10 @@ class NumericScalePicker { this.minOutput = defaults.HALO.WIDTH; this.maxOutput = 10; break; + case 'Edge Opacity': + this.minOutput = 0.2; + this.maxOutput = defaults.OPACITY; + break; default: this.minOutput = 0; this.maxOutput = 100; diff --git a/templates/simple-template.xlsx b/templates/simple-template.xlsx index 5a079bcd376b1d58ebeec25ad75dad72d2320dad..879429d38e9c691f9c788e1f554147f13554fefd 100644 GIT binary patch literal 16444 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 x>iQ?OH2YtK{#B1{emI{A14?=lX{JzW}7^z1;u+ literal 17405 zcmeHvbyQqS)-Or$;BJB78r3L7_&A^Jb0QN7=2u?XgxPU~6D2*iflc2G0YJ*l&hk~L zNO3hkKFaRQ=*dnOFM4TSNZ`3%$_>_t!4&1>lwmz7!JIWajnECIiRxLpF-_+Sc_}Dp z$}cchXU|sYc?AY0|1a%^_`BVHwpc-ew#_0vTF@zonrKyM0}NXPUPVlxqK>X!+x?iG z{A-z^SOaD5^y8f@oUfC#%8WL@B|&!oE{|9IbZuc=7cmxfog~b$7ztRfBkeavQa)?U zb_SSoLn>26YVkox8jf@?cI^+q81n}ty^b16%FHryeKS`W^jc4C|8DVInO;2EQO3IZ z7fDMfUsM<)@+n}b$v>t+HE4Oc4jh(9vJQhMfAabR4tP9dT5hFy6f6mMmg8;x^{ zGc(`X;6x%cU1!!!W?{tdn-h|Jx3a>nZh0J{VqT4qkbPM)r?;uSRrewwWNxa@$RjleX=JVxZrizaP=dkCO%hPY%h7jcX9=f|>&}vJrXN3rGItWc( z3?Z+;i)W*d8%kiWUCazh9&A8nU!)+qwlY;tAQxm#}^&<;H{X2e2l zt(QU?o+>bsmNP(MPttCDlpCbzt`o#MyeG`+mLDWub3KNG2pmVdS#ciczd>kpR=iRC zcnrHF^>BDf|7}HvjKPHL<*OYkg1WD}3zECnuIwcJRV0W{vOyjWL)Mg2W! z9n6fajOl)RXZUfdFDJ@GFVZ6goj#x^oob`jyyXu-hseWMJ@9P+b$vvkYd~FCSw27g zkO6j6+E7ZRvZQquk?qQ}nHsa*gGf2?dO8S`s=I3$nz+y`);3Y(dTH)@6OPzWfb=bF zFOi1Zboa&SVF3f2t zFk0)JI#045v4t5mRsz|iN)0HJ(mswQ=^4q8#1K>4*S-EOjuLj{^Bmn7ZqQx zi22NBp~a)RZu<9*s*!QpeL~I#_)^M~wqT)`b`74sX~C2912o+>C9B3AFSx-HS`=?h zp=ur3VLvpyP#N~c>)!WR-od@qkJb%dugoV{xxT~avv&}CdhC;PyEoxo)jIw3+;1O@ z55Z4693)TM#EGzSeIlqETZfbl1sEiGKh8UI9Z3`kDA`D~f>$z(BH>2obn>yOOjigz zDlwvz@2_Bi@>LjoH)EO%fp#VCOuy|ugaE3}% zu9%}YewCKdWK${n8h1|0VTMToieyVnOi zQtmq8q=}8epumvf#C8x0cUl2*JqgE1ZMs4Oa3?Q{i0wz;pO+aI46{TC+y;>#*^h=z zVA1P*%1tCg^LvXf%23nNaIC=)Qq6(CA#>DE!;)m=zc6r=L?op12+hvgXo@qnoB)nD zbc!Sd8)HsM4N6b+esmU^U%Dv@PHl59L%xnj1T_n@H`p0%u7|C`=9o%$xuke8b%1?+ zwTO*?B&41-Aug!t%Pz=8c?8>K2X8iH9&e(Kw0{5>e3h6=_sMS(9<60c6C}8`cdr|E z&+|0N6zp#Ogj*thVKW_r5&U zbZ;swy9Jm5&9-#{yPj3~7U{cTRje9L>zl>`W?vf94ZgCQ5Hk8f2Y|o2h=~|M=cio0ViJCxIWSXzAJ@pq}%!o5D(Q_VBfteZEUYhWVh`8(T~Z$X>V5A*pokP9VJq3SdzEAqBg~rkhzLHa~ z(ymsPUB;d8Os`{M*b6%@y_R=a@F}&ss;>(nrv1#j>h#85OxstQ7aciP!pfYMPB>J- zM1s2Y%5MSM_&bNYF1z3D@0}c>+Y$Cc=WUMVpEAv+xnb(dwX;|N^q$8FmsCS7jzgRl zLs_?99)#d(y?Rrp?VK1*+mmYJISv~$GTXA=)I#8SE4#sSeL<&pBJL?)wN59u-)YFu zK6070;%J2*v(|@d`rzzi>OJex`{{BC>f$Q15A?}$LUwenIcrd!?i|gwHluxu|HEol z=9v23)se$kl≧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 PfBunqj?^^%AD{jYVQuCs diff --git a/tests/excel-style-roundtrip.test.js b/tests/excel-style-roundtrip.test.js index e2d295c..d15bd3d 100644 --- a/tests/excel-style-roundtrip.test.js +++ b/tests/excel-style-roundtrip.test.js @@ -148,6 +148,20 @@ describe("Excel style round-trip β€” nodes", () => { expect(reimported.type).toBe(DEFAULTS.NODE.TYPE); expect(reimported.style).toEqual(original.style); }); + + it("a column-less node defaults opacity to fully opaque", () => { + expect(importNode({ "ID": "n1" }).style.opacity).toBe(DEFAULTS.NODE.OPACITY); + }); + + it("Opacity column survives the round-trip and fades the sigma fill", () => { + const original = importNode({ "ID": "n1", "Shape": "circle", "Fill Color": "#403C53", "Opacity": "0.5" }); + const row = exportToRow(original, EXCEL_NODE_PROPERTIES); + + expect(row["Opacity"]).toBe(0.5); + const reimported = importNode(row); + expect(reimported.style.opacity).toBe(0.5); + expect(nodeAttributesFromStyle(reimported.style, reimported.type).color).toBe("#403c5380"); + }); }); describe("Excel style round-trip β€” edges", () => { @@ -227,4 +241,19 @@ describe("Excel style round-trip β€” edges", () => { expect(reimported.type).toBe(DEFAULTS.EDGE.TYPE); expect(reimported.style).toEqual(original.style); }); + + it("a column-less edge defaults opacity to fully opaque", () => { + expect(importEdge({ "Source ID": "A", "Target ID": "B" }).style.opacity).toBe(DEFAULTS.EDGE.OPACITY); + }); + + it("Opacity column survives the round-trip and composes with the color alpha", () => { + const original = importEdge({ "Source ID": "A", "Target ID": "B", "Color": "#403C5390", "Opacity": "0.5" }); + const row = exportToRow(original, EXCEL_EDGE_PROPERTIES); + + expect(row["Opacity"]).toBe(0.5); + const reimported = importEdge(row); + expect(reimported.style.opacity).toBe(0.5); + // 0x90 (144) Γ— 0.5 = 72 = 0x48. + expect(edgeAttributesFromStyle(reimported.style, reimported.type).color).toBe("#403c5348"); + }); }); diff --git a/tests/graph-model.test.js b/tests/graph-model.test.js index d141889..1951098 100644 --- a/tests/graph-model.test.js +++ b/tests/graph-model.test.js @@ -632,6 +632,76 @@ describe("attribute mapping helpers", () => { }); }); +describe("element opacity β†’ color alpha mapping", () => { + const TRANSPARENT = "#00000000"; + + it("node opacity fades the native circle/square fill", () => { + expect(nodeAttributesFromStyle({ fill: "#403c53", opacity: 0.5 }, "circle").color).toBe( + "#403c5380", + ); + expect(nodeAttributesFromStyle({ fill: "#403c53", opacity: 0.5 }, "rect").color).toBe( + "#403c5380", + ); + }); + + it("node opacity fades both the texture fill and the baked-in border", () => { + const attrs = nodeAttributesFromStyle( + { fill: "#403c53", stroke: "#123456", lineWidth: 2, opacity: 0.5, size: 20 }, + "hexagon", + ); + // Texture program: real color lives in fillColor + the SVG image, the + // quad color stays transparent. + expect(attrs.type).toBe("shape"); + expect(attrs.color).toBe(TRANSPARENT); + expect(attrs.fillColor).toBe("#403c5380"); + expect(attrs.borderColor).toBe("#12345680"); + expect(attrs.image).toContain(encodeURIComponent("#403c5380")); + expect(attrs.image).toContain(encodeURIComponent("#12345680")); + }); + + it("node opacity fades the bordered-circle fill and border ring", () => { + const attrs = nodeAttributesFromStyle( + { fill: "#403c53", stroke: "#123456", lineWidth: 2, opacity: 0.5 }, + "circle", + ); + expect(attrs.type).toBe("borderCircle"); + expect(attrs.color).toBe("#403c5380"); + expect(attrs.borderColor).toBe("#12345680"); + }); + + it("edge opacity folds into the stroke color alpha and composes with existing alpha", () => { + // Opaque stroke gains an alpha pair. + expect(edgeAttributesFromStyle({ stroke: "#403c53", opacity: 0.5 }, "line").color).toBe( + "#403c5380", + ); + // A stroke that already carries alpha (the default #403C5390) multiplies: + // 0x90 (144) Γ— 0.5 = 72 = 0x48. + expect(edgeAttributesFromStyle({ stroke: "#403c5390", opacity: 0.5 }, "line").color).toBe( + "#403c5348", + ); + }); + + it("opacity of 1 or absent is the identity β€” no alpha rewrite (backward compatible)", () => { + expect(nodeAttributesFromStyle({ fill: "#403c53" }, "circle").color).toBe("#403c53"); + expect(nodeAttributesFromStyle({ fill: "#403c53", opacity: 1 }, "circle").color).toBe( + "#403c53", + ); + expect(edgeAttributesFromStyle({ stroke: "#403c5390" }, "line").color).toBe("#403c5390"); + expect(edgeAttributesFromStyle({ stroke: "#403c5390", opacity: 1 }, "line").color).toBe( + "#403c5390", + ); + }); + + it("opacity of 0 makes the element fully transparent", () => { + expect(nodeAttributesFromStyle({ fill: "#403c53", opacity: 0 }, "circle").color).toBe( + "#403c5300", + ); + expect(edgeAttributesFromStyle({ stroke: "#403c53", opacity: 0 }, "line").color).toBe( + "#403c5300", + ); + }); +}); + describe("edge marker + halo attribute mapping", () => { // The adapter applies updates via mergeEdgeAttributes, so the full // marker/halo set must be emitted on every type-bearing mapping β€” off diff --git a/tests/numeric-scale-picker.test.js b/tests/numeric-scale-picker.test.js new file mode 100644 index 0000000..72db9fa --- /dev/null +++ b/tests/numeric-scale-picker.test.js @@ -0,0 +1,34 @@ +import { describe, it, expect } from "vitest"; +import { NumericScalePicker } from "../src/utilities/numeric_scale_picker.js"; +import { DEFAULTS } from "../src/config.js"; + +// setDefaultOutputRange is pure logic: it maps the (elementType, propertyName) +// pair to the picker's default output range. The render layer (applyHexOpacity) +// clamps opacity to [0, 1] regardless, so these ranges are UI defaults only. +function rangeFor(elementType, propertyName) { + const picker = new NumericScalePicker({ DEFAULTS }); + picker.elementType = elementType; + picker.propertyName = propertyName; + picker.setDefaultOutputRange(); + return { min: picker.minOutput, max: picker.maxOutput }; +} + +describe("NumericScalePicker.setDefaultOutputRange", () => { + it("maps Node Opacity to a visible-to-opaque range", () => { + expect(rangeFor("nodes", "Node Opacity")).toEqual({ min: 0.2, max: DEFAULTS.NODE.OPACITY }); + }); + + it("maps Edge Opacity to a visible-to-opaque range", () => { + expect(rangeFor("edges", "Edge Opacity")).toEqual({ min: 0.2, max: DEFAULTS.EDGE.OPACITY }); + }); + + it("keeps the existing size ranges intact", () => { + expect(rangeFor("nodes", "Node Size")).toEqual({ min: DEFAULTS.NODE.SIZE, max: 50 }); + expect(rangeFor("edges", "Edge Width")).toEqual({ min: DEFAULTS.EDGE.LINE_WIDTH, max: 10 }); + }); + + it("falls back to the 0–100 default for unknown properties", () => { + expect(rangeFor("nodes", "Unknown")).toEqual({ min: 0, max: 100 }); + expect(rangeFor("edges", "Unknown")).toEqual({ min: 0, max: 100 }); + }); +}); From 740e0449324667ab4bfdb502b41d873d95b73142 Mon Sep 17 00:00:00 2001 From: Mnikley Date: Fri, 10 Jul 2026 10:01:24 +0200 Subject: [PATCH 2/2] fix(export): paint bubble-set hulls above nodes/edges to match live z-order PNG (toDataURL) and SVG (collectSvgScene) exports painted bubble-set hulls beneath the node/edge mesh, but the live view has drawn them on top (afterLayer: "nodes") since 1.15.1. On dense graphs the edges buried the hulls and also read as more opaque than on screen. Both exporters now composite hulls above nodes/edges and below labels. Sparse graphs were unaffected, so this only showed on large graphs. Added an SVG z-order regression test (node < body < label). --- CHANGELOG.md | 4 ++++ src/graph/export_svg.js | 7 ++++--- src/graph/sigma_adapter.js | 11 +++++++++-- tests/export-svg.test.js | 20 ++++++++++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fa8d1f..98f3658 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Saved graph files load unchanged; older files (and files saved by earlier versio * **Node and edge opacity.** The styling panel (under 🎨) now has an **Opacity** slider for both nodes and edges (1 = opaque, 0 = invisible). It folds into the element's color alpha, so it composes with a color that already carries transparency, and β€” like the other numeric style knobs β€” it can be **mapped by data** via the ∿ button to scale opacity from a numeric property. Opacity round-trips through both JSON and the Excel `Opacity` column (documented in the template's readme tab). Being an alpha effect, low opacity composites toward the background: light colors (e.g. the default slate edges) approach white sooner than saturated ones. +### Fixes + +* **Exported images now match the on-screen bubble-group z-order.** PNG and SVG exports painted bubble-set hulls *underneath* the nodes and edges, but the live view draws them on top (since 1.15.1). On dense graphs the edge mesh buried the hulls, which also made edges read as more opaque than on screen. Both exporters now composite hulls above nodes/edges and below labels, matching the live layer. Sparse graphs were unaffected, so this was only visible on large graphs. + ## 1.15.4 β€” 2026-07-01 Saved graph files load unchanged; older files default the two new per-view settings to the previous behavior (OR, non-strict). diff --git a/src/graph/export_svg.js b/src/graph/export_svg.js index f918a39..9dbef1b 100644 --- a/src/graph/export_svg.js +++ b/src/graph/export_svg.js @@ -658,8 +658,9 @@ function bubbleLabelPrimitives({ points, opts = {}, defaults = {} }, measureText /** * Collect the full scene as a flat array of typed primitives, in draw order: - * bubble bodies β†’ edges β†’ nodes β†’ edge labels β†’ node labels (+badges) β†’ - * bubble labels. (The background rect is the serializer's job.) + * edges β†’ nodes β†’ bubble bodies β†’ edge labels β†’ node labels (+badges) β†’ + * bubble labels. Bubble bodies sit above nodes/edges to match the live layer + * (afterLayer:"nodes"), below labels. (The background rect is the serializer's job.) * * @param {object} args same shape as buildGraphSvg (background unused here) * @returns {Array} primitives ({kind: "rect"|"circle"|"path"| @@ -672,9 +673,9 @@ function collectSvgScene({ sigma, graph, bubbleGroups = [], measureText }) { const sortedNodes = sortByZIndex([...nodes.values()]); const prims = []; - for (const group of bubbleGroups) prims.push(...bubbleBodyPrimitives(group)); for (const edge of edges) prims.push(...edgePrimitives(edge, sigma)); for (const node of sortedNodes) prims.push(...nodePrimitives(node)); + for (const group of bubbleGroups) prims.push(...bubbleBodyPrimitives(group)); if (env.renderEdgeLabels) { for (const edge of edges) { if (env.edgeLabelSet && !env.edgeLabelSet.has(edge.id)) continue; diff --git a/src/graph/sigma_adapter.js b/src/graph/sigma_adapter.js index 555e5b8..7f070f9 100644 --- a/src/graph/sigma_adapter.js +++ b/src/graph/sigma_adapter.js @@ -900,7 +900,8 @@ class SigmaAdapter { * scene on a temp renderer, which only carries sigma's own layers, so the * bubble-set hulls and group labels are re-painted here at the export * resolution (BubbleSetLayer.drawExport*) in their on-screen z-order: the - * body/outline UNDER the sigma image, the group labels OVER it. An opaque + * body/outline OVER the sigma image (the live layer is afterLayer:"nodes", + * i.e. above nodes/edges), then the group labels OVER that. An opaque * stage-background fill goes down first (export-image renders sigma * transparent). The minimap is a viewport control and stays out of exports. * @@ -958,8 +959,14 @@ class SigmaAdapter { // scaled exports blurry, and their zoom-bucketed outlines drift from // the freshly rendered nodes after a zoom). const bubbleGroups = this.bubbleLayer.exportOutlines(); - this.bubbleLayer.drawExportBodies(ctx, bubbleGroups, dpr * appliedScale); ctx.drawImage(sigmaImage, 0, 0); + // Bodies/outlines sit ABOVE nodes/edges on screen (afterLayer:"nodes"), + // so composite them over the sigma image, not under it β€” under-painting + // let a dense edge mesh bury the hulls and left edges looking un-softened. + // ponytail: the flattened sigma image also carries node labels, so a body + // (0.25 fill) faintly tints labels it overlaps; a separate label-only + // render pass would fix it β€” not worth it (dense graphs hide labels). + this.bubbleLayer.drawExportBodies(ctx, bubbleGroups, dpr * appliedScale); // Group labels sit above sigma's node labels on screen (their own canvas // at afterLayer "labels"); composite them last to keep that z-order. this.bubbleLayer.drawExportLabels(ctx, bubbleGroups, dpr * appliedScale); diff --git a/tests/export-svg.test.js b/tests/export-svg.test.js index 95c5795..06f66ae 100644 --- a/tests/export-svg.test.js +++ b/tests/export-svg.test.js @@ -564,6 +564,26 @@ describe("bubble groups", () => { expect(svg).toContain(' { + const scene = makeScene({ + nodes: [{ id: "a", x: 1, y: 1, label: "NodeLbl" }], + edges: [], + }); + + const svg = build(scene, { + bubbleGroups: [ + { group: "g", points: SQUARE, opts: { fill: "#e74c3c", stroke: "#111111" }, defaults: {} }, + ], + }); + + const nodeAt = svg.indexOf("NodeLbl"); + expect(nodeAt).toBeGreaterThanOrEqual(0); + expect(nodeAt).toBeLessThan(bodyAt); + expect(bodyAt).toBeLessThan(labelAt); + }); + it("skips groups with fewer than 2 outline points", () => { const scene = makeScene();