Skip to content

Commit 739ca32

Browse files
committed
feat(ui): add fine-grained bubble-set label controls with tabbed UI
- Add 7 new per-group properties: labelFontSize, labelCloseToPath, labelAutoRotate, labelOffsetX, labelOffsetY, labelPlacement, labelBackground (was missing from defaults) - Replace 4 stacked sub-cards with compact tabbed interface colored by each group's fill color - Decouple labelFill from background toggle for independent control - Refactor updateBubbleSet to spread full style object (future-proof) - Merge new defaults into old JSON files per-group for backward compat - Add data-property attributes to color inputs and slider containers for reliable sync on layout switch - Disable entire Bubble Sets card when no groups are active
1 parent b7921ac commit 739ca32

5 files changed

Lines changed: 296 additions & 71 deletions

File tree

src/config.js

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,51 +48,79 @@ const DEFAULTS = {
4848
stroke: '#C33D35',
4949
strokeOpacity: 1,
5050
virtualEdges: true,
51+
label: true,
52+
labelText: 'group one',
5153
labelFill: '#fff',
54+
labelFontSize: 12,
5255
labelPadding: 2,
56+
labelBackground: true,
5357
labelBackgroundFill: '#403C53',
5458
labelBackgroundRadius: 5,
55-
label: true,
56-
labelText: 'group one'
59+
labelCloseToPath: true,
60+
labelAutoRotate: true,
61+
labelOffsetX: 0,
62+
labelOffsetY: 0,
63+
labelPlacement: 'bottom',
5764
},
5865
"groupTwo": {
5966
fill: '#c33d35',
6067
fillOpacity: 0.25,
6168
stroke: '#403c53',
6269
strokeOpacity: 1,
6370
virtualEdges: true,
71+
label: true,
72+
labelText: 'group two',
6473
labelFill: '#fff',
74+
labelFontSize: 12,
6575
labelPadding: 2,
76+
labelBackground: true,
6677
labelBackgroundFill: '#c33d35',
6778
labelBackgroundRadius: 5,
68-
label: true,
69-
labelText: 'group two'
79+
labelCloseToPath: true,
80+
labelAutoRotate: true,
81+
labelOffsetX: 0,
82+
labelOffsetY: 0,
83+
labelPlacement: 'bottom',
7084
},
7185
"groupThree": {
7286
fill: '#EFB0AA',
7387
fillOpacity: 0.4,
7488
stroke: '#8CA6D9',
7589
strokeOpacity: 1,
7690
virtualEdges: true,
91+
label: true,
92+
labelText: 'group three',
7793
labelFill: '#fff',
94+
labelFontSize: 12,
7895
labelPadding: 2,
96+
labelBackground: true,
7997
labelBackgroundFill: '#EFB0AA',
8098
labelBackgroundRadius: 5,
81-
label: true,
82-
labelText: 'group three'
99+
labelCloseToPath: true,
100+
labelAutoRotate: true,
101+
labelOffsetX: 0,
102+
labelOffsetY: 0,
103+
labelPlacement: 'bottom',
83104
},
84105
"groupFour": {
85106
fill: '#8CA6D9',
86107
fillOpacity: 0.4,
87108
stroke: '#EFB0AA',
88109
strokeOpacity: 1,
89110
virtualEdges: true,
111+
label: true,
112+
labelText: 'group four',
90113
labelFill: '#fff',
114+
labelFontSize: 12,
91115
labelPadding: 2,
116+
labelBackground: true,
92117
labelBackgroundFill: '#8CA6D9',
93118
labelBackgroundRadius: 5,
94-
label: true,
95-
labelText: 'group four'
119+
labelCloseToPath: true,
120+
labelAutoRotate: true,
121+
labelOffsetX: 0,
122+
labelOffsetY: 0,
123+
labelPlacement: 'bottom',
96124
},
97125
},
98126
BUBBLE_GROUP_QUADRANT_POSITIONS: {

src/graph/bubble_sets.js

Lines changed: 73 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -62,20 +62,38 @@ class GraphBubbleSetManager {
6262
break;
6363
case "Label Background":
6464
bStyle.labelBackground = value;
65-
if (!value) {
66-
bStyle.labelFill = "#000000";
67-
} else {
68-
bStyle.labelFill = "#FFFFFF";
69-
}
65+
break;
66+
case "Label Fill Color":
67+
bStyle.labelFill = value;
68+
break;
69+
case "Label Font Size":
70+
bStyle.labelFontSize = value;
71+
break;
72+
case "Label Close To Path":
73+
bStyle.labelCloseToPath = value;
74+
break;
75+
case "Label Auto Rotate":
76+
bStyle.labelAutoRotate = value;
77+
break;
78+
case "Label Offset X":
79+
bStyle.labelOffsetX = value;
80+
break;
81+
case "Label Offset Y":
82+
bStyle.labelOffsetY = value;
83+
break;
84+
case "Label Placement":
85+
bStyle.labelPlacement = value;
7086
break;
7187
default:
7288
break;
7389
}
74-
await this.cache.INSTANCES.BUBBLE_GROUPS[group].update(bStyle);
90+
await this.cache.INSTANCES.BUBBLE_GROUPS[group].update({ ...bStyle });
7591
await this.cache.gcm.decideToRenderOrDraw(true);
92+
this.refreshBubbleStyleElements();
7693
}
7794

