smart-labels 0.0.1

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.
@@ -0,0 +1,483 @@
1
+ // smart-labels v0.0.1 Copyright (c) 2024 John Alexis Guerra Gómez
2
+ (function (global, factory) {
3
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3')) :
4
+ typeof define === 'function' && define.amd ? define(['exports', 'd3'], factory) :
5
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.smartLabels = global.smartLabels || {}, global.d3));
6
+ })(this, (function (exports, d3) { 'use strict';
7
+
8
+ function _interopNamespaceDefault(e) {
9
+ var n = Object.create(null);
10
+ if (e) {
11
+ Object.keys(e).forEach(function (k) {
12
+ if (k !== 'default') {
13
+ var d = Object.getOwnPropertyDescriptor(e, k);
14
+ Object.defineProperty(n, k, d.get ? d : {
15
+ enumerable: true,
16
+ get: function () { return e[k]; }
17
+ });
18
+ }
19
+ });
20
+ }
21
+ n.default = e;
22
+ return Object.freeze(n);
23
+ }
24
+
25
+ var d3__namespace = /*#__PURE__*/_interopNamespaceDefault(d3);
26
+
27
+ // Based on https://observablehq.com/@d3/voronoi-labels by Mike Bostock
28
+
29
+ // ISC License
30
+
31
+ // Copyright 2018–2023 Observable, Inc.
32
+ // Permission to use, copy, modify, and/or distribute this software for any
33
+ // purpose with or without fee is hereby granted, provided that the above
34
+ // copyright notice and this permission notice appear in all copies.
35
+
36
+ // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
37
+ // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
38
+ // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
39
+ // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
40
+ // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
41
+ // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
42
+ // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
43
+ function smartLabels(
44
+ data,
45
+ {
46
+ x = (d) => d[0], // x coordinate accessor, expected to be in pixels
47
+ y = (d) => d[1], // y coordinate accessor, expected to be in pixels
48
+ label = (d, i) => i, // Accessor for the label
49
+ fill = "#333", // label fill color
50
+ stroke = "white", // label stroke color
51
+ threshold = 2000, // Areas over this size would get labels
52
+ width = null,
53
+ height = null,
54
+ target = null, // Where do you want it to draw
55
+ font = "10px sans-serif",
56
+ hover = true, // Show label of the hovered point
57
+ onHover = (i) => i, // callback when hovered, will pass the index of the selected element
58
+ hoverFont = "bolder 12px sans-serif",
59
+ labelsInCentroids = true,
60
+
61
+ backgroundFill = "#fefefe01", // What to paint the bg rect of the labels. Needed for the onHover
62
+ strokeWidth = 5,
63
+
64
+ showVoronoi = false,
65
+ voronoiStroke = "#ccc",
66
+
67
+ showAnchors = false,
68
+ anchorsStroke = "orange",
69
+ anchorsFill = "none",
70
+
71
+ useOcclusion = true,
72
+ occludedStyle = "opacity: 0.2", // css style rules to be used on occluded labels
73
+
74
+ // For debugging
75
+ showPoints = false,
76
+ pointsFill = "#ccc",
77
+ pointsSelectedFill = "firebrick",
78
+ pointsStroke = "#ccc",
79
+ renderer = "svg",
80
+ debug = false,
81
+ } = {}
82
+ ) {
83
+ data = data.filter(
84
+ (d) =>
85
+ x(d) !== undefined && x(d) !== null && y(d) !== undefined && y(d) !== null
86
+ );
87
+
88
+ let xExtent = d3__namespace.extent(data, x),
89
+ yExtent = d3__namespace.extent(data, y);
90
+ width = width || xExtent[1] - xExtent[0];
91
+ height = height || yExtent[1] - yExtent[0];
92
+
93
+ if (debug) console.log("✅ smartLabels renderer", renderer);
94
+ if (renderer.toLocaleLowerCase() === "canvas") {
95
+ target =
96
+ d3__namespace.select(target) ||
97
+ d3__namespace.create("canvas").attr("width", width).attr("height", height);
98
+ useOcclusion = false;
99
+ } else {
100
+ target = target || d3__namespace.create("svg").attr("viewBox", [0, 0, width, height]);
101
+ }
102
+
103
+ const delaunay = d3__namespace.Delaunay.from(data, x, y);
104
+ const voronoi = delaunay.voronoi([
105
+ xExtent[0] - 1,
106
+ yExtent[0] - 1,
107
+ xExtent[1] + 1,
108
+ yExtent[1] + 1,
109
+ ]);
110
+
111
+ let cells = data.map((d, i) => [d, voronoi.cellPolygon(i)]);
112
+ // Replace null cells with the nearest one
113
+ cells = cells
114
+ .map(([d, cell]) => [d, getNearestCell(d, cell)])
115
+ .map(([d, cell]) => ({ d, cell, show: -d3__namespace.polygonArea(cell) > threshold }));
116
+
117
+ // cells can be null when we have duplicated coords
118
+ // https://github.com/d3/d3-delaunay/issues/106
119
+ function getNearestCell(d, cell) {
120
+ if (!cell) {
121
+ const i = delaunay.find(x(d), y(d));
122
+ if (i === -1) {
123
+ console.log("couldn't find cell", i, d, x(d), y(d));
124
+ return null;
125
+ }
126
+ cell = cells[i][1];
127
+ }
128
+ return cell;
129
+ }
130
+
131
+ function renderSVG() {
132
+ let anchors = null;
133
+ if (useOcclusion) {
134
+ target
135
+ .selectAll("style.smartLabels")
136
+ .data([0])
137
+ .join("style")
138
+ .attr("class", "smartLabels").html(`
139
+ svg g.labels > text.occluded:not(.selected) { ${occludedStyle} }
140
+ `);
141
+ }
142
+
143
+ const mouseRect = target
144
+ .selectAll("rect.smartLabels")
145
+ .data([0])
146
+ .join("rect")
147
+ .attr("class", "smartLabels")
148
+ .attr("width", width)
149
+ .attr("height", height)
150
+ .attr("fill", backgroundFill)
151
+ .attr("stroke", "none");
152
+
153
+ const orient = {
154
+ top: (text) => text.attr("text-anchor", "middle").attr("y", -6),
155
+ right: (text) =>
156
+ text.attr("text-anchor", "start").attr("dy", "0.35em").attr("x", 6),
157
+ bottom: (text) =>
158
+ text.attr("text-anchor", "middle").attr("dy", "0.71em").attr("y", 6),
159
+ left: (text) =>
160
+ text.attr("text-anchor", "end").attr("dy", "0.35em").attr("x", -6),
161
+ };
162
+
163
+ if (showAnchors || labelsInCentroids) {
164
+ anchors = target
165
+ .selectAll("g#anchors")
166
+ .data([cells])
167
+ .join("g")
168
+ .attr("pointer-events", "none")
169
+ .attr("id", "anchors")
170
+ .attr("stroke", anchorsStroke)
171
+ .attr("fill", anchorsFill)
172
+ .selectAll("path.anchor")
173
+ .data((d) => d)
174
+ .join("path")
175
+ .attr("class", "anchor")
176
+ .attr("display", ({ show }) => (show ? null : "none"))
177
+ .attr("d", ({ d, cell }) =>
178
+ cell ? `M${d3__namespace.polygonCentroid(cell)}L${x(d)},${y(d)}` : null
179
+ );
180
+ }
181
+
182
+ if (showVoronoi) {
183
+ target
184
+ .selectAll("path#voronoi")
185
+ .data([1])
186
+ .join("path")
187
+ .attr("id", "voronoi")
188
+ .attr("stroke", voronoiStroke)
189
+ .attr("pointer-events", "none")
190
+ .attr("d", voronoi.render());
191
+ }
192
+
193
+ let points;
194
+ if (showPoints) {
195
+ points = target
196
+ .selectAll("circle#points")
197
+ .data([1])
198
+ .join("path")
199
+ .attr("id", "points")
200
+ .attr("pointer-events", "none")
201
+ .attr("stroke", pointsStroke)
202
+ .attr("fill", pointsFill)
203
+ .attr("d", delaunay.renderPoints(null, 2));
204
+ }
205
+
206
+ const labels = target
207
+ .selectAll("g.labels")
208
+ .data([cells])
209
+ .join("g")
210
+ .attr("class", "labels")
211
+ .attr("fill", fill)
212
+
213
+ .attr("stroke", stroke)
214
+ .attr("stroke-width", strokeWidth)
215
+ .attr("paint-order", "stroke")
216
+ .attr("pointer-events", "none")
217
+ .selectAll("text")
218
+ .data((d) => d)
219
+ .join("text")
220
+ .style("font", font)
221
+ .each(function ({ d, cell }) {
222
+ if (!cell) return;
223
+ const [cx, cy] = d3__namespace.polygonCentroid(cell);
224
+ const angle =
225
+ (Math.round((Math.atan2(cy - y(d), cx - x(d)) / Math.PI) * 2) + 4) %
226
+ 4;
227
+
228
+ d3__namespace.select(this).call(
229
+ angle === 0
230
+ ? orient.right
231
+ : angle === 3
232
+ ? orient.top
233
+ : angle === 1
234
+ ? orient.bottom
235
+ : orient.left
236
+ );
237
+ })
238
+ .attr("transform", ({ d, cell }) => {
239
+ if (labelsInCentroids) {
240
+ const [cx, cy] = d3__namespace.polygonCentroid(cell);
241
+
242
+ return `translate(${cx}, ${cy})`;
243
+ } else {
244
+ return `translate(${x(d)}, ${y(d)})`;
245
+ }
246
+ })
247
+ .attr("display", ({ show }) => (show ? null : "none"))
248
+ .text(({ d }, i, all) => label(d, i, all));
249
+
250
+ let selected = null;
251
+ if (hover) {
252
+ mouseRect.on("mousemove", (event) => {
253
+ // Find the selected point
254
+ const i = delaunay.find(...d3__namespace.pointer(event));
255
+ if (i === selected) return;
256
+ selected = i;
257
+
258
+ // Update labels
259
+ labels
260
+ .attr("display", ({ show }, i) =>
261
+ i === selected || show ? null : "none"
262
+ )
263
+ .style("font", (_, i) => (i === selected ? hoverFont : font))
264
+ .classed("selected", (_, i) => i === selected)
265
+ .filter((_, i) => i === selected)
266
+ .raise();
267
+
268
+ anchors &&
269
+ anchors.attr("display", ({ show }, i) =>
270
+ show || i === selected ? null : "none"
271
+ );
272
+
273
+ if (showPoints) {
274
+ points
275
+ .attr("fill", (_, i) =>
276
+ i === selected ? pointsSelectedFill : pointsSelectedFill
277
+ )
278
+ .classed("selected", (_, i) => i === selected);
279
+ }
280
+
281
+ if (onHover && typeof onHover === "function") onHover(i);
282
+ });
283
+ }
284
+
285
+ // Setup a MutationObserver to wait for the element to be rendered
286
+ // https://stackoverflow.com/questions/15875128/is-there-element-rendered-event
287
+ if (useOcclusion) {
288
+ const observer = new MutationObserver(function () {
289
+ if (document.contains(target.node())) {
290
+ target.call(occlusion, "g.labels > text");
291
+ observer.disconnect();
292
+ }
293
+ });
294
+
295
+ observer.observe(document, {
296
+ attributes: false,
297
+ childList: true,
298
+ characterData: false,
299
+ subtree: true,
300
+ });
301
+ }
302
+ } // renderSVG
303
+
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
+
311
+ if (showAnchors || labelsInCentroids) {
312
+ cells.forEach(({ d, cell, show }) => {
313
+ if (!show) return;
314
+ const [cx, cy] = d3__namespace.polygonCentroid(cell);
315
+ context.beginPath();
316
+ context.moveTo(cx, cy);
317
+ context.lineTo(x(d), y(d));
318
+ context.strokeStyle = anchorsStroke;
319
+ context.stroke();
320
+ });
321
+ }
322
+
323
+ if (showVoronoi) {
324
+ context.beginPath();
325
+ voronoi.render(context);
326
+ context.strokeStyle = voronoiStroke;
327
+ context.stroke();
328
+ }
329
+
330
+ if (showPoints) {
331
+ context.beginPath();
332
+ delaunay.renderPoints(context, 2);
333
+ context.fillStyle = pointsFill;
334
+ context.fill();
335
+ context.strokeStyle = pointsStroke;
336
+ context.stroke();
337
+ }
338
+
339
+ cells.forEach(({ d, cell, show }) => {
340
+ if (!show) return;
341
+ const [cx, cy] = d3__namespace.polygonCentroid(cell);
342
+ const angle =
343
+ (Math.round((Math.atan2(cy - y(d), cx - x(d)) / Math.PI) * 2) + 4) % 4;
344
+
345
+ context.save();
346
+ context.translate(
347
+ labelsInCentroids ? cx : x(d),
348
+ labelsInCentroids ? cy : y(d)
349
+ );
350
+ context.textAlign =
351
+ angle === 0
352
+ ? "left"
353
+ : angle === 1
354
+ ? "center"
355
+ : angle === 2
356
+ ? "right"
357
+ : "center";
358
+ context.textBaseline =
359
+ angle === 0
360
+ ? "middle"
361
+ : angle === 1
362
+ ? "top"
363
+ : angle === 2
364
+ ? "middle"
365
+ : "bottom";
366
+ context.font = font;
367
+ context.fillStyle = fill;
368
+ context.strokeStyle = stroke;
369
+ context.lineWidth = strokeWidth;
370
+ context.strokeText(label(d), 0, 0);
371
+ context.fillText(label(d), 0, 0);
372
+ context.restore();
373
+ });
374
+
375
+ let selected = null;
376
+ if (hover) {
377
+ if (debug) console.log("setting onmousemove", target);
378
+
379
+ target.on("mousemove", (event) => {
380
+ const [mx, my] = d3__namespace.pointer(event);
381
+ const i = delaunay.find(mx, my);
382
+ if (i === selected) return;
383
+ selected = i;
384
+
385
+ 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
+
423
+ if (onHover && typeof onHover === "function") onHover(i);
424
+ });
425
+ }
426
+ } // renderCanvas
427
+
428
+ if (renderer.toLocaleLowerCase() === "canvas") {
429
+ renderCanvas();
430
+ } else {
431
+ renderSVG();
432
+ }
433
+
434
+ return Object.assign(target.node(), { delaunay, voronoi, cells });
435
+ }
436
+
437
+ // https://observablehq.com/@d3/occlusion
438
+ // ISC License
439
+
440
+ // Copyright 2020-2023 Philippe Rivière
441
+ // Permission to use, copy, modify, and/or distribute this software for any
442
+ // purpose with or without fee is hereby granted, provided that the above
443
+ // copyright notice and this permission notice appear in all copies.
444
+
445
+ // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
446
+ // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
447
+ // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
448
+ // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
449
+ // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
450
+ // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
451
+ // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
452
+ function occlusion(svg, against = "text") {
453
+ const nodes = d3__namespace
454
+ .sort(svg.selectAll(against), (node) => +node.getAttribute("data-priority"))
455
+ .reverse()
456
+ .map((node) => {
457
+ const { x, y, width, height } = node.getBoundingClientRect();
458
+ return { node, x, y, width, height };
459
+ });
460
+
461
+ const visible = [];
462
+ for (const d of nodes) {
463
+ const occluded = visible.some((e) => intersectRect(d, e));
464
+ d3__namespace.select(d.node).classed("occluded", occluded);
465
+ if (!occluded) visible.push(d);
466
+ }
467
+ return visible;
468
+ }
469
+
470
+ // This intersection function works for rect and text, but not so much for circles.
471
+ function intersectRect(a, b) {
472
+ return !(
473
+ a.x + a.width < b.x ||
474
+ b.x + b.width < a.x ||
475
+ a.y + a.height < b.y ||
476
+ b.y + b.height < a.y
477
+ );
478
+ }
479
+
480
+ exports.occlusion = occlusion;
481
+ exports.smartLabels = smartLabels;
482
+
483
+ }));
@@ -0,0 +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?e(exports,require("d3")):"function"==typeof define&&define.amd?define(["exports","d3"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).smartLabels=t.smartLabels||{},t.d3)}(this,(function(t,e){"use strict";function o(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 n=o(e);function l(t,e="text"){const o=n.sort(t.selectAll(e),(t=>+t.getAttribute("data-priority"))).reverse().map((t=>{const{x:e,y:o,width:n,height:l}=t.getBoundingClientRect();return{node:t,x:e,y:o,width:n,height:l}})),l=[];for(const t of o){const e=l.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}));n.select(t.node).classed("occluded",e),e||l.push(t)}return l}t.occlusion=l,t.smartLabels=function(t,{x:e=t=>t[0],y:o=t=>t[1],label:r=(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:L="opacity: 0.2",showPoints:S=!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!==o(t)&&null!==o(t)));let $=n.extent(t,e),F=n.extent(t,o);c=c||$[1]-$[0],d=d||F[1]-F[0],T&&console.log("✅ smartLabels renderer",O),"canvas"===O.toLocaleLowerCase()?(h=n.select(h)||n.create("canvas").attr("width",c).attr("height",d),A=!1):h=h||n.create("svg").attr("viewBox",[0,0,c,d]);const B=n.Delaunay.from(t,e,o),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,n){if(!n){const l=B.find(e(t),o(t));if(-1===l)return console.log("couldn't find cell",l,t,e(t),o(t)),null;n=I[l][1]}return n}return I=I.map((([t,e])=>[t,D(t,e)])).map((([t,e])=>({d:t,cell:e,show:-n.polygonArea(e)>i}))),"canvas"===O.toLocaleLowerCase()?function(){const t=h.node().getContext("2d");(k||y)&&I.forEach((({d:l,cell:r,show:a})=>{if(!a)return;const[s,i]=n.polygonCentroid(r);t.beginPath(),t.moveTo(s,i),t.lineTo(e(l),o(l)),t.strokeStyle=w,t.stroke()})),x&&(t.beginPath(),E.render(t),t.strokeStyle=v,t.stroke()),S&&(t.beginPath(),B.renderPoints(t,2),t.fillStyle=P,t.fill(),t.strokeStyle=M,t.stroke()),I.forEach((({d:l,cell:i,show:c})=>{if(!c)return;const[d,h]=n.polygonCentroid(i),u=(Math.round(Math.atan2(h-o(l),d-e(l))/Math.PI*2)+4)%4;t.save(),t.translate(y?d:e(l),y?h:o(l)),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(r(l),0,0),t.fillText(r(l),0,0),t.restore()}));let l=null;u&&(T&&console.log("setting onmousemove",h),h.on("mousemove",(i=>{const[h,u]=n.pointer(i),m=B.find(h,u);m!==l&&(l=m,t.clearRect(0,0,c,d),I.forEach((({d:i,cell:c,show:d},h)=>{if(!d&&h!==l)return;const[u,p]=n.polygonCentroid(c),m=(Math.round(Math.atan2(p-o(i),u-e(i))/Math.PI*2)+4)%4;t.save(),t.translate(y?u:e(i),y?p:o(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===l?g:f,t.fillStyle=a,t.strokeStyle=s,t.lineWidth=b,t.strokeText(r(i),0,0),t.fillText(r(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) { ${L} }\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:l})=>l?`M${n.polygonCentroid(l)}L${e(t)},${o(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()),S&&(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:l}){if(!l)return;const[r,a]=n.polygonCentroid(l),s=(Math.round(Math.atan2(a-o(t),r-e(t))/Math.PI*2)+4)%4;n.select(this).call(0===s?O.right:3===s?O.top:1===s?O.bottom:O.left)})).attr("transform",(({d:t,cell:l})=>{if(y){const[t,e]=n.polygonCentroid(l);return`translate(${t}, ${e})`}return`translate(${e(t)}, ${o(t)})`})).attr("display",(({show:t})=>t?null:"none")).text((({d:t},e,o)=>r(t,e,o)));let F=null;if(u&&i.on("mousemove",(e=>{const o=B.find(...n.pointer(e));o!==F&&(F=o,$.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")),S&&T.attr("fill",((t,e)=>C)).classed("selected",((t,e)=>e===F)),p&&"function"==typeof p&&p(o))})),A){const t=new MutationObserver((function(){document.contains(h.node())&&(h.call(l,"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})}}));
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "smart-labels",
3
+ "version": "0.0.1",
4
+ "description": "A basic library for adding labels to a visualization when there is enough space",
5
+ "type": "module",
6
+ "main": "dist/smartLabels.min.js",
7
+ "module": "dist/smartLabels.es.js",
8
+ "browser": "dist/smartLabels.min.js",
9
+ "unpkg": "dist/smartLabels.min.js",
10
+ "jsdelivr": "dist/smartLabels.min.js",
11
+ "scripts": {
12
+ "build": "rollup -c",
13
+ "dev": "rollup -c -w",
14
+ "pretest": "npm run build"
15
+ },
16
+ "devDependencies": {
17
+ "@eslint/js": "^9.14.0",
18
+ "@rollup/plugin-json": "^6.1.0",
19
+ "eslint": "^9.14.0",
20
+ "eslint-config-prettier": "^9.1.0",
21
+ "eslint-plugin-html": "^8.1.2",
22
+ "globals": "^15.12.0",
23
+ "prettier": "^3.3.3",
24
+ "rollup": "^4.20.0"
25
+ },
26
+ "dependencies": {
27
+ "@rollup/plugin-node-resolve": "^15.2.3",
28
+ "@rollup/plugin-terser": "^0.4.4",
29
+ "d3": "^7.9.0",
30
+ "rollup-plugin-ascii": "^0.0.3",
31
+ "rollup-plugin-commonjs": "^10.1.0"
32
+ },
33
+ "author": {
34
+ "name": "John Alexis Guerra Gómez",
35
+ "url": "https://johnguerra.co"
36
+ },
37
+ "license": "MIT",
38
+ "bugs": {
39
+ "url": "https://github.com/john-guerra/smart-labels/issues"
40
+ },
41
+ "homepage": "https://github.com/john-guerra/smart-labels"
42
+ }
@@ -0,0 +1,66 @@
1
+ import { readFileSync } from "fs";
2
+ import node from "@rollup/plugin-node-resolve";
3
+ import commonjs from "rollup-plugin-commonjs";
4
+ import terser from "@rollup/plugin-terser";
5
+ // import babel from "rollup-plugin-babel";
6
+ import json from "@rollup/plugin-json";
7
+ import meta from "./package.json" assert { type: "json" };
8
+
9
+ // Extract copyrights from the LICENSE.
10
+ const copyrights = readFileSync("./LICENSE", "utf-8")
11
+ .split(/\n/g)
12
+ .filter((line) => /^copyright\s+/i.test(line))
13
+ .map((line) => line.replace(/^copyright\s+/i, ""));
14
+
15
+ const filename = "smartLabels";
16
+
17
+ // Observable Plot style
18
+ const config = {
19
+ input: "src/index.js",
20
+ external: ["d3"],
21
+ output: {
22
+ indent: false,
23
+ banner: `// ${meta.name} v${meta.version} Copyright ${copyrights.join(", ")}`,
24
+ extend: true,
25
+ globals: {
26
+ "d3": "d3",
27
+ },
28
+ name: "smartLabels",
29
+ },
30
+ plugins: [commonjs(), json(), node()],
31
+ };
32
+
33
+ export default [
34
+ {
35
+ ...config,
36
+ output: {
37
+ ...config.output,
38
+ format: "umd",
39
+ file: `dist/${filename}.js`,
40
+ },
41
+ },
42
+ {
43
+ ...config,
44
+ output: {
45
+ ...config.output,
46
+ format: "es",
47
+ file: `dist/${filename}.es.js`,
48
+ },
49
+ },
50
+ {
51
+ ...config,
52
+ output: {
53
+ ...config.output,
54
+ format: "umd",
55
+ file: `dist/${filename}.min.js`,
56
+ },
57
+ plugins: [
58
+ ...config.plugins,
59
+ terser({
60
+ output: {
61
+ preamble: config.output.banner,
62
+ },
63
+ }),
64
+ ],
65
+ },
66
+ ];