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.
@@ -1,4 +1,4 @@
1
- // smart-labels v0.0.1 Copyright (c) 2024 John Alexis Guerra Gómez
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) !== 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),
@@ -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
- d3.create("canvas").attr("width", width).attr("height", height);
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) => text.attr("text-anchor", "middle").attr("y", -6),
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.attr("text-anchor", "start").attr("dy", "0.35em").attr("x", 6),
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.attr("text-anchor", "middle").attr("dy", "0.71em").attr("y", 6),
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.attr("text-anchor", "end").attr("dy", "0.35em").attr("x", -6),
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 ? `M${d3.polygonCentroid(cell)}L${x(d)},${y(d)}` : null
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((Math.atan2(cy - y(d), cx - x(d)) / Math.PI) * 2) + 4) %
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 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
-
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
- cells.forEach(({ d, cell, show }) => {
317
- if (!show) return;
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((Math.atan2(cy - y(d), cx - x(d)) / Math.PI) * 2) + 4) % 4;
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
- context.strokeText(label(d), 0, 0);
348
- context.fillText(label(d), 0, 0);
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
- 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
-
441
+ renderCanvasAnchorsVoronoiPoints(context);
400
442
  if (onHover && typeof onHover === "function") onHover(i);
443
+ renderCanvasLabels(context, selected);
401
444
  });
402
445
  }
403
446
  } // renderCanvas
@@ -1,4 +1,4 @@
1
- // smart-labels v0.0.1 Copyright (c) 2024 John Alexis Guerra Gómez
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) !== 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),
@@ -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
- d3__namespace.create("canvas").attr("width", width).attr("height", height);
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) => text.attr("text-anchor", "middle").attr("y", -6),
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.attr("text-anchor", "start").attr("dy", "0.35em").attr("x", 6),
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.attr("text-anchor", "middle").attr("dy", "0.71em").attr("y", 6),
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.attr("text-anchor", "end").attr("dy", "0.35em").attr("x", -6),
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 ? `M${d3__namespace.polygonCentroid(cell)}L${x(d)},${y(d)}` : null
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((Math.atan2(cy - y(d), cx - x(d)) / Math.PI) * 2) + 4) %
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 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
-
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
- cells.forEach(({ d, cell, show }) => {
340
- if (!show) return;
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((Math.atan2(cy - y(d), cx - x(d)) / Math.PI) * 2) + 4) % 4;
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
- context.strokeText(label(d), 0, 0);
371
- context.fillText(label(d), 0, 0);
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
- 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
-
464
+ renderCanvasAnchorsVoronoiPoints(context);
423
465
  if (onHover && typeof onHover === "function") onHover(i);
466
+ renderCanvasLabels(context, selected);
424
467
  });
425
468
  }
426
469
  } // 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.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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smart-labels",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
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),
@@ -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
- d3.create("canvas").attr("width", width).attr("height", height);
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) => 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)),
130
164
  right: (text) =>
131
- 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),
132
169
  bottom: (text) =>
133
- 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),
134
174
  left: (text) =>
135
- 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)),
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 ? `M${d3.polygonCentroid(cell)}L${x(d)},${y(d)}` : null
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((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) %
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 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
-
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
- cells.forEach(({ d, cell, show }) => {
315
- if (!show) return;
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((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;
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
- context.strokeText(label(d), 0, 0);
346
- 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);
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
- 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
-
439
+ renderCanvasAnchorsVoronoiPoints(context);
398
440
  if (onHover && typeof onHover === "function") onHover(i);
441
+ renderCanvasLabels(context, selected);
399
442
  });
400
443
  }
401
444
  } // renderCanvas