7895
refreshBubbleStyleElements() {
96+
let anyGroupActive = false;
7997
for (const group of this.traverseBubbleSets()) {
8098
const currentLayout = this.cache.data.layouts[this.cache.data.selectedLayout];
8199
const bubbleStyle = currentLayout.bubbleSetStyle[group];
@@ -105,6 +123,7 @@ class GraphBubbleSetManager {
105123
}
106124

107125
const hasActiveMembers = actualMembers.size > 0;
126+
if (hasActiveMembers) anyGroupActive = true;
108127
const labelConfigShouldBeEnabled = bubbleStyle.label;
109128

110129
// toggle entire cards based on bubble group members
@@ -117,25 +136,61 @@ class GraphBubbleSetManager {
117136
labelInput.value = bubbleStyle.labelText;
118137
}
119138

120-
// Update color pickers (fill, stroke, label background)
121-
const fillColorInput = document.querySelector(`input[data-property="Bubble Set ${group} Fill Color"]`);
122-
if (fillColorInput && bubbleStyle.fill) {
123-
fillColorInput.value = bubbleStyle.fill;
124-
}
125-
126-
const strokeColorInput = document.querySelector(`input[data-property="Bubble Set ${group} Stroke Color"]`);
127-
if (strokeColorInput && bubbleStyle.stroke) {
128-
strokeColorInput.value = bubbleStyle.stroke;
129-
}
139+
// Sync color inputs by data-property attribute
140+
const syncColorInput = (prop, val) => {
141+
const el = card.querySelector(`input[data-property="Bubble Set ${group} ${prop}"]`);
142+
if (el && val != null) el.value = val;
143+
};
144+
syncColorInput("Fill Color", bubbleStyle.fill);
145+
syncColorInput("Stroke Color", bubbleStyle.stroke);
146+
syncColorInput("Label Background Color", bubbleStyle.labelBackgroundFill);
147+
syncColorInput("Label Fill Color", bubbleStyle.labelFill);
148+
149+
// Sync slider inputs by data-property attribute
150+
const syncSliderInput = (prop, val) => {
151+
const container = card.querySelector(`[data-property="Bubble Set ${group} ${prop}"]`);
152+
if (!container || val === undefined) return;
153+
const slider = container.querySelector('input[type="range"]');
154+
const numInput = container.querySelector('input[type="number"]');
155+
if (slider) slider.value = val;
156+
if (numInput) numInput.value = val;
157+
};
158+
syncSliderInput("Label Font Size", bubbleStyle.labelFontSize);
159+
syncSliderInput("Label Offset X", bubbleStyle.labelOffsetX);
160+
syncSliderInput("Label Offset Y", bubbleStyle.labelOffsetY);
161+
162+
// Sync switch inputs by data-property attribute
163+
const syncSwitch = (prop, val) => {
164+
const el = card.querySelector(`[data-property="Bubble Set ${group} ${prop}"]`);
165+
if (el && el.setChecked) el.setChecked(!!val);
166+
};
167+
syncSwitch("Label Close To Path", bubbleStyle.labelCloseToPath);
168+
syncSwitch("Label Auto Rotate", bubbleStyle.labelAutoRotate);
169+
170+
// Sync dropdown by data-property attribute
171+
const placementDropdown = card.querySelector(`[data-property="Bubble Set ${group} Label Placement"]`);
172+
if (placementDropdown && bubbleStyle.labelPlacement) placementDropdown.value = bubbleStyle.labelPlacement;
130173

131174
// toggle label-related properties
132175
for (const elem of card.querySelectorAll(".bubbleSetOptionalLabelConfig")) {
133176
labelConfigShouldBeEnabled ? elem.classList.remove("disabled") : elem.classList.add("disabled");
134177
}
135178

136-
// override css properties to style round-button quadrants
179+
// override css properties to style round-button quadrants and tabs
137180
const fillColor = bubbleStyle.fill || this.cache.DEFAULTS.BUBBLE_GROUP_STYLE[group].fill;
138181
document.documentElement.style.setProperty(`--${group}-color`, fillColor);
182+
183+
const tab = document.querySelector(`.bubble-set-tab[data-group="${group}"]`);
184+
if (tab) {
185+
tab.style.setProperty("--tab-color", fillColor);
186+
tab.style.setProperty("--tab-text-color", StaticUtilities.getReadableForegroundColor(fillColor));
187+
}
188+
}
189+
190+
// Toggle the entire bubble sets card when no groups are active
191+
const outerCard = document.querySelector(".bubble-set-config-card-header");
192+
if (outerCard) {
193+
anyGroupActive ? outerCard.classList.remove("disabled") : outerCard.classList.add("disabled");
139194
}
140195
}
141196

