├── .npmignore ├── index.js ├── test └── tip-test.js ├── package.json ├── LICENSE ├── src ├── d3-tip.css └── d3-tip.js ├── index.html ├── .gitignore └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | build/*.zip 2 | test/ 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export {default as tip} from "./src/d3-tip"; 2 | -------------------------------------------------------------------------------- /test/tip-test.js: -------------------------------------------------------------------------------- 1 | var tape = require("tape"); 2 | 3 | tape("foo() returns the answer to the ultimate question of life, the universe, and everything.", function(test) { 4 | test.end(); 5 | }); 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3-v6-tip", 3 | "version": "1.0.9", 4 | "description": "d3-v6-tip Module", 5 | "keywords": [ 6 | "d3", 7 | "d3-module" 8 | ], 9 | "license": "BSD-3-Clause", 10 | "main": "build/d3-v6-tip.js", 11 | "jsnext:main": "index", 12 | "homepage": "https://github.com/bumbeishvili/d3.tip-for-d3.v6", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/bumbeishvili/d3.tip-for-d3.v6.git" 16 | }, 17 | "scripts": { 18 | "pretest": "rm -rf build && mkdir build && rollup -g d3-selection:d3 -f umd -n d3 -o build/d3-v6-tip.js -- index.js", 19 | "test": "tape 'test/**/*-test.js'", 20 | "prepublish": "npm run test && uglifyjs build/d3-v6-tip.js -c -m -o build/d3-v6-tip.min.js", 21 | "postpublish": "zip -j build/d3-v6-tip.zip -- LICENSE README.md build/d3-v6-tip.js build/d3-v6-tip.min.js" 22 | }, 23 | "devDependencies": { 24 | "rollup": "0.27", 25 | "tape": "4", 26 | "uglify-es": "3" 27 | }, 28 | "dependencies": { 29 | "d3-selection": "2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 David Bumbeishvili 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/d3-tip.css: -------------------------------------------------------------------------------- 1 | .d3-tip { 2 | font-family: Arial, Helvetica, sans-serif; 3 | line-height: 1.4; 4 | padding: 20px; 5 | pointer-events: none !important; 6 | color: #203d5d; 7 | box-shadow: 0 4px 20px 4px rgba(0, 20, 60, .1), 0 4px 80px -8px rgba(0, 20, 60, .2); 8 | background-color: #fff; 9 | border-radius: 4px; 10 | } 11 | 12 | /* Creates a small triangle extender for the tooltip */ 13 | .d3-tip:after { 14 | box-sizing: border-box; 15 | display: inline; 16 | font-size: 10px; 17 | width: 100%; 18 | line-height: 1; 19 | color: #fff; 20 | position: absolute; 21 | pointer-events: none; 22 | } 23 | 24 | /* Northward tooltips */ 25 | .d3-tip.n:after { 26 | content: "▼"; 27 | margin: -1px 0 0 0; 28 | top: 100%; 29 | left: 0; 30 | text-align: center; 31 | } 32 | 33 | /* Eastward tooltips */ 34 | .d3-tip.e:after { 35 | content: "◀"; 36 | margin: -4px 0 0 0; 37 | top: 50%; 38 | left: -8px; 39 | } 40 | 41 | /* Southward tooltips */ 42 | .d3-tip.s:after { 43 | content: "▲"; 44 | margin: 0 0 1px 0; 45 | top: -8px; 46 | left: 0; 47 | text-align: center; 48 | } 49 | 50 | /* Westward tooltips */ 51 | .d3-tip.w:after { 52 | content: "▶"; 53 | margin: -4px 0 0 -1px; 54 | top: 50%; 55 | left: 100%; 56 | } 57 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
Hover over bars to see tooltip!
7 | 8 | 9 | 10 | 11 | 12 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://github.com/vshymanskyy/StandWithUkraine/blob/main/docs/README.md) 2 | 3 | 4 | # d3-v6-tip 5 | A famous d3-tip lib adapted to the latest - d3.v6 version. 6 | 7 | ![](https://user-images.githubusercontent.com/6873202/92922793-6e621b00-f447-11ea-933d-50bac9a25fbb.gif) 8 | 9 | 10 | ## Foreword 11 | 12 | d3.v6 introduced several changes and some of them concerns d3-tip. 13 | 14 | Those are: 15 | 16 | * Global d3.event has been removed 17 | * Every event handler, from now on, will receive event as a first argument 18 | 19 | d3-tip version which lies under this repository, is adapted to this change. 20 | 21 | It also fixes one annoying bug, when several DOM tip instances were being created , which eventually would lead unexpected and undesirable results. 22 | 23 | See [original documentation](https://github.com/caged/d3-tip/blob/master/docs/index.md), but please note changes in `tip.html` API. 24 | Short story is that, you will get same arguments in `tip.html()` as `tip.show()` receives, in the same order. 25 | 26 | ## Installing 27 | If you are using npm 28 | 29 | ```npm i d3-v6-tip``` 30 | 31 | And then use it like this in your application 32 | ```javascript 33 | import { tip as d3tip } from "d3-v6-tip"; 34 | 35 | const tip = d3tip() 36 | ``` 37 | 38 | Otherwise, you can load as a standalone library or as part of D3. ES modules, AMD, CommonJS, and vanilla environments are supported. In vanilla, a d3 global is exported: 39 | 40 | If you want to load it as part of d3 41 | ```html 42 | 43 | 44 | 45 | 46 | 51 | ``` 52 | 53 | If you want to load it as standalone 54 | ```html 55 | 56 | 57 | 58 | 59 | 93 | ``` 94 | 95 | 96 | See [minimal jsfiddle example here](https://jsfiddle.net/aftjeb0g/2/) 97 | 98 | 99 | 100 | 101 | ## Style 102 | for default styling, include this [css file](https://github.com/bumbeishvili/d3-v6-tip/blob/master/src/d3-tip.css) into your app 103 | ```css 104 | .d3-tip { 105 | font-family: Arial, Helvetica, sans-serif; 106 | line-height: 1.4; 107 | padding: 20px; 108 | pointer-events: none !important; 109 | color: #203d5d; 110 | box-shadow: 0 4px 20px 4px rgba(0, 20, 60, .1), 0 4px 80px -8px rgba(0, 20, 60, .2); 111 | background-color: #fff; 112 | border-radius: 4px; 113 | } 114 | 115 | /* Creates a small triangle extender for the tooltip */ 116 | .d3-tip:after { 117 | box-sizing: border-box; 118 | display: inline; 119 | font-size: 10px; 120 | width: 100%; 121 | line-height: 1; 122 | color: #fff; 123 | position: absolute; 124 | pointer-events: none; 125 | } 126 | 127 | /* Northward tooltips */ 128 | .d3-tip.n:after { 129 | content: "▼"; 130 | margin: -1px 0 0 0; 131 | top: 100%; 132 | left: 0; 133 | text-align: center; 134 | } 135 | 136 | /* Eastward tooltips */ 137 | .d3-tip.e:after { 138 | content: "◀"; 139 | margin: -4px 0 0 0; 140 | top: 50%; 141 | left: -8px; 142 | } 143 | 144 | /* Southward tooltips */ 145 | .d3-tip.s:after { 146 | content: "▲"; 147 | margin: 0 0 1px 0; 148 | top: -8px; 149 | left: 0; 150 | text-align: center; 151 | } 152 | 153 | /* Westward tooltips */ 154 | .d3-tip.w:after { 155 | content: "▶"; 156 | margin: -4px 0 0 -1px; 157 | top: 50%; 158 | left: 100%; 159 | } 160 | ``` 161 | 162 | 163 | ## History 164 | 165 | * [Caged](https://github.com/caged/d3-tip) created d3-tip for d3.v3 166 | * [cgav](https://github.com/cgav/d3-tip) converted it to d3-v4 and ES6 code 167 | * [VACLAB](https://github.com/VACLab/d3-tip) removed ES6 specific code for wider browser support 168 | * [bumbeishvili](https://github.com/bumbeishvili/d3-tip-v6) adapted for newer d3.v6 version and [published to NPM](https://www.npmjs.com/package/d3-v6-tip) 169 | 170 | ## Author 171 | [David B (twitter)](https://twitter.com/dbumbeishvili) 172 | [David B (linkedin)](https://www.linkedin.com/in/bumbeishvili/) 173 | 174 | I am available for freelance data visualization work. Please [contact me](https://davidb.dev/about) in case you'd like me to help you with my experience and expertise 175 | 176 | You can also [book data viz related consultation session](https://www.fiverr.com/share/4XxG21) with me 177 | -------------------------------------------------------------------------------- /src/d3-tip.js: -------------------------------------------------------------------------------- 1 | // d3.tip 2 | // Copyright (c) 2013 Justin Palmer 3 | // ES6 / D3 v4 Adaption Copyright (c) 2016 Constantin Gavrilete 4 | // Removal of ES6 for D3 v4 Adaption Copyright (c) 2016 David Gotz 5 | // Adaptation for d3.v6 and publish on NPM by bumbeishvili (github.com/bumbeishvili) - npm: d3-v6-tip 6 | // 7 | 8 | // Tooltips for d3.js SVG visualizations 9 | // import * as d3 from d3; 10 | 11 | import {select,selection} from "d3-selection"; 12 | 13 | 14 | export default function () { 15 | const functor = function functor(v) { 16 | return typeof v === "function" ? v : function () { 17 | return v; 18 | }; 19 | }; 20 | 21 | var direction = d3_tip_direction, 22 | offset = d3_tip_offset, 23 | html = d3_tip_html, 24 | node = initNode(), 25 | svg = null, 26 | point = null, 27 | target = null, 28 | rootElement = functor(document.body) 29 | 30 | function tip(vis) { 31 | svg = getSVGNode(vis) 32 | point = svg.createSVGPoint() 33 | rootElement().appendChild(node) 34 | } 35 | 36 | // Public - show the tooltip on the screen 37 | // 38 | // Returns a tip 39 | tip.show = function () { 40 | var args = Array.prototype.slice.call(arguments) 41 | if (args[0] instanceof Event) { 42 | target = args[0].target; 43 | } 44 | if (args[args.length - 1] instanceof SVGElement) target = args.pop() 45 | 46 | var content = html.apply(this, args), 47 | poffset = offset.apply(this, args), 48 | dir = direction.apply(this, args), 49 | nodel = getNodeEl(), 50 | i = directions.length, 51 | coords, 52 | scrollTop = document.documentElement.scrollTop || rootElement().scrollTop, 53 | scrollLeft = document.documentElement.scrollLeft || rootElement().scrollLeft 54 | 55 | nodel.html(content) 56 | .style('position', 'absolute') 57 | .style('opacity', 1) 58 | .style('pointer-events', 'all') 59 | 60 | while (i--) nodel.classed(directions[i], false) 61 | coords = direction_callbacks[dir].apply(this) 62 | nodel.classed(dir, true) 63 | .style('top', (coords.top + poffset[0]) + scrollTop + 'px') 64 | .style('left', (coords.left + poffset[1]) + scrollLeft + 'px') 65 | 66 | return tip 67 | } 68 | 69 | // Public - hide the tooltip 70 | // 71 | // Returns a tip 72 | tip.hide = function () { 73 | var nodel = getNodeEl() 74 | nodel 75 | .style('opacity', 0) 76 | .style('pointer-events', 'none') 77 | return tip 78 | } 79 | 80 | // Public: Proxy attr calls to the d3 tip container. Sets or gets attribute value. 81 | // 82 | // n - name of the attribute 83 | // v - value of the attribute 84 | // 85 | // Returns tip or attribute value 86 | tip.attr = function (n, v) { 87 | if (arguments.length < 2 && typeof n === 'string') { 88 | return getNodeEl().attr(n) 89 | } else { 90 | var args = Array.prototype.slice.call(arguments) 91 | if (n == 'class') { 92 | // Prevent internal class override internally 93 | getNodeEl() 94 | .classed(v, true); 95 | } else { 96 | selection.prototype.attr.apply(getNodeEl(), args) 97 | } 98 | 99 | } 100 | 101 | return tip 102 | } 103 | 104 | // Public: Proxy style calls to the d3 tip container. Sets or gets a style value. 105 | // 106 | // n - name of the property 107 | // v - value of the property 108 | // 109 | // Returns tip or style property value 110 | tip.style = function (n, v) { 111 | if (arguments.length < 2 && typeof n === 'string') { 112 | return getNodeEl().style(n) 113 | } else { 114 | var args = Array.prototype.slice.call(arguments); 115 | if (args.length === 1) { 116 | var styles = args[0]; 117 | Object.keys(styles).forEach(function (key) { 118 | return selection.prototype.style.apply(getNodeEl(), [key, styles[key]]); 119 | }); 120 | } 121 | } 122 | 123 | return tip 124 | } 125 | 126 | // Public: Set or get the direction of the tooltip 127 | // 128 | // v - One of n(north), s(south), e(east), or w(west), nw(northwest), 129 | // sw(southwest), ne(northeast) or se(southeast) 130 | // 131 | // Returns tip or direction 132 | tip.direction = function (v) { 133 | if (!arguments.length) return direction 134 | direction = v == null ? v : functor(v) 135 | 136 | return tip 137 | } 138 | 139 | // Public: Sets or gets the offset of the tip 140 | // 141 | // v - Array of [x, y] offset 142 | // 143 | // Returns offset or 144 | tip.offset = function (v) { 145 | if (!arguments.length) return offset 146 | offset = v == null ? v : functor(v) 147 | 148 | return tip; 149 | } 150 | 151 | // Public: sets or gets the html value of the tooltip 152 | // 153 | // v - String value of the tip 154 | // 155 | // Returns html value or tip 156 | tip.html = function (v) { 157 | if (!arguments.length) return html 158 | html = v == null ? v : functor(v) 159 | 160 | return tip 161 | } 162 | 163 | // Public: sets or gets the root element anchor of the tooltip 164 | // 165 | // v - the html rootElement 166 | // 167 | // Returns root node of tip 168 | tip.rootElement = function (v) { 169 | if (!arguments.length) return rootElement 170 | rootElement = v == null ? v : functor(v) 171 | 172 | return tip 173 | } 174 | 175 | // Public: destroys the tooltip and removes it from the DOM 176 | // 177 | // Returns a tip 178 | tip.destroy = function () { 179 | if (node) { 180 | getNodeEl().remove(); 181 | node = null; 182 | } 183 | return tip; 184 | } 185 | 186 | function d3_tip_direction() { return 'n' } 187 | function d3_tip_offset() { return [0, 0] } 188 | function d3_tip_html() { return ' ' } 189 | 190 | var direction_callbacks = { 191 | n: direction_n, 192 | s: direction_s, 193 | e: direction_e, 194 | w: direction_w, 195 | nw: direction_nw, 196 | ne: direction_ne, 197 | sw: direction_sw, 198 | se: direction_se 199 | }; 200 | 201 | var directions = Object.keys(direction_callbacks); 202 | 203 | function direction_n() { 204 | var bbox = getScreenBBox() 205 | return { 206 | top: bbox.n.y - node.offsetHeight, 207 | left: bbox.n.x - node.offsetWidth / 2 208 | } 209 | } 210 | 211 | function direction_s() { 212 | var bbox = getScreenBBox() 213 | return { 214 | top: bbox.s.y, 215 | left: bbox.s.x - node.offsetWidth / 2 216 | } 217 | } 218 | 219 | function direction_e() { 220 | var bbox = getScreenBBox() 221 | return { 222 | top: bbox.e.y - node.offsetHeight / 2, 223 | left: bbox.e.x 224 | } 225 | } 226 | 227 | function direction_w() { 228 | var bbox = getScreenBBox() 229 | return { 230 | top: bbox.w.y - node.offsetHeight / 2, 231 | left: bbox.w.x - node.offsetWidth 232 | } 233 | } 234 | 235 | function direction_nw() { 236 | var bbox = getScreenBBox() 237 | return { 238 | top: bbox.nw.y - node.offsetHeight, 239 | left: bbox.nw.x - node.offsetWidth 240 | } 241 | } 242 | 243 | function direction_ne() { 244 | var bbox = getScreenBBox() 245 | return { 246 | top: bbox.ne.y - node.offsetHeight, 247 | left: bbox.ne.x 248 | } 249 | } 250 | 251 | function direction_sw() { 252 | var bbox = getScreenBBox() 253 | return { 254 | top: bbox.sw.y, 255 | left: bbox.sw.x - node.offsetWidth 256 | } 257 | } 258 | 259 | function direction_se() { 260 | var bbox = getScreenBBox() 261 | return { 262 | top: bbox.se.y, 263 | left: bbox.e.x 264 | } 265 | } 266 | 267 | function initNode() { 268 | let tipNode2 = 2; 269 | let tipNode = select('.d3-tip-lib-node'); 270 | if (!tipNode.node()) { 271 | tipNode = select(document.createElement('div')) 272 | } 273 | tipNode 274 | .classed('d3-tip-lib-node', true) 275 | .style('position', 'absolute') 276 | .style('top', '0') 277 | .style('opacity', '0') 278 | .style('pointer-events', 'none') 279 | .style('box-sizing', 'border-box') 280 | 281 | return tipNode.node() 282 | } 283 | 284 | function getSVGNode(el) { 285 | el = el.node() 286 | if (el.tagName.toLowerCase() === 'svg') 287 | return el 288 | 289 | return el.ownerSVGElement 290 | } 291 | 292 | function getNodeEl() { 293 | if (node === null) { 294 | node = initNode(); 295 | // re-add node to DOM 296 | document.body.appendChild(node); 297 | }; 298 | return select(node); 299 | } 300 | 301 | // Private - gets the screen coordinates of a shape 302 | // 303 | // Given a shape on the screen, will return an SVGPoint for the directions 304 | // n(north), s(south), e(east), w(west), ne(northeast), se(southeast), nw(northwest), 305 | // sw(southwest). 306 | // 307 | // +-+-+ 308 | // | | 309 | // + + 310 | // | | 311 | // +-+-+ 312 | // 313 | // Returns an Object {n, s, e, w, nw, sw, ne, se} 314 | function getScreenBBox() { 315 | var targetel = target || d3.event.target; 316 | 317 | while ('undefined' === typeof targetel.getScreenCTM && 'undefined' === targetel.parentNode) { 318 | targetel = targetel.parentNode; 319 | } 320 | 321 | var bbox = {}, 322 | matrix = targetel.getScreenCTM(), 323 | tbbox = targetel.getBBox(), 324 | width = tbbox.width, 325 | height = tbbox.height, 326 | x = tbbox.x, 327 | y = tbbox.y 328 | 329 | point.x = x 330 | point.y = y 331 | bbox.nw = point.matrixTransform(matrix) 332 | point.x += width 333 | bbox.ne = point.matrixTransform(matrix) 334 | point.y += height 335 | bbox.se = point.matrixTransform(matrix) 336 | point.x -= width 337 | bbox.sw = point.matrixTransform(matrix) 338 | point.y -= height / 2 339 | bbox.w = point.matrixTransform(matrix) 340 | point.x += width 341 | bbox.e = point.matrixTransform(matrix) 342 | point.x -= width / 2 343 | point.y -= height / 2 344 | bbox.n = point.matrixTransform(matrix) 345 | point.y += height 346 | bbox.s = point.matrixTransform(matrix) 347 | 348 | return bbox 349 | } 350 | 351 | return tip 352 | }; 353 | --------------------------------------------------------------------------------