smart-labels 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -0
- package/dist/smartLabels.es.js +2 -2
- package/dist/smartLabels.js +2 -2
- package/dist/smartLabels.min.js +2 -2
- package/package.json +1 -1
- package/src/index.js +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Smart Labels
|
|
2
|
+
|
|
3
|
+
Add nearest on-hover and labels when there is space to your D3 chart. It should be faster than force based versions as it uses d3-delaunay, and it will choose which labels to show based on the amount of space available in the chart.
|
|
4
|
+
|
|
5
|
+
Based on the [voronoi-labels](https://observablehq.com/@d3/voronoi-labels) example by [@mbostock](https://observablehq.com/@mbostock), and combining it with [@fil's](https://observablehq.com/@fil) great [occlusion](https://observablehq.com/@fil/occlusion) example.
|
|
6
|
+
|
|
7
|
+
Receives the data, accessors for the x and y coordinates as well as the label. Expects all the coordinates to come in pixels.
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<script src="https://cdn.jsdelivr.net/npm/smart-labels"></script>
|
|
13
|
+
<script>
|
|
14
|
+
const svg = d3.create("svg")
|
|
15
|
+
// 𓈓 add your own marks (e.g. points), we will add the text labels
|
|
16
|
+
|
|
17
|
+
smartLabels(data, {
|
|
18
|
+
target: svg, // where to draw
|
|
19
|
+
label: (d) => d.name, // where to find the label in the data
|
|
20
|
+
x: (d) => xScale(d.x), // expected in pixels
|
|
21
|
+
y: (d) => yScale(d.y),
|
|
22
|
+
labelsInCentroids : true, // Draw the labels in the centroid of the voronoi
|
|
23
|
+
useOcclusion: true // Make occluded labels transparent
|
|
24
|
+
})
|
|
25
|
+
</script>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Default options
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
{
|
|
32
|
+
x = (d) => d[0], // x coordinate accessor, expected to be in pixels
|
|
33
|
+
y = (d) => d[1], // y coordinate accessor, expected to be in pixels
|
|
34
|
+
r = () => 3, // radius accessor, expected to be in pixels
|
|
35
|
+
label = (d, i) => i, // Accessor for the label
|
|
36
|
+
fill = "#333", // label fill color
|
|
37
|
+
stroke = "white", // label stroke color
|
|
38
|
+
threshold = 2000, // Areas over this size would get labels
|
|
39
|
+
width = null,
|
|
40
|
+
height = null,
|
|
41
|
+
target = null, // Where do you want it to draw
|
|
42
|
+
font = "10px sans-serif",
|
|
43
|
+
hover = true, // Show label of the hovered point
|
|
44
|
+
onHover = (i) => i, // callback when hovered, will pass the index of the selected element
|
|
45
|
+
hoverFont = "bolder 12px sans-serif",
|
|
46
|
+
labelsInCentroids = true,
|
|
47
|
+
|
|
48
|
+
backgroundFill = "#fefefe01", // What to paint the bg rect of the labels. Needed for the onHover
|
|
49
|
+
strokeWidth = 5,
|
|
50
|
+
|
|
51
|
+
showVoronoi = false,
|
|
52
|
+
voronoiStroke = "#ccc",
|
|
53
|
+
|
|
54
|
+
showAnchors = false,
|
|
55
|
+
anchorsStroke = "orange",
|
|
56
|
+
anchorsFill = "none",
|
|
57
|
+
|
|
58
|
+
useOcclusion = true,
|
|
59
|
+
occludedStyle = "opacity: 0.2", // css style rules to be used on occluded labels
|
|
60
|
+
|
|
61
|
+
// For debugging
|
|
62
|
+
showPoints = false,
|
|
63
|
+
pointsFill = "#ccc",
|
|
64
|
+
pointsSelectedFill = "firebrick",
|
|
65
|
+
pointsStroke = "#ccc",
|
|
66
|
+
renderer = "svg",
|
|
67
|
+
debug = false,
|
|
68
|
+
selected = null,
|
|
69
|
+
padding = 3, // label padding in pixels
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
## Known problems
|
|
75
|
+
|
|
76
|
+
* Doesn't guarantee non-overlapping
|
|
77
|
+
* If two marks share the same coordinates it will draw their labels and anchors in the same position. It won't be possible to hover over the repeated ones
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
## More examples
|
|
81
|
+
|
|
82
|
+
For observable notebooks please see [@john-guerra/smart-labels](https://observablehq.com/@john-guerra/smart-labels#smartLabels)
|
|
83
|
+
|
|
84
|
+
For other vanilla JS examples see the [/examples](/examples) folder
|
package/dist/smartLabels.es.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// smart-labels v0.0.
|
|
1
|
+
// smart-labels v0.0.4 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
|
|
@@ -86,7 +86,7 @@ function smartLabels(
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
if (!checkIfTargetMatchesRenderer(target, renderer)) {
|
|
89
|
+
if (target && !checkIfTargetMatchesRenderer(target, renderer)) {
|
|
90
90
|
if (debug) console.log("❌ smartLabels Target doesn't match the renderer", target, renderer);
|
|
91
91
|
throw new Error("Smartlabels Target doesn't match the renderer", target, renderer);
|
|
92
92
|
}
|
package/dist/smartLabels.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// smart-labels v0.0.
|
|
1
|
+
// smart-labels v0.0.4 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) :
|
|
@@ -109,7 +109,7 @@ function smartLabels(
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
-
if (!checkIfTargetMatchesRenderer(target, renderer)) {
|
|
112
|
+
if (target && !checkIfTargetMatchesRenderer(target, renderer)) {
|
|
113
113
|
if (debug) console.log("❌ smartLabels Target doesn't match the renderer", target, renderer);
|
|
114
114
|
throw new Error("Smartlabels Target doesn't match the renderer", target, renderer);
|
|
115
115
|
}
|
package/dist/smartLabels.min.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
// smart-labels v0.0.
|
|
2
|
-
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("d3")):"function"==typeof define&&define.amd?define(["d3"],e):(t="undefined"!=typeof globalThis?globalThis:t||self).smartLabels=e(t.d3)}(this,(function(t){"use strict";function e(t){var e=Object.create(null);return t&&Object.keys(t).forEach((function(o){if("default"!==o){var n=Object.getOwnPropertyDescriptor(t,o);Object.defineProperty(e,o,n.get?n:{enumerable:!0,get:function(){return t[o]}})}})),e.default=t,Object.freeze(e)}var o=e(t);function n(t,e="text"){const n=o.sort(t.selectAll(e),(t=>+t.getAttribute("data-priority"))).reverse().map((t=>{const{x:e,y:o,width:n,height:r}=t.getBoundingClientRect();return{node:t,x:e,y:o,width:n,height:r}})),r=[];for(const t of n){const e=r.some((e=>{return n=e,!((o=t).x+o.width<n.x||n.x+n.width<o.x||o.y+o.height<n.y||n.y+n.height<o.y);var o,n}));o.select(t.node).classed("occluded",e),e||r.push(t)}return r}return function(t,{x:e=t=>t[0],y:r=t=>t[1],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)
|
|
1
|
+
// smart-labels v0.0.4 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),f&&!function(t,e){return t&&t.node&&(t=t.node()),"canvas"===e.toLocaleLowerCase()?t instanceof HTMLCanvasElement:t instanceof SVGElement}(f,T))throw $&&console.log("❌ smartLabels Target doesn't match the renderer",f,T),new Error("Smartlabels Target doesn't match the renderer",f,T);"canvas"===T.toLocaleLowerCase()?(f=f?o.select(f.node?f.node():f):o.create("canvas").attr("width",d).attr("height",h),j=!1):f=(f=f?o.select(f.node?f.node():f):o.create("svg").attr("viewBox",[0,0,d,h]))||o.create("svg").attr("viewBox",[0,0,d,h]);const V=o.Delaunay.from(t,e,r),D=V.voronoi([I[0]-1,B[0]-1,I[1]+1,B[1]+1]);let H=t.map(((t,e)=>[t,D.cellPolygon(e)]));function R(t,o,n){if(!o){const l=V.find(e(t,n),r(t,n));if(-1===l)return console.log("couldn't find cell",l,t,e(t,n),r(t,n)),null;o=H[l][1]}return o}function W(n){$&&console.log("renderCanvasAnchorsVoronoiPoints","showAnchors",k,"labelsInCentroids",b,"showVoronoi",x,"showPoints",P),(k||b)&&H.forEach((({d:t,cell:l,show:a},s)=>{if(!a)return;const[i,c]=o.polygonCentroid(l);n.beginPath(),n.moveTo(i,c),n.lineTo(e(t,s),r(t,s)),n.strokeStyle=A,n.stroke()})),x&&(n.beginPath(),D.render(n),n.strokeStyle=w,n.stroke()),P&&(n.beginPath(),n.save(),n.globalAlpha=.7,n.fillStyle="black",t.forEach(((t,o)=>{n.beginPath(),n.arc(e(t,o),r(t,o),l(t,o),0,2*Math.PI)})),n.restore(),n.fillStyle=S,n.fill(),n.strokeStyle=O,n.stroke())}function q(t,n){H.forEach((({d:c,cell:d,show:h},f)=>{if(!h&&f!==n)return;const[g,p]=o.polygonCentroid(d),m=(Math.round(Math.atan2(p-r(c,f),g-e(c,f))/Math.PI*2)+4)%4;t.save();const x=l(c,f)+F,w=0===m?x:2===m?-x:x,k=1===m?x:3===m?-x:x;t.translate(b?g:e(c,f)+w,b?p:r(c,f)+k),t.textAlign=0===m?"left":1===m?"center":2===m?"right":"center",t.textBaseline=0===m?"middle":1===m?"top":2===m?"middle":"bottom",t.font=f===n?y:u,t.fillStyle=s,t.strokeStyle=i,t.lineWidth=v,t.strokeText(a(c,f),0,0),t.fillText(a(c,f),0,0),t.restore()}))}return H=H.map((([t,e],o)=>[t,R(t,e,o)])).map((([t,e])=>({d:t,cell:e,show:-o.polygonArea(e)>c}))),"canvas"===T.toLocaleLowerCase()?function(){const t=f.node().getContext("2d");W(t),q(t,E),g&&($&&console.log("setting onmousemove",f),f.on("mousemove",(e=>{const[n,r]=o.pointer(e),l=V.find(n,r);l!==E&&(E=l,t.clearRect(0,0,d,h),W(t),p&&"function"==typeof p&&p(l),q(t,E))})))}():function(){let t=null;j&&f.selectAll("style.smartLabels").data([0]).join("style").attr("class","smartLabels").html(`\n svg g.labels > text.occluded:not(.selected) { ${C} }\n `);const c=f.selectAll("rect.smartLabels").data([0]).join("rect").attr("class","smartLabels").attr("width",d).attr("height",h).attr("fill",m).attr("stroke","none"),T={top:t=>t.attr("text-anchor","middle").attr("y",((t,e)=>-(l(t,e)+F))),right:t=>t.attr("text-anchor","start").attr("dy","0.35em").attr("x",((t,e)=>l(t,e)+F)),bottom:t=>t.attr("text-anchor","middle").attr("dy","0.71em").attr("y",((t,e)=>l(t,e)+F)),left:t=>t.attr("text-anchor","end").attr("dy","0.35em").attr("x",((t,e)=>-(l(t,e)+F)))};let $;(k||b)&&(t=f.selectAll("g#anchors").data([H]).join("g").attr("pointer-events","none").attr("id","anchors").attr("stroke",A).attr("fill",L).selectAll("path.anchor").data((t=>t)).join("path").attr("class","anchor").attr("display",(({show:t})=>t?null:"none")).attr("d",(({d:t,cell:n},l)=>n?`M${o.polygonCentroid(n)}L${e(t,l)},${r(t,l)}`:null))),x&&f.selectAll("path#voronoi").data([1]).join("path").attr("id","voronoi").attr("stroke",w).attr("pointer-events","none").attr("d",D.render()),P&&($=f.selectAll("circle#points").data([1]).join("path").attr("id","points").attr("pointer-events","none").attr("stroke",O).attr("fill",S).attr("d",V.renderPoints(null,2)));const E=f.selectAll("g.labels").data([H]).join("g").attr("class","labels").attr("fill",s).attr("stroke",i).attr("stroke-width",v).attr("paint-order","stroke").attr("pointer-events","none").selectAll("text").data((t=>t)).join("text").style("font",u).each((function({d:t,cell:n},l){if(!n)return;const[a,s]=o.polygonCentroid(n),i=(Math.round(Math.atan2(s-r(t,l),a-e(t,l))/Math.PI*2)+4)%4;o.select(this).call(0===i?T.right:3===i?T.top:1===i?T.bottom:T.left)})).attr("transform",(({d:t,cell:n},l)=>{if(b){const[t,e]=o.polygonCentroid(n);return`translate(${t}, ${e})`}return`translate(${e(t,l)}, ${r(t,l)})`})).attr("display",(({show:t})=>t?null:"none")).text((({d:t},e,o)=>a(t,e,o)));let I=null;if(g&&c.on("mousemove",(e=>{const n=V.find(...o.pointer(e));n!==I&&(I=n,E.attr("display",(({show:t},e)=>e===I||t?null:"none")).style("font",((t,e)=>e===I?y:u)).classed("selected",((t,e)=>e===I)).filter(((t,e)=>e===I)).raise(),t&&t.attr("display",(({show:t},e)=>t||e===I?null:"none")),P&&$.attr("fill",((t,e)=>M)).classed("selected",((t,e)=>e===I)),p&&"function"==typeof p&&p(n))})),j){const t=new MutationObserver((function(){document.contains(f.node())&&(f.call(n,"g.labels > text"),t.disconnect())}));t.observe(document,{attributes:!1,childList:!0,characterData:!1,subtree:!0})}}(),Object.assign(f.node(),{delaunay:V,voronoi:D,cells:H})}}));
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -84,7 +84,7 @@ export default function smartLabels(
|
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
if (!checkIfTargetMatchesRenderer(target, renderer)) {
|
|
87
|
+
if (target && !checkIfTargetMatchesRenderer(target, renderer)) {
|
|
88
88
|
if (debug) console.log("❌ smartLabels Target doesn't match the renderer", target, renderer);
|
|
89
89
|
throw new Error("Smartlabels Target doesn't match the renderer", target, renderer);
|
|
90
90
|
}
|