smart-labels 0.0.2 → 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.
@@ -1,4 +1,4 @@
1
- // smart-labels v0.0.1 Copyright (c) 2024 John Alexis Guerra Gómez
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) !== undefined && x(d) !== null && y(d) !== undefined && y(d) !== null
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) => text.attr("text-anchor", "middle").attr("y", -6),
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.attr("text-anchor", "start").attr("dy", "0.35em").attr("x", 6),
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.attr("text-anchor", "middle").attr("dy", "0.71em").attr("y", 6),
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.attr("text-anchor", "end").attr("dy", "0.35em").attr("x", -6),
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 ? `M${d3.polygonCentroid(cell)}L${x(d)},${y(d)}` : null
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((Math.atan2(cy - y(d), cx - x(d)) / Math.PI) * 2) + 4) %
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 renderCanvas() {
282
- const context = target.node().getContext("2d");
283
- // const ratio = window.devicePixelRatio || 1;
284
- // target.attr("width", width * ratio).attr("height", height * ratio);
285
- // context.scale(ratio, ratio);
286
- // context.clearRect(0, 0, width, height);
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
- cells.forEach(({ d, cell, show }) => {
317
- if (!show) return;
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((Math.atan2(cy - y(d), cx - x(d)) / Math.PI) * 2) + 4) % 4;
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
- context.strokeText(label(d), 0, 0);
348
- context.fillText(label(d), 0, 0);
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
- cells.forEach(({ d, cell, show }, index) => {
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
@@ -1,4 +1,4 @@
1
- // smart-labels v0.0.1 Copyright (c) 2024 John Alexis Guerra Gómez
1
+ // smart-labels v0.0.2 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) !== undefined && x(d) !== null && y(d) !== undefined && y(d) !== null
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) => text.attr("text-anchor", "middle").attr("y", -6),
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.attr("text-anchor", "start").attr("dy", "0.35em").attr("x", 6),
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.attr("text-anchor", "middle").attr("dy", "0.71em").attr("y", 6),
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.attr("text-anchor", "end").attr("dy", "0.35em").attr("x", -6),
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 ? `M${d3__namespace.polygonCentroid(cell)}L${x(d)},${y(d)}` : null
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((Math.atan2(cy - y(d), cx - x(d)) / Math.PI) * 2) + 4) %
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 renderCanvas() {
305
- const context = target.node().getContext("2d");
306
- // const ratio = window.devicePixelRatio || 1;
307
- // target.attr("width", width * ratio).attr("height", height * ratio);
308
- // context.scale(ratio, ratio);
309
- // context.clearRect(0, 0, width, height);
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
- cells.forEach(({ d, cell, show }) => {
340
- if (!show) return;
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((Math.atan2(cy - y(d), cx - x(d)) / Math.PI) * 2) + 4) % 4;
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
- context.strokeText(label(d), 0, 0);
371
- context.fillText(label(d), 0, 0);
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
- cells.forEach(({ d, cell, show }, index) => {
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
@@ -1,2 +1,2 @@
1
- // smart-labels v0.0.1 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],label:l=(t,e)=>e,fill:a="#333",stroke:s="white",threshold:i=2e3,width:c=null,height:d=null,target:h=null,font:f="10px sans-serif",hover:u=!0,onHover:p=t=>t,hoverFont:g="bolder 12px sans-serif",labelsInCentroids:y=!0,backgroundFill:m="#fefefe01",strokeWidth:b=5,showVoronoi:x=!1,voronoiStroke:v="#ccc",showAnchors:k=!1,anchorsStroke:w="orange",anchorsFill:j="none",useOcclusion:A=!0,occludedStyle:S="opacity: 0.2",showPoints:L=!1,pointsFill:P="#ccc",pointsSelectedFill:C="firebrick",pointsStroke:M="#ccc",renderer:O="svg",debug:T=!1}={}){t=t.filter((t=>void 0!==e(t)&&null!==e(t)&&void 0!==r(t)&&null!==r(t)));let $=o.extent(t,e),F=o.extent(t,r);c=c||$[1]-$[0],d=d||F[1]-F[0],T&&console.log("✅ smartLabels renderer",O),"canvas"===O.toLocaleLowerCase()?(h=o.select(h)||o.create("canvas").attr("width",c).attr("height",d),A=!1):h=h||o.create("svg").attr("viewBox",[0,0,c,d]);const B=o.Delaunay.from(t,e,r),E=B.voronoi([$[0]-1,F[0]-1,$[1]+1,F[1]+1]);let I=t.map(((t,e)=>[t,E.cellPolygon(e)]));function D(t,o){if(!o){const n=B.find(e(t),r(t));if(-1===n)return console.log("couldn't find cell",n,t,e(t),r(t)),null;o=I[n][1]}return o}return I=I.map((([t,e])=>[t,D(t,e)])).map((([t,e])=>({d:t,cell:e,show:-o.polygonArea(e)>i}))),"canvas"===O.toLocaleLowerCase()?function(){const t=h.node().getContext("2d");(k||y)&&I.forEach((({d:n,cell:l,show:a})=>{if(!a)return;const[s,i]=o.polygonCentroid(l);t.beginPath(),t.moveTo(s,i),t.lineTo(e(n),r(n)),t.strokeStyle=w,t.stroke()})),x&&(t.beginPath(),E.render(t),t.strokeStyle=v,t.stroke()),L&&(t.beginPath(),B.renderPoints(t,2),t.fillStyle=P,t.fill(),t.strokeStyle=M,t.stroke()),I.forEach((({d:n,cell:i,show:c})=>{if(!c)return;const[d,h]=o.polygonCentroid(i),u=(Math.round(Math.atan2(h-r(n),d-e(n))/Math.PI*2)+4)%4;t.save(),t.translate(y?d:e(n),y?h:r(n)),t.textAlign=0===u?"left":1===u?"center":2===u?"right":"center",t.textBaseline=0===u?"middle":1===u?"top":2===u?"middle":"bottom",t.font=f,t.fillStyle=a,t.strokeStyle=s,t.lineWidth=b,t.strokeText(l(n),0,0),t.fillText(l(n),0,0),t.restore()}));let n=null;u&&(T&&console.log("setting onmousemove",h),h.on("mousemove",(i=>{const[h,u]=o.pointer(i),m=B.find(h,u);m!==n&&(n=m,t.clearRect(0,0,c,d),I.forEach((({d:i,cell:c,show:d},h)=>{if(!d&&h!==n)return;const[u,p]=o.polygonCentroid(c),m=(Math.round(Math.atan2(p-r(i),u-e(i))/Math.PI*2)+4)%4;t.save(),t.translate(y?u:e(i),y?p:r(i)),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=h===n?g:f,t.fillStyle=a,t.strokeStyle=s,t.lineWidth=b,t.strokeText(l(i),0,0),t.fillText(l(i),0,0),t.restore()})),p&&"function"==typeof p&&p(m))})))}():function(){let t=null;A&&h.selectAll("style.smartLabels").data([0]).join("style").attr("class","smartLabels").html(`\n svg g.labels > text.occluded:not(.selected) { ${S} }\n `);const i=h.selectAll("rect.smartLabels").data([0]).join("rect").attr("class","smartLabels").attr("width",c).attr("height",d).attr("fill",m).attr("stroke","none"),O={top:t=>t.attr("text-anchor","middle").attr("y",-6),right:t=>t.attr("text-anchor","start").attr("dy","0.35em").attr("x",6),bottom:t=>t.attr("text-anchor","middle").attr("dy","0.71em").attr("y",6),left:t=>t.attr("text-anchor","end").attr("dy","0.35em").attr("x",-6)};let T;(k||y)&&(t=h.selectAll("g#anchors").data([I]).join("g").attr("pointer-events","none").attr("id","anchors").attr("stroke",w).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})=>n?`M${o.polygonCentroid(n)}L${e(t)},${r(t)}`:null))),x&&h.selectAll("path#voronoi").data([1]).join("path").attr("id","voronoi").attr("stroke",v).attr("pointer-events","none").attr("d",E.render()),L&&(T=h.selectAll("circle#points").data([1]).join("path").attr("id","points").attr("pointer-events","none").attr("stroke",M).attr("fill",P).attr("d",B.renderPoints(null,2)));const $=h.selectAll("g.labels").data([I]).join("g").attr("class","labels").attr("fill",a).attr("stroke",s).attr("stroke-width",b).attr("paint-order","stroke").attr("pointer-events","none").selectAll("text").data((t=>t)).join("text").style("font",f).each((function({d:t,cell:n}){if(!n)return;const[l,a]=o.polygonCentroid(n),s=(Math.round(Math.atan2(a-r(t),l-e(t))/Math.PI*2)+4)%4;o.select(this).call(0===s?O.right:3===s?O.top:1===s?O.bottom:O.left)})).attr("transform",(({d:t,cell:n})=>{if(y){const[t,e]=o.polygonCentroid(n);return`translate(${t}, ${e})`}return`translate(${e(t)}, ${r(t)})`})).attr("display",(({show:t})=>t?null:"none")).text((({d:t},e,o)=>l(t,e,o)));let F=null;if(u&&i.on("mousemove",(e=>{const n=B.find(...o.pointer(e));n!==F&&(F=n,$.attr("display",(({show:t},e)=>e===F||t?null:"none")).style("font",((t,e)=>e===F?g:f)).classed("selected",((t,e)=>e===F)).filter(((t,e)=>e===F)).raise(),t&&t.attr("display",(({show:t},e)=>t||e===F?null:"none")),L&&T.attr("fill",((t,e)=>C)).classed("selected",((t,e)=>e===F)),p&&"function"==typeof p&&p(n))})),A){const t=new MutationObserver((function(){document.contains(h.node())&&(h.call(n,"g.labels > text"),t.disconnect())}));t.observe(document,{attributes:!1,childList:!0,characterData:!1,subtree:!0})}}(),Object.assign(h.node(),{delaunay:B,voronoi:E,cells:I})}}));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-labels",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "A basic library for adding labels to a visualization when there is enough space",
5
5
  "type": "module",
6
6
  "main": "dist/smartLabels.min.js",
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) !== undefined && x(d) !== null && y(d) !== undefined && y(d) !== null
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 default 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 default function smartLabels(
126
132
  .attr("stroke", "none");
127
133
 
128
134
  const orient = {
129
- top: (text) => text.attr("text-anchor", "middle").attr("y", -6),
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.attr("text-anchor", "start").attr("dy", "0.35em").attr("x", 6),
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.attr("text-anchor", "middle").attr("dy", "0.71em").attr("y", 6),
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.attr("text-anchor", "end").attr("dy", "0.35em").attr("x", -6),
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 default 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 ? `M${d3.polygonCentroid(cell)}L${x(d)},${y(d)}` : null
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 default 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((Math.atan2(cy - y(d), cx - x(d)) / Math.PI) * 2) + 4) %
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 default 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 default function smartLabels(
276
299
  }
277
300
  } // renderSVG
278
301
 
279
- function renderCanvas() {
280
- const context = target.node().getContext("2d");
281
- // const ratio = window.devicePixelRatio || 1;
282
- // target.attr("width", width * ratio).attr("height", height * ratio);
283
- // context.scale(ratio, ratio);
284
- // context.clearRect(0, 0, width, height);
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 default 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
- cells.forEach(({ d, cell, show }) => {
315
- if (!show) return;
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((Math.atan2(cy - y(d), cx - x(d)) / Math.PI) * 2) + 4) % 4;
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 default 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
- context.strokeText(label(d), 0, 0);
346
- context.fillText(label(d), 0, 0);
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 default function smartLabels(
358
411
  selected = i;
359
412
 
360
413
  context.clearRect(0, 0, width, height);
361
- cells.forEach(({ d, cell, show }, index) => {
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