smart-labels 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/smartLabels.es.js +119 -76
- package/dist/smartLabels.js +119 -76
- package/dist/smartLabels.min.js +2 -2
- package/package.json +1 -1
- package/src/index.js +118 -75
package/dist/smartLabels.es.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// smart-labels v0.0.
|
|
1
|
+
// smart-labels v0.0.3 Copyright (c) 2024 John Alexis Guerra Gómez
|
|
2
2
|
import * as d3 from 'd3';
|
|
3
3
|
|
|
4
4
|
// Based on https://observablehq.com/@d3/voronoi-labels by Mike Bostock
|
|
@@ -22,6 +22,7 @@ function smartLabels(
|
|
|
22
22
|
{
|
|
23
23
|
x = (d) => d[0], // x coordinate accessor, expected to be in pixels
|
|
24
24
|
y = (d) => d[1], // y coordinate accessor, expected to be in pixels
|
|
25
|
+
r = () => 3, // radius accessor, expected to be in pixels
|
|
25
26
|
label = (d, i) => i, // Accessor for the label
|
|
26
27
|
fill = "#333", // label fill color
|
|
27
28
|
stroke = "white", // label stroke color
|
|
@@ -55,11 +56,16 @@ function smartLabels(
|
|
|
55
56
|
pointsStroke = "#ccc",
|
|
56
57
|
renderer = "svg",
|
|
57
58
|
debug = false,
|
|
59
|
+
selected = null,
|
|
60
|
+
padding = 3, // label padding in pixels
|
|
58
61
|
} = {}
|
|
59
62
|
) {
|
|
60
63
|
data = data.filter(
|
|
61
|
-
(d) =>
|
|
62
|
-
x(d
|
|
64
|
+
(d, index) =>
|
|
65
|
+
x(d, index) !== undefined &&
|
|
66
|
+
x(d, index) !== null &&
|
|
67
|
+
y(d, index) !== undefined &&
|
|
68
|
+
y(d, index) !== null
|
|
63
69
|
);
|
|
64
70
|
|
|
65
71
|
let xExtent = d3.extent(data, x),
|
|
@@ -68,12 +74,37 @@ function smartLabels(
|
|
|
68
74
|
height = height || yExtent[1] - yExtent[0];
|
|
69
75
|
|
|
70
76
|
if (debug) console.log("✅ smartLabels renderer", renderer);
|
|
77
|
+
|
|
78
|
+
function checkIfTargetMatchesRenderer(target, renderer) {
|
|
79
|
+
if (target && target.node) {
|
|
80
|
+
target = target.node();
|
|
81
|
+
}
|
|
82
|
+
if (renderer.toLocaleLowerCase() === "canvas") {
|
|
83
|
+
return target instanceof HTMLCanvasElement;
|
|
84
|
+
} else {
|
|
85
|
+
return target instanceof SVGElement;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!checkIfTargetMatchesRenderer(target, renderer)) {
|
|
90
|
+
if (debug) console.log("❌ smartLabels Target doesn't match the renderer", target, renderer);
|
|
91
|
+
throw new Error("Smartlabels Target doesn't match the renderer", target, renderer);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Try to reuse the target
|
|
71
95
|
if (renderer.toLocaleLowerCase() === "canvas") {
|
|
72
|
-
target
|
|
73
|
-
d3.select(target)
|
|
74
|
-
|
|
96
|
+
if (target) {
|
|
97
|
+
target = d3.select(target.node ? target.node() : target);
|
|
98
|
+
} else {
|
|
99
|
+
target = d3.create("canvas").attr("width", width).attr("height", height);
|
|
100
|
+
}
|
|
75
101
|
useOcclusion = false;
|
|
76
102
|
} else {
|
|
103
|
+
if (target) {
|
|
104
|
+
target = d3.select(target.node ? target.node() : target);
|
|
105
|
+
} else {
|
|
106
|
+
target = d3.create("svg").attr("viewBox", [0, 0, width, height]);
|
|
107
|
+
}
|
|
77
108
|
target = target || d3.create("svg").attr("viewBox", [0, 0, width, height]);
|
|
78
109
|
}
|
|
79
110
|
|
|
@@ -88,16 +119,16 @@ function smartLabels(
|
|
|
88
119
|
let cells = data.map((d, i) => [d, voronoi.cellPolygon(i)]);
|
|
89
120
|
// Replace null cells with the nearest one
|
|
90
121
|
cells = cells
|
|
91
|
-
.map(([d, cell]) => [d, getNearestCell(d, cell)])
|
|
122
|
+
.map(([d, cell], index) => [d, getNearestCell(d, cell, index)])
|
|
92
123
|
.map(([d, cell]) => ({ d, cell, show: -d3.polygonArea(cell) > threshold }));
|
|
93
124
|
|
|
94
125
|
// cells can be null when we have duplicated coords
|
|
95
126
|
// https://github.com/d3/d3-delaunay/issues/106
|
|
96
|
-
function getNearestCell(d, cell) {
|
|
127
|
+
function getNearestCell(d, cell, index) {
|
|
97
128
|
if (!cell) {
|
|
98
|
-
const i = delaunay.find(x(d), y(d));
|
|
129
|
+
const i = delaunay.find(x(d, index), y(d, index));
|
|
99
130
|
if (i === -1) {
|
|
100
|
-
console.log("couldn't find cell", i, d, x(d), y(d));
|
|
131
|
+
console.log("couldn't find cell", i, d, x(d, index), y(d, index));
|
|
101
132
|
return null;
|
|
102
133
|
}
|
|
103
134
|
cell = cells[i][1];
|
|
@@ -128,13 +159,25 @@ function smartLabels(
|
|
|
128
159
|
.attr("stroke", "none");
|
|
129
160
|
|
|
130
161
|
const orient = {
|
|
131
|
-
top: (text) =>
|
|
162
|
+
top: (text) =>
|
|
163
|
+
text
|
|
164
|
+
.attr("text-anchor", "middle")
|
|
165
|
+
.attr("y", (d, i) => -(r(d, i) + padding)),
|
|
132
166
|
right: (text) =>
|
|
133
|
-
text
|
|
167
|
+
text
|
|
168
|
+
.attr("text-anchor", "start")
|
|
169
|
+
.attr("dy", "0.35em")
|
|
170
|
+
.attr("x", (d, i) => r(d, i) + padding),
|
|
134
171
|
bottom: (text) =>
|
|
135
|
-
text
|
|
172
|
+
text
|
|
173
|
+
.attr("text-anchor", "middle")
|
|
174
|
+
.attr("dy", "0.71em")
|
|
175
|
+
.attr("y", (d, i) => r(d, i) + padding),
|
|
136
176
|
left: (text) =>
|
|
137
|
-
text
|
|
177
|
+
text
|
|
178
|
+
.attr("text-anchor", "end")
|
|
179
|
+
.attr("dy", "0.35em")
|
|
180
|
+
.attr("x", (d, i) => -(r(d, i) + padding)),
|
|
138
181
|
};
|
|
139
182
|
|
|
140
183
|
if (showAnchors || labelsInCentroids) {
|
|
@@ -151,8 +194,10 @@ function smartLabels(
|
|
|
151
194
|
.join("path")
|
|
152
195
|
.attr("class", "anchor")
|
|
153
196
|
.attr("display", ({ show }) => (show ? null : "none"))
|
|
154
|
-
.attr("d", ({ d, cell }) =>
|
|
155
|
-
cell
|
|
197
|
+
.attr("d", ({ d, cell }, index) =>
|
|
198
|
+
cell
|
|
199
|
+
? `M${d3.polygonCentroid(cell)}L${x(d, index)},${y(d, index)}`
|
|
200
|
+
: null
|
|
156
201
|
);
|
|
157
202
|
}
|
|
158
203
|
|
|
@@ -195,11 +240,14 @@ function smartLabels(
|
|
|
195
240
|
.data((d) => d)
|
|
196
241
|
.join("text")
|
|
197
242
|
.style("font", font)
|
|
198
|
-
.each(function ({ d, cell }) {
|
|
243
|
+
.each(function ({ d, cell }, index) {
|
|
199
244
|
if (!cell) return;
|
|
200
245
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
201
246
|
const angle =
|
|
202
|
-
(Math.round(
|
|
247
|
+
(Math.round(
|
|
248
|
+
(Math.atan2(cy - y(d, index), cx - x(d, index)) / Math.PI) * 2
|
|
249
|
+
) +
|
|
250
|
+
4) %
|
|
203
251
|
4;
|
|
204
252
|
|
|
205
253
|
d3.select(this).call(
|
|
@@ -212,13 +260,13 @@ function smartLabels(
|
|
|
212
260
|
: orient.left
|
|
213
261
|
);
|
|
214
262
|
})
|
|
215
|
-
.attr("transform", ({ d, cell }) => {
|
|
263
|
+
.attr("transform", ({ d, cell }, index) => {
|
|
216
264
|
if (labelsInCentroids) {
|
|
217
265
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
218
266
|
|
|
219
267
|
return `translate(${cx}, ${cy})`;
|
|
220
268
|
} else {
|
|
221
|
-
return `translate(${x(d)}, ${y(d)})`;
|
|
269
|
+
return `translate(${x(d, index)}, ${y(d, index)})`;
|
|
222
270
|
}
|
|
223
271
|
})
|
|
224
272
|
.attr("display", ({ show }) => (show ? null : "none"))
|
|
@@ -278,20 +326,26 @@ function smartLabels(
|
|
|
278
326
|
}
|
|
279
327
|
} // renderSVG
|
|
280
328
|
|
|
281
|
-
function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
329
|
+
function renderCanvasAnchorsVoronoiPoints(context) {
|
|
330
|
+
if (debug)
|
|
331
|
+
console.log(
|
|
332
|
+
"renderCanvasAnchorsVoronoiPoints",
|
|
333
|
+
"showAnchors",
|
|
334
|
+
showAnchors,
|
|
335
|
+
"labelsInCentroids",
|
|
336
|
+
labelsInCentroids,
|
|
337
|
+
"showVoronoi",
|
|
338
|
+
showVoronoi,
|
|
339
|
+
"showPoints",
|
|
340
|
+
showPoints
|
|
341
|
+
);
|
|
288
342
|
if (showAnchors || labelsInCentroids) {
|
|
289
|
-
cells.forEach(({ d, cell, show }) => {
|
|
343
|
+
cells.forEach(({ d, cell, show }, index) => {
|
|
290
344
|
if (!show) return;
|
|
291
345
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
292
346
|
context.beginPath();
|
|
293
347
|
context.moveTo(cx, cy);
|
|
294
|
-
context.lineTo(x(d), y(d));
|
|
348
|
+
context.lineTo(x(d, index), y(d, index));
|
|
295
349
|
context.strokeStyle = anchorsStroke;
|
|
296
350
|
context.stroke();
|
|
297
351
|
});
|
|
@@ -306,23 +360,40 @@ function smartLabels(
|
|
|
306
360
|
|
|
307
361
|
if (showPoints) {
|
|
308
362
|
context.beginPath();
|
|
309
|
-
delaunay.renderPoints(context, 2);
|
|
363
|
+
// delaunay.renderPoints(context, 2);
|
|
364
|
+
context.save();
|
|
365
|
+
context.globalAlpha = 0.7;
|
|
366
|
+
context.fillStyle = "black";
|
|
367
|
+
data.forEach((d, i) => {
|
|
368
|
+
context.beginPath();
|
|
369
|
+
context.arc(x(d, i), y(d, i), r(d, i), 0, 2 * Math.PI);
|
|
370
|
+
});
|
|
371
|
+
context.restore();
|
|
310
372
|
context.fillStyle = pointsFill;
|
|
311
373
|
context.fill();
|
|
312
374
|
context.strokeStyle = pointsStroke;
|
|
313
375
|
context.stroke();
|
|
314
376
|
}
|
|
377
|
+
} // renderCanvasAnchorsVoronoiPoints
|
|
315
378
|
|
|
316
|
-
|
|
317
|
-
|
|
379
|
+
function renderCanvasLabels(context, selected) {
|
|
380
|
+
cells.forEach(({ d, cell, show }, index) => {
|
|
381
|
+
if (!show && index !== selected) return;
|
|
318
382
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
319
383
|
const angle =
|
|
320
|
-
(Math.round(
|
|
384
|
+
(Math.round(
|
|
385
|
+
(Math.atan2(cy - y(d, index), cx - x(d, index)) / Math.PI) * 2
|
|
386
|
+
) +
|
|
387
|
+
4) %
|
|
388
|
+
4;
|
|
321
389
|
|
|
322
390
|
context.save();
|
|
391
|
+
const dr = r(d, index) + padding;
|
|
392
|
+
const dx = angle === 0 ? dr : angle === 2 ? -dr : dr;
|
|
393
|
+
const dy = angle === 1 ? dr : angle === 3 ? -dr : dr;
|
|
323
394
|
context.translate(
|
|
324
|
-
labelsInCentroids ? cx : x(d),
|
|
325
|
-
labelsInCentroids ? cy : y(d)
|
|
395
|
+
labelsInCentroids ? cx : x(d, index) + dx,
|
|
396
|
+
labelsInCentroids ? cy : y(d, index) + dy
|
|
326
397
|
);
|
|
327
398
|
context.textAlign =
|
|
328
399
|
angle === 0
|
|
@@ -340,16 +411,23 @@ function smartLabels(
|
|
|
340
411
|
: angle === 2
|
|
341
412
|
? "middle"
|
|
342
413
|
: "bottom";
|
|
343
|
-
context.font = font;
|
|
414
|
+
context.font = index === selected ? hoverFont : font;
|
|
344
415
|
context.fillStyle = fill;
|
|
345
416
|
context.strokeStyle = stroke;
|
|
346
417
|
context.lineWidth = strokeWidth;
|
|
347
|
-
|
|
348
|
-
context.
|
|
418
|
+
// if (debug) console.log(index, dx, dy, angle);
|
|
419
|
+
context.strokeText(label(d, index), 0, 0);
|
|
420
|
+
context.fillText(label(d, index), 0, 0);
|
|
349
421
|
context.restore();
|
|
350
422
|
});
|
|
423
|
+
} // renderCanvasLabels
|
|
424
|
+
|
|
425
|
+
function renderCanvas() {
|
|
426
|
+
const context = target.node().getContext("2d");
|
|
427
|
+
|
|
428
|
+
renderCanvasAnchorsVoronoiPoints(context);
|
|
429
|
+
renderCanvasLabels(context, selected);
|
|
351
430
|
|
|
352
|
-
let selected = null;
|
|
353
431
|
if (hover) {
|
|
354
432
|
if (debug) console.log("setting onmousemove", target);
|
|
355
433
|
|
|
@@ -360,44 +438,9 @@ function smartLabels(
|
|
|
360
438
|
selected = i;
|
|
361
439
|
|
|
362
440
|
context.clearRect(0, 0, width, height);
|
|
363
|
-
|
|
364
|
-
if (!show && index !== selected) return;
|
|
365
|
-
const [cx, cy] = d3.polygonCentroid(cell);
|
|
366
|
-
const angle =
|
|
367
|
-
(Math.round((Math.atan2(cy - y(d), cx - x(d)) / Math.PI) * 2) + 4) %
|
|
368
|
-
4;
|
|
369
|
-
|
|
370
|
-
context.save();
|
|
371
|
-
context.translate(
|
|
372
|
-
labelsInCentroids ? cx : x(d),
|
|
373
|
-
labelsInCentroids ? cy : y(d)
|
|
374
|
-
);
|
|
375
|
-
context.textAlign =
|
|
376
|
-
angle === 0
|
|
377
|
-
? "left"
|
|
378
|
-
: angle === 1
|
|
379
|
-
? "center"
|
|
380
|
-
: angle === 2
|
|
381
|
-
? "right"
|
|
382
|
-
: "center";
|
|
383
|
-
context.textBaseline =
|
|
384
|
-
angle === 0
|
|
385
|
-
? "middle"
|
|
386
|
-
: angle === 1
|
|
387
|
-
? "top"
|
|
388
|
-
: angle === 2
|
|
389
|
-
? "middle"
|
|
390
|
-
: "bottom";
|
|
391
|
-
context.font = index === selected ? hoverFont : font;
|
|
392
|
-
context.fillStyle = fill;
|
|
393
|
-
context.strokeStyle = stroke;
|
|
394
|
-
context.lineWidth = strokeWidth;
|
|
395
|
-
context.strokeText(label(d), 0, 0);
|
|
396
|
-
context.fillText(label(d), 0, 0);
|
|
397
|
-
context.restore();
|
|
398
|
-
});
|
|
399
|
-
|
|
441
|
+
renderCanvasAnchorsVoronoiPoints(context);
|
|
400
442
|
if (onHover && typeof onHover === "function") onHover(i);
|
|
443
|
+
renderCanvasLabels(context, selected);
|
|
401
444
|
});
|
|
402
445
|
}
|
|
403
446
|
} // renderCanvas
|
package/dist/smartLabels.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// smart-labels v0.0.
|
|
1
|
+
// smart-labels v0.0.3 Copyright (c) 2024 John Alexis Guerra Gómez
|
|
2
2
|
(function (global, factory) {
|
|
3
3
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('d3')) :
|
|
4
4
|
typeof define === 'function' && define.amd ? define(['d3'], factory) :
|
|
@@ -45,6 +45,7 @@ function smartLabels(
|
|
|
45
45
|
{
|
|
46
46
|
x = (d) => d[0], // x coordinate accessor, expected to be in pixels
|
|
47
47
|
y = (d) => d[1], // y coordinate accessor, expected to be in pixels
|
|
48
|
+
r = () => 3, // radius accessor, expected to be in pixels
|
|
48
49
|
label = (d, i) => i, // Accessor for the label
|
|
49
50
|
fill = "#333", // label fill color
|
|
50
51
|
stroke = "white", // label stroke color
|
|
@@ -78,11 +79,16 @@ function smartLabels(
|
|
|
78
79
|
pointsStroke = "#ccc",
|
|
79
80
|
renderer = "svg",
|
|
80
81
|
debug = false,
|
|
82
|
+
selected = null,
|
|
83
|
+
padding = 3, // label padding in pixels
|
|
81
84
|
} = {}
|
|
82
85
|
) {
|
|
83
86
|
data = data.filter(
|
|
84
|
-
(d) =>
|
|
85
|
-
x(d
|
|
87
|
+
(d, index) =>
|
|
88
|
+
x(d, index) !== undefined &&
|
|
89
|
+
x(d, index) !== null &&
|
|
90
|
+
y(d, index) !== undefined &&
|
|
91
|
+
y(d, index) !== null
|
|
86
92
|
);
|
|
87
93
|
|
|
88
94
|
let xExtent = d3__namespace.extent(data, x),
|
|
@@ -91,12 +97,37 @@ function smartLabels(
|
|
|
91
97
|
height = height || yExtent[1] - yExtent[0];
|
|
92
98
|
|
|
93
99
|
if (debug) console.log("✅ smartLabels renderer", renderer);
|
|
100
|
+
|
|
101
|
+
function checkIfTargetMatchesRenderer(target, renderer) {
|
|
102
|
+
if (target && target.node) {
|
|
103
|
+
target = target.node();
|
|
104
|
+
}
|
|
105
|
+
if (renderer.toLocaleLowerCase() === "canvas") {
|
|
106
|
+
return target instanceof HTMLCanvasElement;
|
|
107
|
+
} else {
|
|
108
|
+
return target instanceof SVGElement;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!checkIfTargetMatchesRenderer(target, renderer)) {
|
|
113
|
+
if (debug) console.log("❌ smartLabels Target doesn't match the renderer", target, renderer);
|
|
114
|
+
throw new Error("Smartlabels Target doesn't match the renderer", target, renderer);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Try to reuse the target
|
|
94
118
|
if (renderer.toLocaleLowerCase() === "canvas") {
|
|
95
|
-
target
|
|
96
|
-
d3__namespace.select(target)
|
|
97
|
-
|
|
119
|
+
if (target) {
|
|
120
|
+
target = d3__namespace.select(target.node ? target.node() : target);
|
|
121
|
+
} else {
|
|
122
|
+
target = d3__namespace.create("canvas").attr("width", width).attr("height", height);
|
|
123
|
+
}
|
|
98
124
|
useOcclusion = false;
|
|
99
125
|
} else {
|
|
126
|
+
if (target) {
|
|
127
|
+
target = d3__namespace.select(target.node ? target.node() : target);
|
|
128
|
+
} else {
|
|
129
|
+
target = d3__namespace.create("svg").attr("viewBox", [0, 0, width, height]);
|
|
130
|
+
}
|
|
100
131
|
target = target || d3__namespace.create("svg").attr("viewBox", [0, 0, width, height]);
|
|
101
132
|
}
|
|
102
133
|
|
|
@@ -111,16 +142,16 @@ function smartLabels(
|
|
|
111
142
|
let cells = data.map((d, i) => [d, voronoi.cellPolygon(i)]);
|
|
112
143
|
// Replace null cells with the nearest one
|
|
113
144
|
cells = cells
|
|
114
|
-
.map(([d, cell]) => [d, getNearestCell(d, cell)])
|
|
145
|
+
.map(([d, cell], index) => [d, getNearestCell(d, cell, index)])
|
|
115
146
|
.map(([d, cell]) => ({ d, cell, show: -d3__namespace.polygonArea(cell) > threshold }));
|
|
116
147
|
|
|
117
148
|
// cells can be null when we have duplicated coords
|
|
118
149
|
// https://github.com/d3/d3-delaunay/issues/106
|
|
119
|
-
function getNearestCell(d, cell) {
|
|
150
|
+
function getNearestCell(d, cell, index) {
|
|
120
151
|
if (!cell) {
|
|
121
|
-
const i = delaunay.find(x(d), y(d));
|
|
152
|
+
const i = delaunay.find(x(d, index), y(d, index));
|
|
122
153
|
if (i === -1) {
|
|
123
|
-
console.log("couldn't find cell", i, d, x(d), y(d));
|
|
154
|
+
console.log("couldn't find cell", i, d, x(d, index), y(d, index));
|
|
124
155
|
return null;
|
|
125
156
|
}
|
|
126
157
|
cell = cells[i][1];
|
|
@@ -151,13 +182,25 @@ function smartLabels(
|
|
|
151
182
|
.attr("stroke", "none");
|
|
152
183
|
|
|
153
184
|
const orient = {
|
|
154
|
-
top: (text) =>
|
|
185
|
+
top: (text) =>
|
|
186
|
+
text
|
|
187
|
+
.attr("text-anchor", "middle")
|
|
188
|
+
.attr("y", (d, i) => -(r(d, i) + padding)),
|
|
155
189
|
right: (text) =>
|
|
156
|
-
text
|
|
190
|
+
text
|
|
191
|
+
.attr("text-anchor", "start")
|
|
192
|
+
.attr("dy", "0.35em")
|
|
193
|
+
.attr("x", (d, i) => r(d, i) + padding),
|
|
157
194
|
bottom: (text) =>
|
|
158
|
-
text
|
|
195
|
+
text
|
|
196
|
+
.attr("text-anchor", "middle")
|
|
197
|
+
.attr("dy", "0.71em")
|
|
198
|
+
.attr("y", (d, i) => r(d, i) + padding),
|
|
159
199
|
left: (text) =>
|
|
160
|
-
text
|
|
200
|
+
text
|
|
201
|
+
.attr("text-anchor", "end")
|
|
202
|
+
.attr("dy", "0.35em")
|
|
203
|
+
.attr("x", (d, i) => -(r(d, i) + padding)),
|
|
161
204
|
};
|
|
162
205
|
|
|
163
206
|
if (showAnchors || labelsInCentroids) {
|
|
@@ -174,8 +217,10 @@ function smartLabels(
|
|
|
174
217
|
.join("path")
|
|
175
218
|
.attr("class", "anchor")
|
|
176
219
|
.attr("display", ({ show }) => (show ? null : "none"))
|
|
177
|
-
.attr("d", ({ d, cell }) =>
|
|
178
|
-
cell
|
|
220
|
+
.attr("d", ({ d, cell }, index) =>
|
|
221
|
+
cell
|
|
222
|
+
? `M${d3__namespace.polygonCentroid(cell)}L${x(d, index)},${y(d, index)}`
|
|
223
|
+
: null
|
|
179
224
|
);
|
|
180
225
|
}
|
|
181
226
|
|
|
@@ -218,11 +263,14 @@ function smartLabels(
|
|
|
218
263
|
.data((d) => d)
|
|
219
264
|
.join("text")
|
|
220
265
|
.style("font", font)
|
|
221
|
-
.each(function ({ d, cell }) {
|
|
266
|
+
.each(function ({ d, cell }, index) {
|
|
222
267
|
if (!cell) return;
|
|
223
268
|
const [cx, cy] = d3__namespace.polygonCentroid(cell);
|
|
224
269
|
const angle =
|
|
225
|
-
(Math.round(
|
|
270
|
+
(Math.round(
|
|
271
|
+
(Math.atan2(cy - y(d, index), cx - x(d, index)) / Math.PI) * 2
|
|
272
|
+
) +
|
|
273
|
+
4) %
|
|
226
274
|
4;
|
|
227
275
|
|
|
228
276
|
d3__namespace.select(this).call(
|
|
@@ -235,13 +283,13 @@ function smartLabels(
|
|
|
235
283
|
: orient.left
|
|
236
284
|
);
|
|
237
285
|
})
|
|
238
|
-
.attr("transform", ({ d, cell }) => {
|
|
286
|
+
.attr("transform", ({ d, cell }, index) => {
|
|
239
287
|
if (labelsInCentroids) {
|
|
240
288
|
const [cx, cy] = d3__namespace.polygonCentroid(cell);
|
|
241
289
|
|
|
242
290
|
return `translate(${cx}, ${cy})`;
|
|
243
291
|
} else {
|
|
244
|
-
return `translate(${x(d)}, ${y(d)})`;
|
|
292
|
+
return `translate(${x(d, index)}, ${y(d, index)})`;
|
|
245
293
|
}
|
|
246
294
|
})
|
|
247
295
|
.attr("display", ({ show }) => (show ? null : "none"))
|
|
@@ -301,20 +349,26 @@ function smartLabels(
|
|
|
301
349
|
}
|
|
302
350
|
} // renderSVG
|
|
303
351
|
|
|
304
|
-
function
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
352
|
+
function renderCanvasAnchorsVoronoiPoints(context) {
|
|
353
|
+
if (debug)
|
|
354
|
+
console.log(
|
|
355
|
+
"renderCanvasAnchorsVoronoiPoints",
|
|
356
|
+
"showAnchors",
|
|
357
|
+
showAnchors,
|
|
358
|
+
"labelsInCentroids",
|
|
359
|
+
labelsInCentroids,
|
|
360
|
+
"showVoronoi",
|
|
361
|
+
showVoronoi,
|
|
362
|
+
"showPoints",
|
|
363
|
+
showPoints
|
|
364
|
+
);
|
|
311
365
|
if (showAnchors || labelsInCentroids) {
|
|
312
|
-
cells.forEach(({ d, cell, show }) => {
|
|
366
|
+
cells.forEach(({ d, cell, show }, index) => {
|
|
313
367
|
if (!show) return;
|
|
314
368
|
const [cx, cy] = d3__namespace.polygonCentroid(cell);
|
|
315
369
|
context.beginPath();
|
|
316
370
|
context.moveTo(cx, cy);
|
|
317
|
-
context.lineTo(x(d), y(d));
|
|
371
|
+
context.lineTo(x(d, index), y(d, index));
|
|
318
372
|
context.strokeStyle = anchorsStroke;
|
|
319
373
|
context.stroke();
|
|
320
374
|
});
|
|
@@ -329,23 +383,40 @@ function smartLabels(
|
|
|
329
383
|
|
|
330
384
|
if (showPoints) {
|
|
331
385
|
context.beginPath();
|
|
332
|
-
delaunay.renderPoints(context, 2);
|
|
386
|
+
// delaunay.renderPoints(context, 2);
|
|
387
|
+
context.save();
|
|
388
|
+
context.globalAlpha = 0.7;
|
|
389
|
+
context.fillStyle = "black";
|
|
390
|
+
data.forEach((d, i) => {
|
|
391
|
+
context.beginPath();
|
|
392
|
+
context.arc(x(d, i), y(d, i), r(d, i), 0, 2 * Math.PI);
|
|
393
|
+
});
|
|
394
|
+
context.restore();
|
|
333
395
|
context.fillStyle = pointsFill;
|
|
334
396
|
context.fill();
|
|
335
397
|
context.strokeStyle = pointsStroke;
|
|
336
398
|
context.stroke();
|
|
337
399
|
}
|
|
400
|
+
} // renderCanvasAnchorsVoronoiPoints
|
|
338
401
|
|
|
339
|
-
|
|
340
|
-
|
|
402
|
+
function renderCanvasLabels(context, selected) {
|
|
403
|
+
cells.forEach(({ d, cell, show }, index) => {
|
|
404
|
+
if (!show && index !== selected) return;
|
|
341
405
|
const [cx, cy] = d3__namespace.polygonCentroid(cell);
|
|
342
406
|
const angle =
|
|
343
|
-
(Math.round(
|
|
407
|
+
(Math.round(
|
|
408
|
+
(Math.atan2(cy - y(d, index), cx - x(d, index)) / Math.PI) * 2
|
|
409
|
+
) +
|
|
410
|
+
4) %
|
|
411
|
+
4;
|
|
344
412
|
|
|
345
413
|
context.save();
|
|
414
|
+
const dr = r(d, index) + padding;
|
|
415
|
+
const dx = angle === 0 ? dr : angle === 2 ? -dr : dr;
|
|
416
|
+
const dy = angle === 1 ? dr : angle === 3 ? -dr : dr;
|
|
346
417
|
context.translate(
|
|
347
|
-
labelsInCentroids ? cx : x(d),
|
|
348
|
-
labelsInCentroids ? cy : y(d)
|
|
418
|
+
labelsInCentroids ? cx : x(d, index) + dx,
|
|
419
|
+
labelsInCentroids ? cy : y(d, index) + dy
|
|
349
420
|
);
|
|
350
421
|
context.textAlign =
|
|
351
422
|
angle === 0
|
|
@@ -363,16 +434,23 @@ function smartLabels(
|
|
|
363
434
|
: angle === 2
|
|
364
435
|
? "middle"
|
|
365
436
|
: "bottom";
|
|
366
|
-
context.font = font;
|
|
437
|
+
context.font = index === selected ? hoverFont : font;
|
|
367
438
|
context.fillStyle = fill;
|
|
368
439
|
context.strokeStyle = stroke;
|
|
369
440
|
context.lineWidth = strokeWidth;
|
|
370
|
-
|
|
371
|
-
context.
|
|
441
|
+
// if (debug) console.log(index, dx, dy, angle);
|
|
442
|
+
context.strokeText(label(d, index), 0, 0);
|
|
443
|
+
context.fillText(label(d, index), 0, 0);
|
|
372
444
|
context.restore();
|
|
373
445
|
});
|
|
446
|
+
} // renderCanvasLabels
|
|
447
|
+
|
|
448
|
+
function renderCanvas() {
|
|
449
|
+
const context = target.node().getContext("2d");
|
|
450
|
+
|
|
451
|
+
renderCanvasAnchorsVoronoiPoints(context);
|
|
452
|
+
renderCanvasLabels(context, selected);
|
|
374
453
|
|
|
375
|
-
let selected = null;
|
|
376
454
|
if (hover) {
|
|
377
455
|
if (debug) console.log("setting onmousemove", target);
|
|
378
456
|
|
|
@@ -383,44 +461,9 @@ function smartLabels(
|
|
|
383
461
|
selected = i;
|
|
384
462
|
|
|
385
463
|
context.clearRect(0, 0, width, height);
|
|
386
|
-
|
|
387
|
-
if (!show && index !== selected) return;
|
|
388
|
-
const [cx, cy] = d3__namespace.polygonCentroid(cell);
|
|
389
|
-
const angle =
|
|
390
|
-
(Math.round((Math.atan2(cy - y(d), cx - x(d)) / Math.PI) * 2) + 4) %
|
|
391
|
-
4;
|
|
392
|
-
|
|
393
|
-
context.save();
|
|
394
|
-
context.translate(
|
|
395
|
-
labelsInCentroids ? cx : x(d),
|
|
396
|
-
labelsInCentroids ? cy : y(d)
|
|
397
|
-
);
|
|
398
|
-
context.textAlign =
|
|
399
|
-
angle === 0
|
|
400
|
-
? "left"
|
|
401
|
-
: angle === 1
|
|
402
|
-
? "center"
|
|
403
|
-
: angle === 2
|
|
404
|
-
? "right"
|
|
405
|
-
: "center";
|
|
406
|
-
context.textBaseline =
|
|
407
|
-
angle === 0
|
|
408
|
-
? "middle"
|
|
409
|
-
: angle === 1
|
|
410
|
-
? "top"
|
|
411
|
-
: angle === 2
|
|
412
|
-
? "middle"
|
|
413
|
-
: "bottom";
|
|
414
|
-
context.font = index === selected ? hoverFont : font;
|
|
415
|
-
context.fillStyle = fill;
|
|
416
|
-
context.strokeStyle = stroke;
|
|
417
|
-
context.lineWidth = strokeWidth;
|
|
418
|
-
context.strokeText(label(d), 0, 0);
|
|
419
|
-
context.fillText(label(d), 0, 0);
|
|
420
|
-
context.restore();
|
|
421
|
-
});
|
|
422
|
-
|
|
464
|
+
renderCanvasAnchorsVoronoiPoints(context);
|
|
423
465
|
if (onHover && typeof onHover === "function") onHover(i);
|
|
466
|
+
renderCanvasLabels(context, selected);
|
|
424
467
|
});
|
|
425
468
|
}
|
|
426
469
|
} // renderCanvas
|
package/dist/smartLabels.min.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// smart-labels v0.0.
|
|
2
|
-
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("d3")):"function"==typeof define&&define.amd?define(["d3"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).smartLabels=e(t.d3)}(this,(function(t){"use strict";function e(t){var e=Object.create(null);return t&&Object.keys(t).forEach((function(o){if("default"!==o){var n=Object.getOwnPropertyDescriptor(t,o);Object.defineProperty(e,o,n.get?n:{enumerable:!0,get:function(){return t[o]}})}})),e.default=t,Object.freeze(e)}var o=e(t);function n(t,e="text"){const n=o.sort(t.selectAll(e),(t=>+t.getAttribute("data-priority"))).reverse().map((t=>{const{x:e,y:o,width:n,height:r}=t.getBoundingClientRect();return{node:t,x:e,y:o,width:n,height:r}})),r=[];for(const t of n){const e=r.some((e=>{return n=e,!((o=t).x+o.width<n.x||n.x+n.width<o.x||o.y+o.height<n.y||n.y+n.height<o.y);var o,n}));o.select(t.node).classed("occluded",e),e||r.push(t)}return r}return function(t,{x:e=t=>t[0],y:r=t=>t[1],
|
|
1
|
+
// smart-labels v0.0.3 Copyright (c) 2024 John Alexis Guerra Gómez
|
|
2
|
+
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("d3")):"function"==typeof define&&define.amd?define(["d3"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).smartLabels=e(t.d3)}(this,(function(t){"use strict";function e(t){var e=Object.create(null);return t&&Object.keys(t).forEach((function(o){if("default"!==o){var n=Object.getOwnPropertyDescriptor(t,o);Object.defineProperty(e,o,n.get?n:{enumerable:!0,get:function(){return t[o]}})}})),e.default=t,Object.freeze(e)}var o=e(t);function n(t,e="text"){const n=o.sort(t.selectAll(e),(t=>+t.getAttribute("data-priority"))).reverse().map((t=>{const{x:e,y:o,width:n,height:r}=t.getBoundingClientRect();return{node:t,x:e,y:o,width:n,height:r}})),r=[];for(const t of n){const e=r.some((e=>{return n=e,!((o=t).x+o.width<n.x||n.x+n.width<o.x||o.y+o.height<n.y||n.y+n.height<o.y);var o,n}));o.select(t.node).classed("occluded",e),e||r.push(t)}return r}return function(t,{x:e=t=>t[0],y:r=t=>t[1],r:l=()=>3,label:a=(t,e)=>e,fill:s="#333",stroke:i="white",threshold:c=2e3,width:d=null,height:h=null,target:f=null,font:u="10px sans-serif",hover:g=!0,onHover:p=t=>t,hoverFont:y="bolder 12px sans-serif",labelsInCentroids:b=!0,backgroundFill:m="#fefefe01",strokeWidth:v=5,showVoronoi:x=!1,voronoiStroke:w="#ccc",showAnchors:k=!1,anchorsStroke:A="orange",anchorsFill:L="none",useOcclusion:j=!0,occludedStyle:C="opacity: 0.2",showPoints:P=!1,pointsFill:S="#ccc",pointsSelectedFill:M="firebrick",pointsStroke:O="#ccc",renderer:T="svg",debug:$=!1,selected:E=null,padding:F=3}={}){t=t.filter(((t,o)=>void 0!==e(t,o)&&null!==e(t,o)&&void 0!==r(t,o)&&null!==r(t,o)));let I=o.extent(t,e),B=o.extent(t,r);if(d=d||I[1]-I[0],h=h||B[1]-B[0],$&&console.log("✅ smartLabels renderer",T),!function(t,e){return t&&t.node&&(t=t.node()),"canvas"===e.toLocaleLowerCase()?t instanceof HTMLCanvasElement:t instanceof SVGElement}(f,T))throw $&&console.log("❌ smartLabels Target doesn't match the renderer",f,T),new Error("Smartlabels Target doesn't match the renderer",f,T);"canvas"===T.toLocaleLowerCase()?(f=f?o.select(f.node?f.node():f):o.create("canvas").attr("width",d).attr("height",h),j=!1):f=(f=f?o.select(f.node?f.node():f):o.create("svg").attr("viewBox",[0,0,d,h]))||o.create("svg").attr("viewBox",[0,0,d,h]);const V=o.Delaunay.from(t,e,r),D=V.voronoi([I[0]-1,B[0]-1,I[1]+1,B[1]+1]);let H=t.map(((t,e)=>[t,D.cellPolygon(e)]));function R(t,o,n){if(!o){const l=V.find(e(t,n),r(t,n));if(-1===l)return console.log("couldn't find cell",l,t,e(t,n),r(t,n)),null;o=H[l][1]}return o}function W(n){$&&console.log("renderCanvasAnchorsVoronoiPoints","showAnchors",k,"labelsInCentroids",b,"showVoronoi",x,"showPoints",P),(k||b)&&H.forEach((({d:t,cell:l,show:a},s)=>{if(!a)return;const[i,c]=o.polygonCentroid(l);n.beginPath(),n.moveTo(i,c),n.lineTo(e(t,s),r(t,s)),n.strokeStyle=A,n.stroke()})),x&&(n.beginPath(),D.render(n),n.strokeStyle=w,n.stroke()),P&&(n.beginPath(),n.save(),n.globalAlpha=.7,n.fillStyle="black",t.forEach(((t,o)=>{n.beginPath(),n.arc(e(t,o),r(t,o),l(t,o),0,2*Math.PI)})),n.restore(),n.fillStyle=S,n.fill(),n.strokeStyle=O,n.stroke())}function q(t,n){H.forEach((({d:c,cell:d,show:h},f)=>{if(!h&&f!==n)return;const[g,p]=o.polygonCentroid(d),m=(Math.round(Math.atan2(p-r(c,f),g-e(c,f))/Math.PI*2)+4)%4;t.save();const x=l(c,f)+F,w=0===m?x:2===m?-x:x,k=1===m?x:3===m?-x:x;t.translate(b?g:e(c,f)+w,b?p:r(c,f)+k),t.textAlign=0===m?"left":1===m?"center":2===m?"right":"center",t.textBaseline=0===m?"middle":1===m?"top":2===m?"middle":"bottom",t.font=f===n?y:u,t.fillStyle=s,t.strokeStyle=i,t.lineWidth=v,t.strokeText(a(c,f),0,0),t.fillText(a(c,f),0,0),t.restore()}))}return H=H.map((([t,e],o)=>[t,R(t,e,o)])).map((([t,e])=>({d:t,cell:e,show:-o.polygonArea(e)>c}))),"canvas"===T.toLocaleLowerCase()?function(){const t=f.node().getContext("2d");W(t),q(t,E),g&&($&&console.log("setting onmousemove",f),f.on("mousemove",(e=>{const[n,r]=o.pointer(e),l=V.find(n,r);l!==E&&(E=l,t.clearRect(0,0,d,h),W(t),p&&"function"==typeof p&&p(l),q(t,E))})))}():function(){let t=null;j&&f.selectAll("style.smartLabels").data([0]).join("style").attr("class","smartLabels").html(`\n svg g.labels > text.occluded:not(.selected) { ${C} }\n `);const c=f.selectAll("rect.smartLabels").data([0]).join("rect").attr("class","smartLabels").attr("width",d).attr("height",h).attr("fill",m).attr("stroke","none"),T={top:t=>t.attr("text-anchor","middle").attr("y",((t,e)=>-(l(t,e)+F))),right:t=>t.attr("text-anchor","start").attr("dy","0.35em").attr("x",((t,e)=>l(t,e)+F)),bottom:t=>t.attr("text-anchor","middle").attr("dy","0.71em").attr("y",((t,e)=>l(t,e)+F)),left:t=>t.attr("text-anchor","end").attr("dy","0.35em").attr("x",((t,e)=>-(l(t,e)+F)))};let $;(k||b)&&(t=f.selectAll("g#anchors").data([H]).join("g").attr("pointer-events","none").attr("id","anchors").attr("stroke",A).attr("fill",L).selectAll("path.anchor").data((t=>t)).join("path").attr("class","anchor").attr("display",(({show:t})=>t?null:"none")).attr("d",(({d:t,cell:n},l)=>n?`M${o.polygonCentroid(n)}L${e(t,l)},${r(t,l)}`:null))),x&&f.selectAll("path#voronoi").data([1]).join("path").attr("id","voronoi").attr("stroke",w).attr("pointer-events","none").attr("d",D.render()),P&&($=f.selectAll("circle#points").data([1]).join("path").attr("id","points").attr("pointer-events","none").attr("stroke",O).attr("fill",S).attr("d",V.renderPoints(null,2)));const E=f.selectAll("g.labels").data([H]).join("g").attr("class","labels").attr("fill",s).attr("stroke",i).attr("stroke-width",v).attr("paint-order","stroke").attr("pointer-events","none").selectAll("text").data((t=>t)).join("text").style("font",u).each((function({d:t,cell:n},l){if(!n)return;const[a,s]=o.polygonCentroid(n),i=(Math.round(Math.atan2(s-r(t,l),a-e(t,l))/Math.PI*2)+4)%4;o.select(this).call(0===i?T.right:3===i?T.top:1===i?T.bottom:T.left)})).attr("transform",(({d:t,cell:n},l)=>{if(b){const[t,e]=o.polygonCentroid(n);return`translate(${t}, ${e})`}return`translate(${e(t,l)}, ${r(t,l)})`})).attr("display",(({show:t})=>t?null:"none")).text((({d:t},e,o)=>a(t,e,o)));let I=null;if(g&&c.on("mousemove",(e=>{const n=V.find(...o.pointer(e));n!==I&&(I=n,E.attr("display",(({show:t},e)=>e===I||t?null:"none")).style("font",((t,e)=>e===I?y:u)).classed("selected",((t,e)=>e===I)).filter(((t,e)=>e===I)).raise(),t&&t.attr("display",(({show:t},e)=>t||e===I?null:"none")),P&&$.attr("fill",((t,e)=>M)).classed("selected",((t,e)=>e===I)),p&&"function"==typeof p&&p(n))})),j){const t=new MutationObserver((function(){document.contains(f.node())&&(f.call(n,"g.labels > text"),t.disconnect())}));t.observe(document,{attributes:!1,childList:!0,characterData:!1,subtree:!0})}}(),Object.assign(f.node(),{delaunay:V,voronoi:D,cells:H})}}));
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -20,6 +20,7 @@ export default function smartLabels(
|
|
|
20
20
|
{
|
|
21
21
|
x = (d) => d[0], // x coordinate accessor, expected to be in pixels
|
|
22
22
|
y = (d) => d[1], // y coordinate accessor, expected to be in pixels
|
|
23
|
+
r = () => 3, // radius accessor, expected to be in pixels
|
|
23
24
|
label = (d, i) => i, // Accessor for the label
|
|
24
25
|
fill = "#333", // label fill color
|
|
25
26
|
stroke = "white", // label stroke color
|
|
@@ -53,11 +54,16 @@ export default function smartLabels(
|
|
|
53
54
|
pointsStroke = "#ccc",
|
|
54
55
|
renderer = "svg",
|
|
55
56
|
debug = false,
|
|
57
|
+
selected = null,
|
|
58
|
+
padding = 3, // label padding in pixels
|
|
56
59
|
} = {}
|
|
57
60
|
) {
|
|
58
61
|
data = data.filter(
|
|
59
|
-
(d) =>
|
|
60
|
-
x(d
|
|
62
|
+
(d, index) =>
|
|
63
|
+
x(d, index) !== undefined &&
|
|
64
|
+
x(d, index) !== null &&
|
|
65
|
+
y(d, index) !== undefined &&
|
|
66
|
+
y(d, index) !== null
|
|
61
67
|
);
|
|
62
68
|
|
|
63
69
|
let xExtent = d3.extent(data, x),
|
|
@@ -66,12 +72,37 @@ export default function smartLabels(
|
|
|
66
72
|
height = height || yExtent[1] - yExtent[0];
|
|
67
73
|
|
|
68
74
|
if (debug) console.log("✅ smartLabels renderer", renderer);
|
|
75
|
+
|
|
76
|
+
function checkIfTargetMatchesRenderer(target, renderer) {
|
|
77
|
+
if (target && target.node) {
|
|
78
|
+
target = target.node();
|
|
79
|
+
}
|
|
80
|
+
if (renderer.toLocaleLowerCase() === "canvas") {
|
|
81
|
+
return target instanceof HTMLCanvasElement;
|
|
82
|
+
} else {
|
|
83
|
+
return target instanceof SVGElement;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!checkIfTargetMatchesRenderer(target, renderer)) {
|
|
88
|
+
if (debug) console.log("❌ smartLabels Target doesn't match the renderer", target, renderer);
|
|
89
|
+
throw new Error("Smartlabels Target doesn't match the renderer", target, renderer);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Try to reuse the target
|
|
69
93
|
if (renderer.toLocaleLowerCase() === "canvas") {
|
|
70
|
-
target
|
|
71
|
-
d3.select(target)
|
|
72
|
-
|
|
94
|
+
if (target) {
|
|
95
|
+
target = d3.select(target.node ? target.node() : target);
|
|
96
|
+
} else {
|
|
97
|
+
target = d3.create("canvas").attr("width", width).attr("height", height);
|
|
98
|
+
}
|
|
73
99
|
useOcclusion = false;
|
|
74
100
|
} else {
|
|
101
|
+
if (target) {
|
|
102
|
+
target = d3.select(target.node ? target.node() : target);
|
|
103
|
+
} else {
|
|
104
|
+
target = d3.create("svg").attr("viewBox", [0, 0, width, height]);
|
|
105
|
+
}
|
|
75
106
|
target = target || d3.create("svg").attr("viewBox", [0, 0, width, height]);
|
|
76
107
|
}
|
|
77
108
|
|
|
@@ -86,16 +117,16 @@ export default function smartLabels(
|
|
|
86
117
|
let cells = data.map((d, i) => [d, voronoi.cellPolygon(i)]);
|
|
87
118
|
// Replace null cells with the nearest one
|
|
88
119
|
cells = cells
|
|
89
|
-
.map(([d, cell]) => [d, getNearestCell(d, cell)])
|
|
120
|
+
.map(([d, cell], index) => [d, getNearestCell(d, cell, index)])
|
|
90
121
|
.map(([d, cell]) => ({ d, cell, show: -d3.polygonArea(cell) > threshold }));
|
|
91
122
|
|
|
92
123
|
// cells can be null when we have duplicated coords
|
|
93
124
|
// https://github.com/d3/d3-delaunay/issues/106
|
|
94
|
-
function getNearestCell(d, cell) {
|
|
125
|
+
function getNearestCell(d, cell, index) {
|
|
95
126
|
if (!cell) {
|
|
96
|
-
const i = delaunay.find(x(d), y(d));
|
|
127
|
+
const i = delaunay.find(x(d, index), y(d, index));
|
|
97
128
|
if (i === -1) {
|
|
98
|
-
console.log("couldn't find cell", i, d, x(d), y(d));
|
|
129
|
+
console.log("couldn't find cell", i, d, x(d, index), y(d, index));
|
|
99
130
|
return null;
|
|
100
131
|
}
|
|
101
132
|
cell = cells[i][1];
|
|
@@ -126,13 +157,25 @@ export default function smartLabels(
|
|
|
126
157
|
.attr("stroke", "none");
|
|
127
158
|
|
|
128
159
|
const orient = {
|
|
129
|
-
top: (text) =>
|
|
160
|
+
top: (text) =>
|
|
161
|
+
text
|
|
162
|
+
.attr("text-anchor", "middle")
|
|
163
|
+
.attr("y", (d, i) => -(r(d, i) + padding)),
|
|
130
164
|
right: (text) =>
|
|
131
|
-
text
|
|
165
|
+
text
|
|
166
|
+
.attr("text-anchor", "start")
|
|
167
|
+
.attr("dy", "0.35em")
|
|
168
|
+
.attr("x", (d, i) => r(d, i) + padding),
|
|
132
169
|
bottom: (text) =>
|
|
133
|
-
text
|
|
170
|
+
text
|
|
171
|
+
.attr("text-anchor", "middle")
|
|
172
|
+
.attr("dy", "0.71em")
|
|
173
|
+
.attr("y", (d, i) => r(d, i) + padding),
|
|
134
174
|
left: (text) =>
|
|
135
|
-
text
|
|
175
|
+
text
|
|
176
|
+
.attr("text-anchor", "end")
|
|
177
|
+
.attr("dy", "0.35em")
|
|
178
|
+
.attr("x", (d, i) => -(r(d, i) + padding)),
|
|
136
179
|
};
|
|
137
180
|
|
|
138
181
|
if (showAnchors || labelsInCentroids) {
|
|
@@ -149,8 +192,10 @@ export default function smartLabels(
|
|
|
149
192
|
.join("path")
|
|
150
193
|
.attr("class", "anchor")
|
|
151
194
|
.attr("display", ({ show }) => (show ? null : "none"))
|
|
152
|
-
.attr("d", ({ d, cell }) =>
|
|
153
|
-
cell
|
|
195
|
+
.attr("d", ({ d, cell }, index) =>
|
|
196
|
+
cell
|
|
197
|
+
? `M${d3.polygonCentroid(cell)}L${x(d, index)},${y(d, index)}`
|
|
198
|
+
: null
|
|
154
199
|
);
|
|
155
200
|
}
|
|
156
201
|
|
|
@@ -193,11 +238,14 @@ export default function smartLabels(
|
|
|
193
238
|
.data((d) => d)
|
|
194
239
|
.join("text")
|
|
195
240
|
.style("font", font)
|
|
196
|
-
.each(function ({ d, cell }) {
|
|
241
|
+
.each(function ({ d, cell }, index) {
|
|
197
242
|
if (!cell) return;
|
|
198
243
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
199
244
|
const angle =
|
|
200
|
-
(Math.round(
|
|
245
|
+
(Math.round(
|
|
246
|
+
(Math.atan2(cy - y(d, index), cx - x(d, index)) / Math.PI) * 2
|
|
247
|
+
) +
|
|
248
|
+
4) %
|
|
201
249
|
4;
|
|
202
250
|
|
|
203
251
|
d3.select(this).call(
|
|
@@ -210,13 +258,13 @@ export default function smartLabels(
|
|
|
210
258
|
: orient.left
|
|
211
259
|
);
|
|
212
260
|
})
|
|
213
|
-
.attr("transform", ({ d, cell }) => {
|
|
261
|
+
.attr("transform", ({ d, cell }, index) => {
|
|
214
262
|
if (labelsInCentroids) {
|
|
215
263
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
216
264
|
|
|
217
265
|
return `translate(${cx}, ${cy})`;
|
|
218
266
|
} else {
|
|
219
|
-
return `translate(${x(d)}, ${y(d)})`;
|
|
267
|
+
return `translate(${x(d, index)}, ${y(d, index)})`;
|
|
220
268
|
}
|
|
221
269
|
})
|
|
222
270
|
.attr("display", ({ show }) => (show ? null : "none"))
|
|
@@ -276,20 +324,26 @@ export default function smartLabels(
|
|
|
276
324
|
}
|
|
277
325
|
} // renderSVG
|
|
278
326
|
|
|
279
|
-
function
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
327
|
+
function renderCanvasAnchorsVoronoiPoints(context) {
|
|
328
|
+
if (debug)
|
|
329
|
+
console.log(
|
|
330
|
+
"renderCanvasAnchorsVoronoiPoints",
|
|
331
|
+
"showAnchors",
|
|
332
|
+
showAnchors,
|
|
333
|
+
"labelsInCentroids",
|
|
334
|
+
labelsInCentroids,
|
|
335
|
+
"showVoronoi",
|
|
336
|
+
showVoronoi,
|
|
337
|
+
"showPoints",
|
|
338
|
+
showPoints
|
|
339
|
+
);
|
|
286
340
|
if (showAnchors || labelsInCentroids) {
|
|
287
|
-
cells.forEach(({ d, cell, show }) => {
|
|
341
|
+
cells.forEach(({ d, cell, show }, index) => {
|
|
288
342
|
if (!show) return;
|
|
289
343
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
290
344
|
context.beginPath();
|
|
291
345
|
context.moveTo(cx, cy);
|
|
292
|
-
context.lineTo(x(d), y(d));
|
|
346
|
+
context.lineTo(x(d, index), y(d, index));
|
|
293
347
|
context.strokeStyle = anchorsStroke;
|
|
294
348
|
context.stroke();
|
|
295
349
|
});
|
|
@@ -304,23 +358,40 @@ export default function smartLabels(
|
|
|
304
358
|
|
|
305
359
|
if (showPoints) {
|
|
306
360
|
context.beginPath();
|
|
307
|
-
delaunay.renderPoints(context, 2);
|
|
361
|
+
// delaunay.renderPoints(context, 2);
|
|
362
|
+
context.save();
|
|
363
|
+
context.globalAlpha = 0.7;
|
|
364
|
+
context.fillStyle = "black";
|
|
365
|
+
data.forEach((d, i) => {
|
|
366
|
+
context.beginPath();
|
|
367
|
+
context.arc(x(d, i), y(d, i), r(d, i), 0, 2 * Math.PI);
|
|
368
|
+
});
|
|
369
|
+
context.restore();
|
|
308
370
|
context.fillStyle = pointsFill;
|
|
309
371
|
context.fill();
|
|
310
372
|
context.strokeStyle = pointsStroke;
|
|
311
373
|
context.stroke();
|
|
312
374
|
}
|
|
375
|
+
} // renderCanvasAnchorsVoronoiPoints
|
|
313
376
|
|
|
314
|
-
|
|
315
|
-
|
|
377
|
+
function renderCanvasLabels(context, selected) {
|
|
378
|
+
cells.forEach(({ d, cell, show }, index) => {
|
|
379
|
+
if (!show && index !== selected) return;
|
|
316
380
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
317
381
|
const angle =
|
|
318
|
-
(Math.round(
|
|
382
|
+
(Math.round(
|
|
383
|
+
(Math.atan2(cy - y(d, index), cx - x(d, index)) / Math.PI) * 2
|
|
384
|
+
) +
|
|
385
|
+
4) %
|
|
386
|
+
4;
|
|
319
387
|
|
|
320
388
|
context.save();
|
|
389
|
+
const dr = r(d, index) + padding;
|
|
390
|
+
const dx = angle === 0 ? dr : angle === 2 ? -dr : dr;
|
|
391
|
+
const dy = angle === 1 ? dr : angle === 3 ? -dr : dr;
|
|
321
392
|
context.translate(
|
|
322
|
-
labelsInCentroids ? cx : x(d),
|
|
323
|
-
labelsInCentroids ? cy : y(d)
|
|
393
|
+
labelsInCentroids ? cx : x(d, index) + dx,
|
|
394
|
+
labelsInCentroids ? cy : y(d, index) + dy
|
|
324
395
|
);
|
|
325
396
|
context.textAlign =
|
|
326
397
|
angle === 0
|
|
@@ -338,16 +409,23 @@ export default function smartLabels(
|
|
|
338
409
|
: angle === 2
|
|
339
410
|
? "middle"
|
|
340
411
|
: "bottom";
|
|
341
|
-
context.font = font;
|
|
412
|
+
context.font = index === selected ? hoverFont : font;
|
|
342
413
|
context.fillStyle = fill;
|
|
343
414
|
context.strokeStyle = stroke;
|
|
344
415
|
context.lineWidth = strokeWidth;
|
|
345
|
-
|
|
346
|
-
context.
|
|
416
|
+
// if (debug) console.log(index, dx, dy, angle);
|
|
417
|
+
context.strokeText(label(d, index), 0, 0);
|
|
418
|
+
context.fillText(label(d, index), 0, 0);
|
|
347
419
|
context.restore();
|
|
348
420
|
});
|
|
421
|
+
} // renderCanvasLabels
|
|
422
|
+
|
|
423
|
+
function renderCanvas() {
|
|
424
|
+
const context = target.node().getContext("2d");
|
|
425
|
+
|
|
426
|
+
renderCanvasAnchorsVoronoiPoints(context);
|
|
427
|
+
renderCanvasLabels(context, selected);
|
|
349
428
|
|
|
350
|
-
let selected = null;
|
|
351
429
|
if (hover) {
|
|
352
430
|
if (debug) console.log("setting onmousemove", target);
|
|
353
431
|
|
|
@@ -358,44 +436,9 @@ export default function smartLabels(
|
|
|
358
436
|
selected = i;
|
|
359
437
|
|
|
360
438
|
context.clearRect(0, 0, width, height);
|
|
361
|
-
|
|
362
|
-
if (!show && index !== selected) return;
|
|
363
|
-
const [cx, cy] = d3.polygonCentroid(cell);
|
|
364
|
-
const angle =
|
|
365
|
-
(Math.round((Math.atan2(cy - y(d), cx - x(d)) / Math.PI) * 2) + 4) %
|
|
366
|
-
4;
|
|
367
|
-
|
|
368
|
-
context.save();
|
|
369
|
-
context.translate(
|
|
370
|
-
labelsInCentroids ? cx : x(d),
|
|
371
|
-
labelsInCentroids ? cy : y(d)
|
|
372
|
-
);
|
|
373
|
-
context.textAlign =
|
|
374
|
-
angle === 0
|
|
375
|
-
? "left"
|
|
376
|
-
: angle === 1
|
|
377
|
-
? "center"
|
|
378
|
-
: angle === 2
|
|
379
|
-
? "right"
|
|
380
|
-
: "center";
|
|
381
|
-
context.textBaseline =
|
|
382
|
-
angle === 0
|
|
383
|
-
? "middle"
|
|
384
|
-
: angle === 1
|
|
385
|
-
? "top"
|
|
386
|
-
: angle === 2
|
|
387
|
-
? "middle"
|
|
388
|
-
: "bottom";
|
|
389
|
-
context.font = index === selected ? hoverFont : font;
|
|
390
|
-
context.fillStyle = fill;
|
|
391
|
-
context.strokeStyle = stroke;
|
|
392
|
-
context.lineWidth = strokeWidth;
|
|
393
|
-
context.strokeText(label(d), 0, 0);
|
|
394
|
-
context.fillText(label(d), 0, 0);
|
|
395
|
-
context.restore();
|
|
396
|
-
});
|
|
397
|
-
|
|
439
|
+
renderCanvasAnchorsVoronoiPoints(context);
|
|
398
440
|
if (onHover && typeof onHover === "function") onHover(i);
|
|
441
|
+
renderCanvasLabels(context, selected);
|
|
399
442
|
});
|
|
400
443
|
}
|
|
401
444
|
} // renderCanvas
|