smart-labels 0.0.1 → 0.0.3
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 +92 -74
- package/dist/smartLabels.js +96 -79
- package/dist/smartLabels.min.js +2 -2
- package/package.json +1 -1
- package/src/index.js +92 -74
package/dist/smartLabels.es.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// smart-labels v0.0.
|
|
1
|
+
// smart-labels v0.0.2 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),
|
|
@@ -88,16 +94,16 @@ function smartLabels(
|
|
|
88
94
|
let cells = data.map((d, i) => [d, voronoi.cellPolygon(i)]);
|
|
89
95
|
// Replace null cells with the nearest one
|
|
90
96
|
cells = cells
|
|
91
|
-
.map(([d, cell]) => [d, getNearestCell(d, cell)])
|
|
97
|
+
.map(([d, cell], index) => [d, getNearestCell(d, cell, index)])
|
|
92
98
|
.map(([d, cell]) => ({ d, cell, show: -d3.polygonArea(cell) > threshold }));
|
|
93
99
|
|
|
94
100
|
// cells can be null when we have duplicated coords
|
|
95
101
|
// https://github.com/d3/d3-delaunay/issues/106
|
|
96
|
-
function getNearestCell(d, cell) {
|
|
102
|
+
function getNearestCell(d, cell, index) {
|
|
97
103
|
if (!cell) {
|
|
98
|
-
const i = delaunay.find(x(d), y(d));
|
|
104
|
+
const i = delaunay.find(x(d, index), y(d, index));
|
|
99
105
|
if (i === -1) {
|
|
100
|
-
console.log("couldn't find cell", i, d, x(d), y(d));
|
|
106
|
+
console.log("couldn't find cell", i, d, x(d, index), y(d, index));
|
|
101
107
|
return null;
|
|
102
108
|
}
|
|
103
109
|
cell = cells[i][1];
|
|
@@ -128,13 +134,25 @@ function smartLabels(
|
|
|
128
134
|
.attr("stroke", "none");
|
|
129
135
|
|
|
130
136
|
const orient = {
|
|
131
|
-
top: (text) =>
|
|
137
|
+
top: (text) =>
|
|
138
|
+
text
|
|
139
|
+
.attr("text-anchor", "middle")
|
|
140
|
+
.attr("y", (d, i) => -(r(d, i) + padding)),
|
|
132
141
|
right: (text) =>
|
|
133
|
-
text
|
|
142
|
+
text
|
|
143
|
+
.attr("text-anchor", "start")
|
|
144
|
+
.attr("dy", "0.35em")
|
|
145
|
+
.attr("x", (d, i) => r(d, i) + padding),
|
|
134
146
|
bottom: (text) =>
|
|
135
|
-
text
|
|
147
|
+
text
|
|
148
|
+
.attr("text-anchor", "middle")
|
|
149
|
+
.attr("dy", "0.71em")
|
|
150
|
+
.attr("y", (d, i) => r(d, i) + padding),
|
|
136
151
|
left: (text) =>
|
|
137
|
-
text
|
|
152
|
+
text
|
|
153
|
+
.attr("text-anchor", "end")
|
|
154
|
+
.attr("dy", "0.35em")
|
|
155
|
+
.attr("x", (d, i) => -(r(d, i) + padding)),
|
|
138
156
|
};
|
|
139
157
|
|
|
140
158
|
if (showAnchors || labelsInCentroids) {
|
|
@@ -151,8 +169,10 @@ function smartLabels(
|
|
|
151
169
|
.join("path")
|
|
152
170
|
.attr("class", "anchor")
|
|
153
171
|
.attr("display", ({ show }) => (show ? null : "none"))
|
|
154
|
-
.attr("d", ({ d, cell }) =>
|
|
155
|
-
cell
|
|
172
|
+
.attr("d", ({ d, cell }, index) =>
|
|
173
|
+
cell
|
|
174
|
+
? `M${d3.polygonCentroid(cell)}L${x(d, index)},${y(d, index)}`
|
|
175
|
+
: null
|
|
156
176
|
);
|
|
157
177
|
}
|
|
158
178
|
|
|
@@ -195,11 +215,14 @@ function smartLabels(
|
|
|
195
215
|
.data((d) => d)
|
|
196
216
|
.join("text")
|
|
197
217
|
.style("font", font)
|
|
198
|
-
.each(function ({ d, cell }) {
|
|
218
|
+
.each(function ({ d, cell }, index) {
|
|
199
219
|
if (!cell) return;
|
|
200
220
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
201
221
|
const angle =
|
|
202
|
-
(Math.round(
|
|
222
|
+
(Math.round(
|
|
223
|
+
(Math.atan2(cy - y(d, index), cx - x(d, index)) / Math.PI) * 2
|
|
224
|
+
) +
|
|
225
|
+
4) %
|
|
203
226
|
4;
|
|
204
227
|
|
|
205
228
|
d3.select(this).call(
|
|
@@ -212,13 +235,13 @@ function smartLabels(
|
|
|
212
235
|
: orient.left
|
|
213
236
|
);
|
|
214
237
|
})
|
|
215
|
-
.attr("transform", ({ d, cell }) => {
|
|
238
|
+
.attr("transform", ({ d, cell }, index) => {
|
|
216
239
|
if (labelsInCentroids) {
|
|
217
240
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
218
241
|
|
|
219
242
|
return `translate(${cx}, ${cy})`;
|
|
220
243
|
} else {
|
|
221
|
-
return `translate(${x(d)}, ${y(d)})`;
|
|
244
|
+
return `translate(${x(d, index)}, ${y(d, index)})`;
|
|
222
245
|
}
|
|
223
246
|
})
|
|
224
247
|
.attr("display", ({ show }) => (show ? null : "none"))
|
|
@@ -278,20 +301,26 @@ function smartLabels(
|
|
|
278
301
|
}
|
|
279
302
|
} // renderSVG
|
|
280
303
|
|
|
281
|
-
function
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
304
|
+
function renderCanvasAnchorsVoronoiPoints(context) {
|
|
305
|
+
if (debug)
|
|
306
|
+
console.log(
|
|
307
|
+
"renderCanvasAnchorsVoronoiPoints",
|
|
308
|
+
"showAnchors",
|
|
309
|
+
showAnchors,
|
|
310
|
+
"labelsInCentroids",
|
|
311
|
+
labelsInCentroids,
|
|
312
|
+
"showVoronoi",
|
|
313
|
+
showVoronoi,
|
|
314
|
+
"showPoints",
|
|
315
|
+
showPoints
|
|
316
|
+
);
|
|
288
317
|
if (showAnchors || labelsInCentroids) {
|
|
289
|
-
cells.forEach(({ d, cell, show }) => {
|
|
318
|
+
cells.forEach(({ d, cell, show }, index) => {
|
|
290
319
|
if (!show) return;
|
|
291
320
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
292
321
|
context.beginPath();
|
|
293
322
|
context.moveTo(cx, cy);
|
|
294
|
-
context.lineTo(x(d), y(d));
|
|
323
|
+
context.lineTo(x(d, index), y(d, index));
|
|
295
324
|
context.strokeStyle = anchorsStroke;
|
|
296
325
|
context.stroke();
|
|
297
326
|
});
|
|
@@ -306,23 +335,40 @@ function smartLabels(
|
|
|
306
335
|
|
|
307
336
|
if (showPoints) {
|
|
308
337
|
context.beginPath();
|
|
309
|
-
delaunay.renderPoints(context, 2);
|
|
338
|
+
// delaunay.renderPoints(context, 2);
|
|
339
|
+
context.save();
|
|
340
|
+
context.globalAlpha = 0.7;
|
|
341
|
+
context.fillStyle = "black";
|
|
342
|
+
data.forEach((d, i) => {
|
|
343
|
+
context.beginPath();
|
|
344
|
+
context.arc(x(d, i), y(d, i), r(d, i), 0, 2 * Math.PI);
|
|
345
|
+
});
|
|
346
|
+
context.restore();
|
|
310
347
|
context.fillStyle = pointsFill;
|
|
311
348
|
context.fill();
|
|
312
349
|
context.strokeStyle = pointsStroke;
|
|
313
350
|
context.stroke();
|
|
314
351
|
}
|
|
352
|
+
} // renderCanvasAnchorsVoronoiPoints
|
|
315
353
|
|
|
316
|
-
|
|
317
|
-
|
|
354
|
+
function renderCanvasLabels(context, selected) {
|
|
355
|
+
cells.forEach(({ d, cell, show }, index) => {
|
|
356
|
+
if (!show && index !== selected) return;
|
|
318
357
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
319
358
|
const angle =
|
|
320
|
-
(Math.round(
|
|
359
|
+
(Math.round(
|
|
360
|
+
(Math.atan2(cy - y(d, index), cx - x(d, index)) / Math.PI) * 2
|
|
361
|
+
) +
|
|
362
|
+
4) %
|
|
363
|
+
4;
|
|
321
364
|
|
|
322
365
|
context.save();
|
|
366
|
+
const dr = r(d, index) + padding;
|
|
367
|
+
const dx = angle === 0 ? dr : angle === 2 ? -dr : dr;
|
|
368
|
+
const dy = angle === 1 ? dr : angle === 3 ? -dr : dr;
|
|
323
369
|
context.translate(
|
|
324
|
-
labelsInCentroids ? cx : x(d),
|
|
325
|
-
labelsInCentroids ? cy : y(d)
|
|
370
|
+
labelsInCentroids ? cx : x(d, index) + dx,
|
|
371
|
+
labelsInCentroids ? cy : y(d, index) + dy
|
|
326
372
|
);
|
|
327
373
|
context.textAlign =
|
|
328
374
|
angle === 0
|
|
@@ -340,16 +386,23 @@ function smartLabels(
|
|
|
340
386
|
: angle === 2
|
|
341
387
|
? "middle"
|
|
342
388
|
: "bottom";
|
|
343
|
-
context.font = font;
|
|
389
|
+
context.font = index === selected ? hoverFont : font;
|
|
344
390
|
context.fillStyle = fill;
|
|
345
391
|
context.strokeStyle = stroke;
|
|
346
392
|
context.lineWidth = strokeWidth;
|
|
347
|
-
|
|
348
|
-
context.
|
|
393
|
+
// if (debug) console.log(index, dx, dy, angle);
|
|
394
|
+
context.strokeText(label(d, index), 0, 0);
|
|
395
|
+
context.fillText(label(d, index), 0, 0);
|
|
349
396
|
context.restore();
|
|
350
397
|
});
|
|
398
|
+
} // renderCanvasLabels
|
|
399
|
+
|
|
400
|
+
function renderCanvas() {
|
|
401
|
+
const context = target.node().getContext("2d");
|
|
402
|
+
|
|
403
|
+
renderCanvasAnchorsVoronoiPoints(context);
|
|
404
|
+
renderCanvasLabels(context, selected);
|
|
351
405
|
|
|
352
|
-
let selected = null;
|
|
353
406
|
if (hover) {
|
|
354
407
|
if (debug) console.log("setting onmousemove", target);
|
|
355
408
|
|
|
@@ -360,44 +413,9 @@ function smartLabels(
|
|
|
360
413
|
selected = i;
|
|
361
414
|
|
|
362
415
|
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
|
-
|
|
416
|
+
renderCanvasAnchorsVoronoiPoints(context);
|
|
400
417
|
if (onHover && typeof onHover === "function") onHover(i);
|
|
418
|
+
renderCanvasLabels(context, selected);
|
|
401
419
|
});
|
|
402
420
|
}
|
|
403
421
|
} // renderCanvas
|
|
@@ -454,4 +472,4 @@ function intersectRect(a, b) {
|
|
|
454
472
|
);
|
|
455
473
|
}
|
|
456
474
|
|
|
457
|
-
export {
|
|
475
|
+
export { smartLabels as default };
|
package/dist/smartLabels.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// smart-labels v0.0.
|
|
1
|
+
// smart-labels v0.0.2 Copyright (c) 2024 John Alexis Guerra Gómez
|
|
2
2
|
(function (global, factory) {
|
|
3
|
-
typeof exports === 'object' && typeof module !== 'undefined' ? factory(
|
|
4
|
-
typeof define === 'function' && define.amd ? define(['
|
|
5
|
-
(global = typeof globalThis !== 'undefined' ? globalThis : global || self,
|
|
6
|
-
})(this, (function (
|
|
3
|
+
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('d3')) :
|
|
4
|
+
typeof define === 'function' && define.amd ? define(['d3'], factory) :
|
|
5
|
+
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.smartLabels = factory(global.d3));
|
|
6
|
+
})(this, (function (d3) { 'use strict';
|
|
7
7
|
|
|
8
8
|
function _interopNamespaceDefault(e) {
|
|
9
9
|
var n = Object.create(null);
|
|
@@ -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),
|
|
@@ -111,16 +117,16 @@ function smartLabels(
|
|
|
111
117
|
let cells = data.map((d, i) => [d, voronoi.cellPolygon(i)]);
|
|
112
118
|
// Replace null cells with the nearest one
|
|
113
119
|
cells = cells
|
|
114
|
-
.map(([d, cell]) => [d, getNearestCell(d, cell)])
|
|
120
|
+
.map(([d, cell], index) => [d, getNearestCell(d, cell, index)])
|
|
115
121
|
.map(([d, cell]) => ({ d, cell, show: -d3__namespace.polygonArea(cell) > threshold }));
|
|
116
122
|
|
|
117
123
|
// cells can be null when we have duplicated coords
|
|
118
124
|
// https://github.com/d3/d3-delaunay/issues/106
|
|
119
|
-
function getNearestCell(d, cell) {
|
|
125
|
+
function getNearestCell(d, cell, index) {
|
|
120
126
|
if (!cell) {
|
|
121
|
-
const i = delaunay.find(x(d), y(d));
|
|
127
|
+
const i = delaunay.find(x(d, index), y(d, index));
|
|
122
128
|
if (i === -1) {
|
|
123
|
-
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));
|
|
124
130
|
return null;
|
|
125
131
|
}
|
|
126
132
|
cell = cells[i][1];
|
|
@@ -151,13 +157,25 @@ function smartLabels(
|
|
|
151
157
|
.attr("stroke", "none");
|
|
152
158
|
|
|
153
159
|
const orient = {
|
|
154
|
-
top: (text) =>
|
|
160
|
+
top: (text) =>
|
|
161
|
+
text
|
|
162
|
+
.attr("text-anchor", "middle")
|
|
163
|
+
.attr("y", (d, i) => -(r(d, i) + padding)),
|
|
155
164
|
right: (text) =>
|
|
156
|
-
text
|
|
165
|
+
text
|
|
166
|
+
.attr("text-anchor", "start")
|
|
167
|
+
.attr("dy", "0.35em")
|
|
168
|
+
.attr("x", (d, i) => r(d, i) + padding),
|
|
157
169
|
bottom: (text) =>
|
|
158
|
-
text
|
|
170
|
+
text
|
|
171
|
+
.attr("text-anchor", "middle")
|
|
172
|
+
.attr("dy", "0.71em")
|
|
173
|
+
.attr("y", (d, i) => r(d, i) + padding),
|
|
159
174
|
left: (text) =>
|
|
160
|
-
text
|
|
175
|
+
text
|
|
176
|
+
.attr("text-anchor", "end")
|
|
177
|
+
.attr("dy", "0.35em")
|
|
178
|
+
.attr("x", (d, i) => -(r(d, i) + padding)),
|
|
161
179
|
};
|
|
162
180
|
|
|
163
181
|
if (showAnchors || labelsInCentroids) {
|
|
@@ -174,8 +192,10 @@ function smartLabels(
|
|
|
174
192
|
.join("path")
|
|
175
193
|
.attr("class", "anchor")
|
|
176
194
|
.attr("display", ({ show }) => (show ? null : "none"))
|
|
177
|
-
.attr("d", ({ d, cell }) =>
|
|
178
|
-
cell
|
|
195
|
+
.attr("d", ({ d, cell }, index) =>
|
|
196
|
+
cell
|
|
197
|
+
? `M${d3__namespace.polygonCentroid(cell)}L${x(d, index)},${y(d, index)}`
|
|
198
|
+
: null
|
|
179
199
|
);
|
|
180
200
|
}
|
|
181
201
|
|
|
@@ -218,11 +238,14 @@ function smartLabels(
|
|
|
218
238
|
.data((d) => d)
|
|
219
239
|
.join("text")
|
|
220
240
|
.style("font", font)
|
|
221
|
-
.each(function ({ d, cell }) {
|
|
241
|
+
.each(function ({ d, cell }, index) {
|
|
222
242
|
if (!cell) return;
|
|
223
243
|
const [cx, cy] = d3__namespace.polygonCentroid(cell);
|
|
224
244
|
const angle =
|
|
225
|
-
(Math.round(
|
|
245
|
+
(Math.round(
|
|
246
|
+
(Math.atan2(cy - y(d, index), cx - x(d, index)) / Math.PI) * 2
|
|
247
|
+
) +
|
|
248
|
+
4) %
|
|
226
249
|
4;
|
|
227
250
|
|
|
228
251
|
d3__namespace.select(this).call(
|
|
@@ -235,13 +258,13 @@ function smartLabels(
|
|
|
235
258
|
: orient.left
|
|
236
259
|
);
|
|
237
260
|
})
|
|
238
|
-
.attr("transform", ({ d, cell }) => {
|
|
261
|
+
.attr("transform", ({ d, cell }, index) => {
|
|
239
262
|
if (labelsInCentroids) {
|
|
240
263
|
const [cx, cy] = d3__namespace.polygonCentroid(cell);
|
|
241
264
|
|
|
242
265
|
return `translate(${cx}, ${cy})`;
|
|
243
266
|
} else {
|
|
244
|
-
return `translate(${x(d)}, ${y(d)})`;
|
|
267
|
+
return `translate(${x(d, index)}, ${y(d, index)})`;
|
|
245
268
|
}
|
|
246
269
|
})
|
|
247
270
|
.attr("display", ({ show }) => (show ? null : "none"))
|
|
@@ -301,20 +324,26 @@ function smartLabels(
|
|
|
301
324
|
}
|
|
302
325
|
} // renderSVG
|
|
303
326
|
|
|
304
|
-
function
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
+
);
|
|
311
340
|
if (showAnchors || labelsInCentroids) {
|
|
312
|
-
cells.forEach(({ d, cell, show }) => {
|
|
341
|
+
cells.forEach(({ d, cell, show }, index) => {
|
|
313
342
|
if (!show) return;
|
|
314
343
|
const [cx, cy] = d3__namespace.polygonCentroid(cell);
|
|
315
344
|
context.beginPath();
|
|
316
345
|
context.moveTo(cx, cy);
|
|
317
|
-
context.lineTo(x(d), y(d));
|
|
346
|
+
context.lineTo(x(d, index), y(d, index));
|
|
318
347
|
context.strokeStyle = anchorsStroke;
|
|
319
348
|
context.stroke();
|
|
320
349
|
});
|
|
@@ -329,23 +358,40 @@ function smartLabels(
|
|
|
329
358
|
|
|
330
359
|
if (showPoints) {
|
|
331
360
|
context.beginPath();
|
|
332
|
-
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();
|
|
333
370
|
context.fillStyle = pointsFill;
|
|
334
371
|
context.fill();
|
|
335
372
|
context.strokeStyle = pointsStroke;
|
|
336
373
|
context.stroke();
|
|
337
374
|
}
|
|
375
|
+
} // renderCanvasAnchorsVoronoiPoints
|
|
338
376
|
|
|
339
|
-
|
|
340
|
-
|
|
377
|
+
function renderCanvasLabels(context, selected) {
|
|
378
|
+
cells.forEach(({ d, cell, show }, index) => {
|
|
379
|
+
if (!show && index !== selected) return;
|
|
341
380
|
const [cx, cy] = d3__namespace.polygonCentroid(cell);
|
|
342
381
|
const angle =
|
|
343
|
-
(Math.round(
|
|
382
|
+
(Math.round(
|
|
383
|
+
(Math.atan2(cy - y(d, index), cx - x(d, index)) / Math.PI) * 2
|
|
384
|
+
) +
|
|
385
|
+
4) %
|
|
386
|
+
4;
|
|
344
387
|
|
|
345
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;
|
|
346
392
|
context.translate(
|
|
347
|
-
labelsInCentroids ? cx : x(d),
|
|
348
|
-
labelsInCentroids ? cy : y(d)
|
|
393
|
+
labelsInCentroids ? cx : x(d, index) + dx,
|
|
394
|
+
labelsInCentroids ? cy : y(d, index) + dy
|
|
349
395
|
);
|
|
350
396
|
context.textAlign =
|
|
351
397
|
angle === 0
|
|
@@ -363,16 +409,23 @@ function smartLabels(
|
|
|
363
409
|
: angle === 2
|
|
364
410
|
? "middle"
|
|
365
411
|
: "bottom";
|
|
366
|
-
context.font = font;
|
|
412
|
+
context.font = index === selected ? hoverFont : font;
|
|
367
413
|
context.fillStyle = fill;
|
|
368
414
|
context.strokeStyle = stroke;
|
|
369
415
|
context.lineWidth = strokeWidth;
|
|
370
|
-
|
|
371
|
-
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);
|
|
372
419
|
context.restore();
|
|
373
420
|
});
|
|
421
|
+
} // renderCanvasLabels
|
|
422
|
+
|
|
423
|
+
function renderCanvas() {
|
|
424
|
+
const context = target.node().getContext("2d");
|
|
425
|
+
|
|
426
|
+
renderCanvasAnchorsVoronoiPoints(context);
|
|
427
|
+
renderCanvasLabels(context, selected);
|
|
374
428
|
|
|
375
|
-
let selected = null;
|
|
376
429
|
if (hover) {
|
|
377
430
|
if (debug) console.log("setting onmousemove", target);
|
|
378
431
|
|
|
@@ -383,44 +436,9 @@ function smartLabels(
|
|
|
383
436
|
selected = i;
|
|
384
437
|
|
|
385
438
|
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
|
-
|
|
439
|
+
renderCanvasAnchorsVoronoiPoints(context);
|
|
423
440
|
if (onHover && typeof onHover === "function") onHover(i);
|
|
441
|
+
renderCanvasLabels(context, selected);
|
|
424
442
|
});
|
|
425
443
|
}
|
|
426
444
|
} // renderCanvas
|
|
@@ -477,7 +495,6 @@ function intersectRect(a, b) {
|
|
|
477
495
|
);
|
|
478
496
|
}
|
|
479
497
|
|
|
480
|
-
|
|
481
|
-
exports.smartLabels = smartLabels;
|
|
498
|
+
return smartLabels;
|
|
482
499
|
|
|
483
500
|
}));
|
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?e(
|
|
1
|
+
// smart-labels v0.0.2 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:j="none",useOcclusion:P=!0,occludedStyle:C="opacity: 0.2",showPoints:L=!1,pointsFill:S="#ccc",pointsSelectedFill:M="firebrick",pointsStroke:O="#ccc",renderer:$="svg",debug:T=!1,selected:F=null,padding:I=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 E=o.extent(t,e),B=o.extent(t,r);d=d||E[1]-E[0],h=h||B[1]-B[0],T&&console.log("✅ smartLabels renderer",$),"canvas"===$.toLocaleLowerCase()?(f=o.select(f)||o.create("canvas").attr("width",d).attr("height",h),P=!1):f=f||o.create("svg").attr("viewBox",[0,0,d,h]);const D=o.Delaunay.from(t,e,r),V=D.voronoi([E[0]-1,B[0]-1,E[1]+1,B[1]+1]);let R=t.map(((t,e)=>[t,V.cellPolygon(e)]));function W(t,o,n){if(!o){const l=D.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=R[l][1]}return o}function q(n){T&&console.log("renderCanvasAnchorsVoronoiPoints","showAnchors",k,"labelsInCentroids",b,"showVoronoi",x,"showPoints",L),(k||b)&&R.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(),V.render(n),n.strokeStyle=w,n.stroke()),L&&(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 z(t,n){R.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)+I,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 R=R.map((([t,e],o)=>[t,W(t,e,o)])).map((([t,e])=>({d:t,cell:e,show:-o.polygonArea(e)>c}))),"canvas"===$.toLocaleLowerCase()?function(){const t=f.node().getContext("2d");q(t),z(t,F),g&&(T&&console.log("setting onmousemove",f),f.on("mousemove",(e=>{const[n,r]=o.pointer(e),l=D.find(n,r);l!==F&&(F=l,t.clearRect(0,0,d,h),q(t),p&&"function"==typeof p&&p(l),z(t,F))})))}():function(){let t=null;P&&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"),$={top:t=>t.attr("text-anchor","middle").attr("y",((t,e)=>-(l(t,e)+I))),right:t=>t.attr("text-anchor","start").attr("dy","0.35em").attr("x",((t,e)=>l(t,e)+I)),bottom:t=>t.attr("text-anchor","middle").attr("dy","0.71em").attr("y",((t,e)=>l(t,e)+I)),left:t=>t.attr("text-anchor","end").attr("dy","0.35em").attr("x",((t,e)=>-(l(t,e)+I)))};let T;(k||b)&&(t=f.selectAll("g#anchors").data([R]).join("g").attr("pointer-events","none").attr("id","anchors").attr("stroke",A).attr("fill",j).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",V.render()),L&&(T=f.selectAll("circle#points").data([1]).join("path").attr("id","points").attr("pointer-events","none").attr("stroke",O).attr("fill",S).attr("d",D.renderPoints(null,2)));const F=f.selectAll("g.labels").data([R]).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?$.right:3===i?$.top:1===i?$.bottom:$.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 E=null;if(g&&c.on("mousemove",(e=>{const n=D.find(...o.pointer(e));n!==E&&(E=n,F.attr("display",(({show:t},e)=>e===E||t?null:"none")).style("font",((t,e)=>e===E?y:u)).classed("selected",((t,e)=>e===E)).filter(((t,e)=>e===E)).raise(),t&&t.attr("display",(({show:t},e)=>t||e===E?null:"none")),L&&T.attr("fill",((t,e)=>M)).classed("selected",((t,e)=>e===E)),p&&"function"==typeof p&&p(n))})),P){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:D,voronoi:V,cells:R})}}));
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -15,11 +15,12 @@ import * as d3 from "d3";
|
|
|
15
15
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
16
16
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
17
17
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
18
|
-
export function smartLabels(
|
|
18
|
+
export default function smartLabels(
|
|
19
19
|
data,
|
|
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 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),
|
|
@@ -86,16 +92,16 @@ export function smartLabels(
|
|
|
86
92
|
let cells = data.map((d, i) => [d, voronoi.cellPolygon(i)]);
|
|
87
93
|
// Replace null cells with the nearest one
|
|
88
94
|
cells = cells
|
|
89
|
-
.map(([d, cell]) => [d, getNearestCell(d, cell)])
|
|
95
|
+
.map(([d, cell], index) => [d, getNearestCell(d, cell, index)])
|
|
90
96
|
.map(([d, cell]) => ({ d, cell, show: -d3.polygonArea(cell) > threshold }));
|
|
91
97
|
|
|
92
98
|
// cells can be null when we have duplicated coords
|
|
93
99
|
// https://github.com/d3/d3-delaunay/issues/106
|
|
94
|
-
function getNearestCell(d, cell) {
|
|
100
|
+
function getNearestCell(d, cell, index) {
|
|
95
101
|
if (!cell) {
|
|
96
|
-
const i = delaunay.find(x(d), y(d));
|
|
102
|
+
const i = delaunay.find(x(d, index), y(d, index));
|
|
97
103
|
if (i === -1) {
|
|
98
|
-
console.log("couldn't find cell", i, d, x(d), y(d));
|
|
104
|
+
console.log("couldn't find cell", i, d, x(d, index), y(d, index));
|
|
99
105
|
return null;
|
|
100
106
|
}
|
|
101
107
|
cell = cells[i][1];
|
|
@@ -126,13 +132,25 @@ export function smartLabels(
|
|
|
126
132
|
.attr("stroke", "none");
|
|
127
133
|
|
|
128
134
|
const orient = {
|
|
129
|
-
top: (text) =>
|
|
135
|
+
top: (text) =>
|
|
136
|
+
text
|
|
137
|
+
.attr("text-anchor", "middle")
|
|
138
|
+
.attr("y", (d, i) => -(r(d, i) + padding)),
|
|
130
139
|
right: (text) =>
|
|
131
|
-
text
|
|
140
|
+
text
|
|
141
|
+
.attr("text-anchor", "start")
|
|
142
|
+
.attr("dy", "0.35em")
|
|
143
|
+
.attr("x", (d, i) => r(d, i) + padding),
|
|
132
144
|
bottom: (text) =>
|
|
133
|
-
text
|
|
145
|
+
text
|
|
146
|
+
.attr("text-anchor", "middle")
|
|
147
|
+
.attr("dy", "0.71em")
|
|
148
|
+
.attr("y", (d, i) => r(d, i) + padding),
|
|
134
149
|
left: (text) =>
|
|
135
|
-
text
|
|
150
|
+
text
|
|
151
|
+
.attr("text-anchor", "end")
|
|
152
|
+
.attr("dy", "0.35em")
|
|
153
|
+
.attr("x", (d, i) => -(r(d, i) + padding)),
|
|
136
154
|
};
|
|
137
155
|
|
|
138
156
|
if (showAnchors || labelsInCentroids) {
|
|
@@ -149,8 +167,10 @@ export function smartLabels(
|
|
|
149
167
|
.join("path")
|
|
150
168
|
.attr("class", "anchor")
|
|
151
169
|
.attr("display", ({ show }) => (show ? null : "none"))
|
|
152
|
-
.attr("d", ({ d, cell }) =>
|
|
153
|
-
cell
|
|
170
|
+
.attr("d", ({ d, cell }, index) =>
|
|
171
|
+
cell
|
|
172
|
+
? `M${d3.polygonCentroid(cell)}L${x(d, index)},${y(d, index)}`
|
|
173
|
+
: null
|
|
154
174
|
);
|
|
155
175
|
}
|
|
156
176
|
|
|
@@ -193,11 +213,14 @@ export function smartLabels(
|
|
|
193
213
|
.data((d) => d)
|
|
194
214
|
.join("text")
|
|
195
215
|
.style("font", font)
|
|
196
|
-
.each(function ({ d, cell }) {
|
|
216
|
+
.each(function ({ d, cell }, index) {
|
|
197
217
|
if (!cell) return;
|
|
198
218
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
199
219
|
const angle =
|
|
200
|
-
(Math.round(
|
|
220
|
+
(Math.round(
|
|
221
|
+
(Math.atan2(cy - y(d, index), cx - x(d, index)) / Math.PI) * 2
|
|
222
|
+
) +
|
|
223
|
+
4) %
|
|
201
224
|
4;
|
|
202
225
|
|
|
203
226
|
d3.select(this).call(
|
|
@@ -210,13 +233,13 @@ export function smartLabels(
|
|
|
210
233
|
: orient.left
|
|
211
234
|
);
|
|
212
235
|
})
|
|
213
|
-
.attr("transform", ({ d, cell }) => {
|
|
236
|
+
.attr("transform", ({ d, cell }, index) => {
|
|
214
237
|
if (labelsInCentroids) {
|
|
215
238
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
216
239
|
|
|
217
240
|
return `translate(${cx}, ${cy})`;
|
|
218
241
|
} else {
|
|
219
|
-
return `translate(${x(d)}, ${y(d)})`;
|
|
242
|
+
return `translate(${x(d, index)}, ${y(d, index)})`;
|
|
220
243
|
}
|
|
221
244
|
})
|
|
222
245
|
.attr("display", ({ show }) => (show ? null : "none"))
|
|
@@ -276,20 +299,26 @@ export function smartLabels(
|
|
|
276
299
|
}
|
|
277
300
|
} // renderSVG
|
|
278
301
|
|
|
279
|
-
function
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
302
|
+
function renderCanvasAnchorsVoronoiPoints(context) {
|
|
303
|
+
if (debug)
|
|
304
|
+
console.log(
|
|
305
|
+
"renderCanvasAnchorsVoronoiPoints",
|
|
306
|
+
"showAnchors",
|
|
307
|
+
showAnchors,
|
|
308
|
+
"labelsInCentroids",
|
|
309
|
+
labelsInCentroids,
|
|
310
|
+
"showVoronoi",
|
|
311
|
+
showVoronoi,
|
|
312
|
+
"showPoints",
|
|
313
|
+
showPoints
|
|
314
|
+
);
|
|
286
315
|
if (showAnchors || labelsInCentroids) {
|
|
287
|
-
cells.forEach(({ d, cell, show }) => {
|
|
316
|
+
cells.forEach(({ d, cell, show }, index) => {
|
|
288
317
|
if (!show) return;
|
|
289
318
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
290
319
|
context.beginPath();
|
|
291
320
|
context.moveTo(cx, cy);
|
|
292
|
-
context.lineTo(x(d), y(d));
|
|
321
|
+
context.lineTo(x(d, index), y(d, index));
|
|
293
322
|
context.strokeStyle = anchorsStroke;
|
|
294
323
|
context.stroke();
|
|
295
324
|
});
|
|
@@ -304,23 +333,40 @@ export function smartLabels(
|
|
|
304
333
|
|
|
305
334
|
if (showPoints) {
|
|
306
335
|
context.beginPath();
|
|
307
|
-
delaunay.renderPoints(context, 2);
|
|
336
|
+
// delaunay.renderPoints(context, 2);
|
|
337
|
+
context.save();
|
|
338
|
+
context.globalAlpha = 0.7;
|
|
339
|
+
context.fillStyle = "black";
|
|
340
|
+
data.forEach((d, i) => {
|
|
341
|
+
context.beginPath();
|
|
342
|
+
context.arc(x(d, i), y(d, i), r(d, i), 0, 2 * Math.PI);
|
|
343
|
+
});
|
|
344
|
+
context.restore();
|
|
308
345
|
context.fillStyle = pointsFill;
|
|
309
346
|
context.fill();
|
|
310
347
|
context.strokeStyle = pointsStroke;
|
|
311
348
|
context.stroke();
|
|
312
349
|
}
|
|
350
|
+
} // renderCanvasAnchorsVoronoiPoints
|
|
313
351
|
|
|
314
|
-
|
|
315
|
-
|
|
352
|
+
function renderCanvasLabels(context, selected) {
|
|
353
|
+
cells.forEach(({ d, cell, show }, index) => {
|
|
354
|
+
if (!show && index !== selected) return;
|
|
316
355
|
const [cx, cy] = d3.polygonCentroid(cell);
|
|
317
356
|
const angle =
|
|
318
|
-
(Math.round(
|
|
357
|
+
(Math.round(
|
|
358
|
+
(Math.atan2(cy - y(d, index), cx - x(d, index)) / Math.PI) * 2
|
|
359
|
+
) +
|
|
360
|
+
4) %
|
|
361
|
+
4;
|
|
319
362
|
|
|
320
363
|
context.save();
|
|
364
|
+
const dr = r(d, index) + padding;
|
|
365
|
+
const dx = angle === 0 ? dr : angle === 2 ? -dr : dr;
|
|
366
|
+
const dy = angle === 1 ? dr : angle === 3 ? -dr : dr;
|
|
321
367
|
context.translate(
|
|
322
|
-
labelsInCentroids ? cx : x(d),
|
|
323
|
-
labelsInCentroids ? cy : y(d)
|
|
368
|
+
labelsInCentroids ? cx : x(d, index) + dx,
|
|
369
|
+
labelsInCentroids ? cy : y(d, index) + dy
|
|
324
370
|
);
|
|
325
371
|
context.textAlign =
|
|
326
372
|
angle === 0
|
|
@@ -338,16 +384,23 @@ export function smartLabels(
|
|
|
338
384
|
: angle === 2
|
|
339
385
|
? "middle"
|
|
340
386
|
: "bottom";
|
|
341
|
-
context.font = font;
|
|
387
|
+
context.font = index === selected ? hoverFont : font;
|
|
342
388
|
context.fillStyle = fill;
|
|
343
389
|
context.strokeStyle = stroke;
|
|
344
390
|
context.lineWidth = strokeWidth;
|
|
345
|
-
|
|
346
|
-
context.
|
|
391
|
+
// if (debug) console.log(index, dx, dy, angle);
|
|
392
|
+
context.strokeText(label(d, index), 0, 0);
|
|
393
|
+
context.fillText(label(d, index), 0, 0);
|
|
347
394
|
context.restore();
|
|
348
395
|
});
|
|
396
|
+
} // renderCanvasLabels
|
|
397
|
+
|
|
398
|
+
function renderCanvas() {
|
|
399
|
+
const context = target.node().getContext("2d");
|
|
400
|
+
|
|
401
|
+
renderCanvasAnchorsVoronoiPoints(context);
|
|
402
|
+
renderCanvasLabels(context, selected);
|
|
349
403
|
|
|
350
|
-
let selected = null;
|
|
351
404
|
if (hover) {
|
|
352
405
|
if (debug) console.log("setting onmousemove", target);
|
|
353
406
|
|
|
@@ -358,44 +411,9 @@ export function smartLabels(
|
|
|
358
411
|
selected = i;
|
|
359
412
|
|
|
360
413
|
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
|
-
|
|
414
|
+
renderCanvasAnchorsVoronoiPoints(context);
|
|
398
415
|
if (onHover && typeof onHover === "function") onHover(i);
|
|
416
|
+
renderCanvasLabels(context, selected);
|
|
399
417
|
});
|
|
400
418
|
}
|
|
401
419
|
} // renderCanvas
|
|
@@ -424,7 +442,7 @@ export function smartLabels(
|
|
|
424
442
|
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
425
443
|
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
426
444
|
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
427
|
-
|
|
445
|
+
function occlusion(svg, against = "text") {
|
|
428
446
|
const nodes = d3
|
|
429
447
|
.sort(svg.selectAll(against), (node) => +node.getAttribute("data-priority"))
|
|
430
448
|
.reverse()
|