Skip to content

Commit 527ab20

Browse files
lastboyArik Levin
andauthored
Add Tooltip Support for Top Label Sections (#30)
Co-authored-by: Arik Levin <arik.levin@aquaseq.com>
1 parent 6f3dafc commit 527ab20

File tree

10 files changed

+267
-56
lines changed

10 files changed

+267
-56
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## [1.1.2] - 2024-12-10
4+
5+
## Added
6+
- Tooltip support for top label sections, now configurable as a general setting. This feature can be enabled or disabled using the tooltipLabel property.
7+
* Formatting Support: The tooltip for top labels supports the standard format configuration for customizing display values.
8+
* Callback for Custom Behavior: A new tooltipLabel callback is available to override the default behavior, providing full control over tooltip content and interactions.
9+
310
## [1.1.1] - 2024-12-08
411

512
## Fixed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ graph.draw();
158158

159159
- **tooltip**
160160
Callback function for tooltip events, overriding the default implementation.
161+
`(event, { label, value, x, y }) => {}`
162+
163+
- **tooltipLabel**
164+
Callback function for tooltip events, overriding the default implementation.
165+
`(event, { label, value, x, y, sectionDetails }) => {}`
161166

162167
- **Signature**:
163168
`(event, { label, value }) => {}`
@@ -190,7 +195,10 @@ graph.draw();
190195
Whether details should be displayed (`true | false`).
191196

192197
- **tooltip**
193-
Whether the tooltip should be displayed (`true | false`).
198+
Whether the tooltip should be displayed (`(default) true | false`).
199+
200+
- **tooltipLabel**
201+
Whether the tooltip label should be displayed (`true | false (default)`).
194202

195203
**Note:** Tooltip display depends on the details display for range calculations according to the dividers.
196204

dist/js/funnel-graph.js

Lines changed: 131 additions & 27 deletions
Large diffs are not rendered by default.

dist/js/funnel-graph.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/example.html

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,22 +120,17 @@
120120
label: (event, opts) => {
121121
// label handler logic
122122
console.log("label clicked", opts);
123-
}
123+
},
124+
// tooltipLabel: (event, opts) => {
125+
// console.log(opts)
126+
// }
124127
},
125128
// example for overriding the lanels
126129
// format: {
127130
// value: (opt) => {
128-
// if (opt.index === 1) {
129-
// return 1;
130-
// }
131-
132131
// return formatLargeNumber(opt.value);
133132
// },
134133
// tooltip: (opt) => {
135-
// if (opt.index === 1) {
136-
// return 1;
137-
// }
138-
139134
// return formatLargeNumber(opt.value);
140135
// }
141136
// }
@@ -164,6 +159,8 @@
164159
{ id: 'gradientToggleDirection', text: 'Gradient Toggle Direction', action: () => graph.gradientToggleDirection() },
165160
{ id: 'hideTooltip', text: 'Hide Tooltip', action: () => graph.updateData({ tooltip: false }) },
166161
{ id: 'showTooltip', text: 'Show Tooltip', action: () => graph.updateData({ tooltip: true }) },
162+
{ id: 'hideTooltipLabel', text: 'Hide Tooltip Lbl', action: () => graph.updateData({ tooltipLabel: false }) },
163+
{ id: 'showTooltipLabel', text: 'Show Tooltip Lbl', action: () => graph.updateData({ tooltipLabel: true }) },
167164
{ id: 'hideDetails', text: 'Hide Details', action: () => graph.updateData({ width: 600, height: 400, details: false, margin: { top: 0, right: 0, bottom: 0, left: 0, text: 0 } }) },
168165
{ id: 'showDetails', text: 'Show Details', action: () => graph.updateData({ width: 600, height: 400, details: true, margin: { top: 100, right: 80, bottom: 80, left: 80, text: 20 } }) },
169166
{ id: 'setResize', text: 'Toggle Resize', action: () => { resize = !resize; graph.updateData({ resize }); } },

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "d3-funnel-graph",
3-
"version": "1.1.1",
3+
"version": "1.1.2",
44
"description": "SVG Funnel Graph Javascript Library",
55
"main": "dist/js/funnel-graph.min.js",
66
"style": "dist/funnel-graph.min.css",
@@ -19,10 +19,10 @@
1919
"url": "https://github.com/lastboy/funnel-graph-js.git"
2020
},
2121
"bugs": {
22-
"url": "https://github.com/lastboy/funnel-graph-js.git/issues"
22+
"url": "https://github.com/lastboy/funnel-graph-js/issues"
2323
},
24-
"homepage": "https://github.com/lastboy/funnel-graph-js.git#readme",
25-
"changelog": "https://github.com/lastboy/funnel-graph-js.git/releases",
24+
"homepage": "https://github.com/lastboy/funnel-graph-js/#readme",
25+
"changelog": "https://github.com/lastboy/funnel-graph-js/blob/master/CHANGELOG.md",
2626
"keywords": [
2727
"d3",
2828
"funnel",

src/js/d3-handlers.js

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,14 @@ export const addMouseEventIfNotExists = ({ context, updateLinePositions }) => (p
3535
tooltipTimeout = timeout(() => {
3636

3737
const path = select(this);
38+
const isMouseOnTooltip = handlerMetadata?.mouseOnTooltip;
39+
const isMouseOnTooltipLabel = handlerMetadata?.mouseOnTooltipLabel;
3840

39-
if (context.showTooltip() && path && tooltipElement) {
41+
const showTooltip = (isMouseOnTooltip && context.showTooltip()) || (isMouseOnTooltipLabel && context.showTooltipLabel())
42+
43+
if (showTooltip && path && tooltipElement) {
44+
45+
const { index = -1 } = metadata;
4046

4147
// get the mouse point
4248
const [x, y] = pointer(e, path);
@@ -47,8 +53,9 @@ export const addMouseEventIfNotExists = ({ context, updateLinePositions }) => (p
4753
label = is2d ? handlerMetadata.subLabel || label : label;
4854
const value = handlerMetadata.value;
4955

56+
const sectionDetails = handlerMetadata?.sectionsDetails?.[index];
5057
if (tooltipHandler) {
51-
tooltipHandler(e, { label, value, x, y })
58+
tooltipHandler(e, { label, value, x, y, sectionDetails });
5259
} else {
5360

5461
const format = context.getFormat();
@@ -57,15 +64,26 @@ export const addMouseEventIfNotExists = ({ context, updateLinePositions }) => (p
5764
labelFormatCallback = format.tooltip;
5865
}
5966

60-
const tooltipText = `${label}: ${labelFormatCallback(handlerMetadata)}`;
67+
let tooltipText = `<div>${label}: ${labelFormatCallback(handlerMetadata)}</div>`;
68+
if (sectionDetails) {
69+
tooltipText = sectionDetails
70+
.map((item) => `<div><strong>${item?.name}</strong>: ${labelFormatCallback({ ...handlerMetadata, value: item?.value })}</div>`)
71+
.join("");
72+
}
73+
6174
tooltipElement
6275
// TODO: when exceeding the document area - move the tooltip up/down or left/right
6376
// according to the position (e.g. top /right window eƒxceeded or right)
6477
.style("left", (clickPoint.x + 10) + "px")
6578
.style("top", (clickPoint.y + 10) + "px")
66-
.text(tooltipText)
79+
.style("display", "flex")
80+
.style("align-items", sectionDetails ? "start" : "center")
81+
.style("flex-direction", "column")
82+
.style("height", "auto")
83+
.style("gap", "10px")
84+
.style("padding", "4px")
85+
.html(tooltipText)
6786
.style("opacity", "1")
68-
.style("display", "flex");
6987
}
7088
}
7189
}, 500);
@@ -143,13 +161,20 @@ export const mouseInfoHandler = ({ context, clickHandler, metadata, tooltip, upd
143161
const dataInfoValues = dataInfoItem?.values || [];
144162
const dataInfoLabels = dataInfoItem?.labels || [];
145163
const dataInfoSubLabels = dataInfoItem?.subLabels || [];
164+
const sectionsDetails = dataInfoItem?.sectionsDetails;
165+
const mouseOnTooltipLabel = dataInfoItem?.mouseOnTooltipLabel;
166+
const mouseOnTooltip = dataInfoItem?.mouseOnTooltip;
167+
146168
const index = metadata.hasOwnProperty("index") ? metadata.index : -1;
147169

148170
dataInfoItemForArea = {
149171
value: dataInfoValues?.[areaIndex],
150172
label: dataInfoLabels?.[areaIndex],
151173
subLabel: dataInfoSubLabels?.[index],
152-
sectionIndex: areaIndex
174+
sectionIndex: areaIndex,
175+
sectionsDetails,
176+
mouseOnTooltipLabel,
177+
mouseOnTooltip
153178
}
154179

155180
metadata = {

src/js/d3.js

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -203,17 +203,31 @@ const onEachPathHandler = ({ context }) => function (d, i, nodes) {
203203
const onEachPathCallbacksHandler = ({ context }) => function (d, i, nodes) {
204204

205205
const callbacks = context.getCallBacks();
206-
const d3Path = select(nodes[i]);
206+
const d3Element = select(nodes[i]);
207207

208208
const addMouseHandler = addMouseEventIfNotExists({ context, updateLinePositions });
209209
addMouseHandler(
210-
d3Path,
210+
d3Element,
211211
(typeof callbacks?.click === 'function') ? callbacks.click : undefined,
212212
(typeof callbacks?.tooltip === 'function') ? callbacks.tooltip : undefined,
213213
{ index: i }
214214
);
215215
};
216216

217+
const onEachGroupLabelsCallbacksHandler = ({ context }) => function (d, i, nodes) {
218+
219+
const callbacks = context.getCallBacks();
220+
const d3Element = select(nodes[i]);
221+
222+
const addMouseHandler = addMouseEventIfNotExists({ context, updateLinePositions });
223+
addMouseHandler(
224+
d3Element,
225+
null,
226+
(typeof callbacks?.tooltipLabel === 'function') ? callbacks.tooltipLabel : undefined,
227+
{ index: i }
228+
);
229+
};
230+
217231
/**
218232
* Get the data nfo for each path
219233
*/
@@ -229,7 +243,29 @@ const getDataInfo = ({ context }) => (d, i) => {
229243
const infoItemLabels = data.labels || [];
230244
const infoItemSubLabels = data?.subLabels || [];
231245

232-
return `{ "values": ${JSON.stringify(infoItemValues)}, "labels": ${JSON.stringify(infoItemLabels)}, "subLabels": ${JSON.stringify(infoItemSubLabels)} }`;
246+
return `{ "mouseOnTooltip": true, "values": ${JSON.stringify(infoItemValues)}, "labels": ${JSON.stringify(infoItemLabels)}, "subLabels": ${JSON.stringify(infoItemSubLabels)} }`;
247+
}
248+
249+
/**
250+
* Get the data nfo for each path
251+
*/
252+
const getGroupLabelDataInfo = ({ context }) => (d, i) => {
253+
254+
const is2d = context.is2d();
255+
const data = {
256+
values: context.getValues(),
257+
labels: context.getLabels(),
258+
subLabels: context.getSubLabels()
259+
};
260+
261+
const infoItemValues = data.values.map(item => is2d ? item.reduce((acc, current) => acc + current, 0) : item ) || [];
262+
const infoItemLabels = data.labels || [];
263+
264+
const sectionDetailsAvailable = (data.subLabels?.length && is2d);
265+
const sectionsDetailsObject = sectionDetailsAvailable ? data.values.map(arr => arr.map( (nestedArr, index) => ( { value: nestedArr, name: data.subLabels?.[index] || "NA" } ))) : undefined;
266+
const sectionsDetails = sectionDetailsAvailable ? `, "sectionsDetails": ${JSON.stringify(sectionsDetailsObject)}` : "";
267+
268+
return `{ "mouseOnTooltipLabel": true ,"values": ${JSON.stringify(infoItemValues)}, "labels": ${JSON.stringify(infoItemLabels)} ${sectionsDetails} }`;
233269
}
234270

235271
/**
@@ -390,13 +426,17 @@ const drawInfo = ({
390426
labelFormatCallback = format.value;
391427
}
392428

429+
const groupLabelsCallbackHandler = onEachGroupLabelsCallbacksHandler({ context });
430+
const getDataInfoHandler = getGroupLabelDataInfo({ context });
431+
393432
getInfoSvgGroup(id, margin).selectAll('g.label__group')
394433
.data(info)
395434
.join(
396435
enter => {
397436

398437
return enter.append("g")
399438
.attr("class", "label__group")
439+
.attr('data-info', getDataInfoHandler)
400440
.each(function (d, i) {
401441
const x = !vertical ? calcTextPos(i) : margin.text.left;
402442
const y = !vertical ? margin.text.top : calcTextPos(i);
@@ -430,10 +470,20 @@ const drawInfo = ({
430470

431471
removeClickEvent(g);
432472
addGroupLabelHandler(g, i);
473+
433474
})
475+
.transition()
476+
.duration(400)
477+
.on("end", function (d, i, nodes) {
478+
const pathElement = select(this);
479+
pathElement.style("pointer-events", "all");
480+
groupLabelsCallbackHandler(d, i, nodes);
481+
});
434482
},
435483

436-
update => update.each(function (d, i) {
484+
update => update
485+
.attr('data-info', getDataInfoHandler)
486+
.each(function (d, i) {
437487

438488
const x = !vertical ? calcTextPos(i) : margin.text.left;
439489
const y = !vertical ? margin.text.top : calcTextPos(i);
@@ -470,9 +520,15 @@ const drawInfo = ({
470520

471521
removeClickEvent(g);
472522
addGroupLabelHandler(g, i);
473-
474-
}),
475-
exit => exit
523+
})
524+
.transition()
525+
.duration(400)
526+
.on("end", function (d, i, nodes) {
527+
const pathElement = select(this);
528+
pathElement.style("pointer-events", "all");
529+
groupLabelsCallbackHandler(d, i, nodes);
530+
})
531+
,exit => exit
476532
.each(function () {
477533
const g = select(this);
478534
removeClickEvent(g);

src/js/logger.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
*/
1111
export const getLogger = ({ module }) => {
1212

13-
const projectName = "[Funnel Graph JS]";
13+
const projectName = "[D3 Funnel Graph]";
1414
const _style = "background: #007acc; color: white; padding: 2px 4px; border-radius: 3px";
1515
const getColorStyle = (method) => {
1616
switch (method) {

src/js/main.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ const logger = getLogger({ module: "Main" });
3838
* 'tooltip': (event, { label, value }) => {},
3939
* -- top label handler
4040
* label: (event, { index, value, label, subLabel, sectionIndex }) => {}
41+
* tooltipLabel: (event, { value, label, sectionIndex }) => {}
4142
* },
4243
*
4344
* format: {
@@ -50,6 +51,9 @@ const logger = getLogger({ module: "Main" });
5051
* -- display the OOTB tooltip - on / off
5152
* tooltip: true,
5253
*
54+
* -- display the OOTB tooltip label - on / off
55+
* tooltipLabel: false,
56+
*
5357
* -- remove the text - only graph will be display
5458
* details: false,
5559
*
@@ -81,6 +85,7 @@ class FunnelGraph {
8185

8286
this.setDetails(options.hasOwnProperty('details') ? options.details : true);
8387
this.setTooltip(options.hasOwnProperty('tooltip') ? options.tooltip : true);
88+
this.setTooltipLabel(options.hasOwnProperty('tooltipLabel') ? options.tooltip : false);
8489
this.getDirection(options?.direction);
8590
this.setValues(options?.data?.values || []);
8691
this.setLabels(options?.data?.labels || []);
@@ -140,6 +145,10 @@ class FunnelGraph {
140145
return this.tooltip;
141146
}
142147

148+
showTooltipLabel() {
149+
return this.tooltipLabel;
150+
}
151+
143152
showDetails() {
144153
return this.details;
145154
}
@@ -345,6 +354,10 @@ class FunnelGraph {
345354
this.tooltip = bool;
346355
}
347356

357+
setTooltipLabel(bool) {
358+
this.tooltipLabel = bool;
359+
}
360+
348361
setDetails(bool) {
349362
this.details = bool;
350363
}
@@ -625,6 +638,7 @@ class FunnelGraph {
625638
{ key: "margin", fn: (arg) => this.setMargin(arg) },
626639
{ key: "details", fn: (arg) => this.setDetails(arg) },
627640
{ key: "tooltip", fn: (arg) => this.setTooltip(arg) },
641+
{ key: "tooltipLabel", fn: (arg) => this.setTooltipLabel(arg) },
628642
{ key: "values", fn: (arg) => this.setValues(arg) },
629643
{ key: "labels", fn: (arg) => this.setLabels(arg) },
630644
{ key: "subLabels", fn: (arg) => this.setSubLabels(arg) },

0 commit comments

Comments
 (0)