@@ -198,18 +253,12 @@ class GraphBubbleSetManager {
198253
const bubbleStyle = this.cache.data.layouts[this.cache.data.selectedLayout].bubbleSetStyle[group];
199254

200255
await this.cache.INSTANCES.BUBBLE_GROUPS[group].update({
256+
...bubbleStyle,
201257
members: empty ? [] : membersAsArray,
202258
avoidMembers: avoidMembers,
203259
fillOpacity: empty ? 0 : bubbleStyle.fillOpacity,
204260
strokeOpacity: empty ? 0 : bubbleStyle.strokeOpacity,
205261
label: empty ? false : bubbleStyle.label,
206-
// Apply all style properties including label text
207-
labelText: bubbleStyle.labelText,
208-
fill: bubbleStyle.fill,
209-
stroke: bubbleStyle.stroke,
210-
labelBackgroundFill: bubbleStyle.labelBackgroundFill,
211-
labelFill: bubbleStyle.labelFill,
212-
labelBackground: bubbleStyle.labelBackground,
213262
});
214263
await this.cache.INSTANCES.BUBBLE_GROUPS[group].drawBubbleSets();
215264
}

src/managers/io.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,9 +1613,16 @@ class IOManager {
16131613
layout.edgeStyles instanceof Map
16141614
? layout.edgeStyles
16151615
: new Map(Object.entries(layout.edgeStyles || {})),
1616-
bubbleSetStyle:
1617-
layout.bubbleSetStyle ||
1618-
structuredClone(this.cache.DEFAULTS.BUBBLE_GROUP_STYLE),
1616+
bubbleSetStyle: (() => {
1617+
const defaults = this.cache.DEFAULTS.BUBBLE_GROUP_STYLE;
1618+
const saved = layout.bubbleSetStyle;
1619+
if (!saved) return structuredClone(defaults);
1620+
const merged = {};
1621+
for (const group of Object.keys(defaults)) {
1622+
merged[group] = { ...defaults[group], ...(saved[group] || {}) };
1623+
}
1624+
return merged;
1625+
})(),
16191626
};
16201627

16211628
for (let group of this.cache.bs.traverseBubbleSets()) {

0 commit comments

Comments
 (0)