Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
# 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.

### 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).
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
6 changes: 3 additions & 3 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -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
},
Expand All @@ -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).
Expand Down
7 changes: 4 additions & 3 deletions src/graph/export_svg.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<object>} primitives ({kind: "rect"|"circle"|"path"|
Expand All @@ -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;
Expand Down
13 changes: 8 additions & 5 deletions src/graph/graph_model.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
11 changes: 9 additions & 2 deletions src/graph/sigma_adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/graph/style.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? [],
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading