├── package.json ├── HTML-SVG-connect.jquery.json ├── LICENSE ├── index.html ├── README.md └── jquery.html-svg-connect.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-svg-connect", 3 | "version": "2.0.0", 4 | "title": "jQuery HTML SVG connect", 5 | "description": "A plugin to draw paths between arbitrary HTML elements (with SVG).", 6 | "keywords": [ 7 | "jquery-plugin", 8 | "ecosystem:jquery", 9 | "svg", 10 | "draw", 11 | "paths" 12 | ], 13 | "author": { 14 | "name": "Owain Lewis", 15 | "email": "contact@a115.co.uk" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/a115/HTML-SVG-connect" 20 | }, 21 | "license": "MIT", 22 | "dependencies": { 23 | "jquery": ">=1.7.1" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/a115/HTML-SVG-connect/issues" 27 | }, 28 | "main": "jquery.html-svg-connect.js" 29 | } 30 | -------------------------------------------------------------------------------- /HTML-SVG-connect.jquery.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HTML-SVG-connect", 3 | "version": "2.0.0", 4 | "title": "jQuery HTML SVG connect", 5 | "description": "A plugin to draw paths between arbitrary HTML elements (with SVG).", 6 | "keywords": [ 7 | "svg", 8 | "draw", 9 | "paths" 10 | ], 11 | "author": { 12 | "name": "Owain Lewis", 13 | "email": "contact@a115.co.uk" 14 | }, 15 | "licenses": [ 16 | { 17 | "type": "MIT", 18 | "url": "http://opensource.org/licenses/MIT" 19 | } 20 | ], 21 | "dependencies": { 22 | "jquery": ">=1.7.1" 23 | }, 24 | "homepage": "https://github.com/a115/HTML-SVG-connect", 25 | "download": "https://github.com/a115/HTML-SVG-connect/archive/master.zip", 26 | "bugs": "https://github.com/a115/HTML-SVG-connect/issues" 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jordan Dimov 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 | 23 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | HTML-SVG-connect: a jQuery plugin 16 | 17 | 18 | 32 | 95 | 96 | 97 | 98 |
99 | 100 |
101 |
102 |
103 |
104 |
105 |
106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTML SVG connect 2 | [![npm](https://img.shields.io/npm/v/html-svg-connect.svg?maxAge=2592000)](https://www.npmjs.com/package/html-svg-connect) 3 | 4 | jQuery plugin for drawing responsive paths between arbitrary HTML elements (with SVG). 5 | 6 | View demo at http://a115.github.io/HTML-SVG-connect/ 7 | 8 | --- 9 | 10 | ## Install 11 | Load jQuery and the plugin: 12 | ```html 13 | 14 | 15 | ``` 16 | or install with npm: 17 | ``` 18 | npm install html-svg-connect 19 | ``` 20 | 21 | ## Usage 22 | 23 | Attach it to your container element on DOM ready, and define your paths as an array. Each path is an object with the start and end elements defined as CSS selector **ID**s: 24 | 25 | **(The elements don't have to be different; you can specify that any *one* element connects with any number of different elements.)** 26 | ```html 27 | 37 | ``` 38 | 39 | This will draw an SVG graphic with two pipes with text written along each between the respective elements, and recalculate them as the window re-sizes. 40 | 41 | Paths can also be added on-demand after loading (e.g., after an AJAX call), by calling **addPaths** with an array of path objects to add (this array has the same options available as when initialising paths). 42 | 43 | ```javascript 44 | var newPaths = [ 45 | { start: "#red", end: "#green", text: "foo" }, 46 | { start: "#aqua", end: "#green", text: "bar" } 47 | ]; 48 | $("#svgContainer").HTMLSVGconnect("addPaths", newPaths); 49 | ``` 50 | 51 | ### Options 52 | 53 | These are defined as properties at the same level as the *paths* property. 54 | 55 | | Name | Type | Description | Default | 56 | | ------------- | ----- | :------------ | ------- | 57 | | stroke | string | Path colour | #000000 | 58 | | strokeWidth | integer | Path thickness (px) | 10 | 59 | | orientation | string | Whether the path begins/ends from the side of the element or from the top/bottom. Options: [horizontal | vertical | auto] | auto | 60 | | offset | integer | Number of pixels added to the path before the first curve. | 0 | 61 | | class | string | Path class (css) name. | empty | 62 | | text | string | Text to be written along the path. | empty | 63 | 64 | **The global options can also be overridden on a per path basis:** 65 | 66 | ```js 67 | { 68 | stroke: "#00FF00", 69 | strokeWidth: 12, 70 | class: "", 71 | paths: [ 72 | { start: "#red", end: "#aqua", stroke: "#FF0000", strokeWidth: 8 }, 73 | { start: "#purple", end: "#green", orientation: "vertical", offset: 20, class: "dashed-blue" } 74 | ] 75 | } 76 | ``` 77 | 78 | ## Author 79 | 80 | Owain Lewis / A115 81 | 82 | Based on work by [alojzije](https://github.com/alojzije): [connectHTMLelements_SVG.png](https://gist.github.com/alojzije/11127839) 83 | 84 | ## Other 85 | 86 | [MIT License](http://www.opensource.org/licenses/mit-license.php) 87 | -------------------------------------------------------------------------------- /jquery.html-svg-connect.js: -------------------------------------------------------------------------------- 1 | /*! 2 | jQuery HTML SVG connect v2.0.0 3 | license: MIT 4 | based on: https://gist.github.com/alojzije/11127839 5 | alojzije/connectHTMLelements_SVG.png 6 | */ 7 | ; (function ($, window, document, undefined) { 8 | //https://github.com/jquery-boilerplate/jquery-boilerplate 9 | "use strict"; 10 | 11 | var pluginName = "HTMLSVGconnect", 12 | defaults = { 13 | stroke: "#000000", 14 | strokeWidth: 12, 15 | orientation: "auto", 16 | class: "", 17 | // Array of objects with properties "start" & "end" that 18 | // define the selectors of the elements to connect: 19 | // i.e., {start: "#purple", end: "#green"}. 20 | // Optional properties: 21 | // "stroke": [color], 22 | // "strokeWidth": [px], 23 | // "orientation": [horizontal|vertical|auto (default)] 24 | // "offset": [px] 25 | paths: [] 26 | }; 27 | 28 | function Plugin(element, options) { 29 | this.element = element; 30 | this.$element = $(this.element); 31 | this.settings = $.extend({}, defaults, options); 32 | this._defaults = defaults; 33 | this._name = pluginName; 34 | this.init(); 35 | } 36 | 37 | $.extend(Plugin.prototype, { 38 | init: function () { 39 | this.$svg = $(document.createElementNS("http://www.w3.org/2000/svg", "svg")); 40 | this.$svg.attr("height", 0).attr("width", 0); 41 | this.$element.append(this.$svg); 42 | // text 43 | this.$text = $(document.createElementNS("http://www.w3.org/2000/svg", "text")); 44 | this.$svg.append(this.$text); 45 | // Draw the paths, and store references to the loaded elements. 46 | this.loadedPaths = $.map(this.settings.paths, $.proxy(this.connectSetup, this)); 47 | $(window).on("resize", this.throttle(this.reset, 200, this)); 48 | }, 49 | 50 | // Recalculate paths. 51 | reset: function () { 52 | this.$svg.attr("height", 0).attr("width", 0); 53 | $.map(this.loadedPaths, $.proxy(this.connectElements, this)); 54 | }, 55 | 56 | connectSetup: function (pathConfig, i) { 57 | if (pathConfig.hasOwnProperty("start") && pathConfig.hasOwnProperty("end")) { 58 | var $start = $(pathConfig.start), $end = $(pathConfig.end); 59 | // Start/end elements exist. 60 | if ($start.length && $end.length) { 61 | var $path = $(document.createElementNS("http://www.w3.org/2000/svg", "path")); 62 | // Custom/default path properties. 63 | var stroke = pathConfig.hasOwnProperty("stroke") ? pathConfig.stroke : this.settings.stroke; 64 | var strokeWidth = pathConfig.hasOwnProperty("strokeWidth") ? pathConfig.strokeWidth : this.settings.strokeWidth; 65 | var path_class = pathConfig.hasOwnProperty("class") ? pathConfig.class : this.settings.class; 66 | var pathId = "path_" + i; 67 | $path.attr("fill", "none") 68 | .attr("stroke", stroke) 69 | .attr("stroke-width", strokeWidth) 70 | .attr("class", path_class) 71 | .attr("id", pathId); 72 | this.$svg.append($path); 73 | 74 | if (pathConfig.text) { 75 | var $tspan = this._createSvgTextPath(pathConfig.text, strokeWidth, pathId); 76 | } 77 | 78 | var pathData = { 79 | "path": $path, 80 | "start": $start, 81 | "end": $end, 82 | "text": pathConfig.text, 83 | "tspan": $tspan, 84 | "orientation": pathConfig.hasOwnProperty("orientation") ? pathConfig.orientation : this.settings.orientation, 85 | "offset": pathConfig.hasOwnProperty("offset") ? parseInt(pathConfig.offset) : 0 86 | }; 87 | this.connectElements(pathData); 88 | // Save for reference. 89 | return pathData; 90 | } 91 | } 92 | return null; // Ignore/invalid. 93 | }, 94 | 95 | _createSvgTextPath: function (text, strokeWidth, pathId) { 96 | // textPath 97 | var textPathElement = document.createElementNS("http://www.w3.org/2000/svg", "textPath"); 98 | textPathElement.setAttributeNS("http://www.w3.org/1999/xlink", "xlink:href", "#" + pathId); 99 | textPathElement.setAttribute("startOffset", "50%"); 100 | var $textPath = $(textPathElement); 101 | this.$text.append($textPath); 102 | var tspan = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); 103 | $textPath.append($(tspan)); 104 | var dy = (strokeWidth / 2) + 2; 105 | tspan.setAttribute("dy", - dy); 106 | // need to reset the dy, another tspan is needed 107 | var otherTspan = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); 108 | $textPath.append($(otherTspan)); 109 | otherTspan.setAttribute("dy", dy); 110 | $(otherTspan).text(" "); 111 | var $tspan = $(tspan); 112 | return $tspan; 113 | }, 114 | 115 | // Whether the path should originate from the top/bottom or the sides; 116 | // based on whichever is greater: the horizontal or vertical gap between the elements 117 | // (this depends on the user positioning the elements sensibly, 118 | // and not overlapping them). 119 | determineOrientation: function ($startElem, $endElem) { 120 | // If first element is lower than the second, swap. 121 | if ($startElem.offset().top > $endElem.offset().top) { 122 | var temp = $startElem; 123 | $startElem = $endElem; 124 | $endElem = temp; 125 | } 126 | var startBottom = $startElem.offset().top + $startElem.outerHeight(); 127 | var endTop = $endElem.offset().top; 128 | var verticalGap = endTop - startBottom; 129 | // If first element is more left than the second, swap. 130 | if ($startElem.offset().left > $endElem.offset().left) { 131 | var temp2 = $startElem; 132 | $startElem = $endElem; 133 | $endElem = temp2; 134 | } 135 | var startRight = $startElem.offset().left + $startElem.outerWidth(); 136 | var endLeft = $endElem.offset().left; 137 | var horizontalGap = endLeft - startRight; 138 | return horizontalGap > verticalGap ? "vertical" : "horizontal"; 139 | }, 140 | 141 | connectElements: function (pathData) { 142 | var $startElem = pathData.start, 143 | $endElem = pathData.end, 144 | orientation = pathData.orientation; 145 | // Orientation not set per path and/or defaulted to global "auto". 146 | if (orientation != "vertical" && orientation != "horizontal") { 147 | orientation = this.determineOrientation($startElem, $endElem); 148 | } 149 | var swap = false; 150 | if (orientation == "vertical") { 151 | // If first element is more left than the second. 152 | swap = $startElem.offset().left > $endElem.offset().left; 153 | } else { // Horizontal 154 | // If first element is lower than the second. 155 | swap = $startElem.offset().top > $endElem.offset().top; 156 | } 157 | if (swap) { 158 | var temp = $startElem; 159 | $startElem = $endElem; 160 | $endElem = temp; 161 | } 162 | // Get (top, left) corner coordinates of the svg container. 163 | var svgTop = this.$element.offset().top; 164 | var svgLeft = this.$element.offset().left; 165 | 166 | // Get (top, left) coordinates for the two elements. 167 | var startCoord = $startElem.offset(); 168 | var endCoord = $endElem.offset(); 169 | 170 | // Centre path above/below or left/right of element. 171 | var centreSX = 0.5, centreSY = 1, 172 | centreEX = 0.5, centreEY = 0; 173 | if (orientation == "vertical") { 174 | centreSX = 1; 175 | centreSY = 0.5; 176 | centreEX = 0; 177 | centreEY = 0.5; 178 | } 179 | // Calculate the path's start/end coordinates. 180 | // We want to align with the elements' mid point. 181 | var startX = startCoord.left + centreSX * $startElem.outerWidth() - svgLeft; 182 | var startY = startCoord.top + centreSY * $startElem.outerHeight() - svgTop; 183 | var endX = endCoord.left + centreEX * $endElem.outerWidth() - svgLeft; 184 | var endY = endCoord.top + centreEY * $endElem.outerHeight() - svgTop; 185 | 186 | this.drawPath(pathData.path, pathData.offset, orientation, startX, startY, endX, endY); 187 | if (pathData.text != undefined && pathData.tspan != undefined) { 188 | this.drawText(pathData.text, pathData.tspan); 189 | } 190 | }, 191 | 192 | drawPath: function ($path, offset, orientation, startX, startY, endX, endY) { 193 | var stroke = parseFloat($path.attr("stroke-width")); 194 | // Check if the svg is big enough to draw the path, if not, set height/width. 195 | if (this.$svg.attr("width") < (Math.max(startX, endX) + stroke)) this.$svg.attr("width", (Math.max(startX, endX) + stroke)); 196 | if (this.$svg.attr("height") < (Math.max(startY, endY) + stroke)) this.$svg.attr("height", (Math.max(startY, endY) + stroke)); 197 | 198 | var deltaX = (Math.max(startX, endX) - Math.min(startX, endX)) * 0.15; 199 | var deltaY = (Math.max(startY, endY) - Math.min(startY, endY)) * 0.15; 200 | // For further calculations whichever is the shortest distance. 201 | var delta = Math.min(deltaY, deltaX); 202 | // Set sweep-flag (counter/clockwise) 203 | var arc1 = 0; var arc2 = 1; 204 | 205 | if (orientation == "vertical") { 206 | var sigY = this.sign(endY - startY); 207 | // If start element is closer to the top edge, 208 | // draw the first arc counter-clockwise, and the second one clockwise. 209 | if (startY < endY) { 210 | arc1 = 1; 211 | arc2 = 0; 212 | } 213 | // Draw the pipe-like path 214 | // 1. move a bit right, 2. arch, 3. move a bit down, 4.arch, 5. move right to the end 215 | $path.attr("d", "M" + startX + " " + startY + 216 | " H" + (startX + offset + delta) + 217 | " A" + delta + " " + delta + " 0 0 " + arc1 + " " + (startX + offset + 2 * delta) + " " + (startY + delta * sigY) + 218 | " V" + (endY - delta * sigY) + 219 | " A" + delta + " " + delta + " 0 0 " + arc2 + " " + (startX + offset + 3 * delta) + " " + endY + 220 | " H" + endX); 221 | } else { 222 | //Horizontal 223 | var sigX = this.sign(endX - startX); 224 | // If start element is closer to the left edge, 225 | // draw the first arc counter-clockwise, and the second one clockwise. 226 | if (startX > endX) { 227 | arc1 = 1; 228 | arc2 = 0; 229 | } 230 | // Draw the pipe-like path 231 | // 1. move a bit down, 2. arch, 3. move a bit to the right, 4.arch, 5. move down to the end 232 | $path.attr("d", "M" + startX + " " + startY + 233 | " V" + (startY + offset + delta) + 234 | " A" + delta + " " + delta + " 0 0 " + arc1 + " " + (startX + delta * sigX) + " " + (startY + offset + 2 * delta) + 235 | " H" + (endX - delta * sigX) + 236 | " A" + delta + " " + delta + " 0 0 " + arc2 + " " + endX + " " + (startY + offset + 3 * delta) + 237 | " V" + endY); 238 | } 239 | }, 240 | 241 | /* 242 | * Draw text for a path, takes the text for a path and the id of the path element and will create a textPath element. 243 | */ 244 | drawText: function (text, $textPath) { 245 | $textPath.text(text); 246 | }, 247 | 248 | /* 249 | * Add array of path objects 250 | * e.g., var paths = [{ start: "#red", end: "#green" }, { start: "#aqua", end: "#green", stroke: "blue" }]; 251 | * Public method within the plugin's prototype: 252 | * $("#svgContainer").HTMLSVGconnect("addPaths", paths); 253 | */ 254 | addPaths: function(paths) { 255 | var loadedPaths = $.map(paths, $.proxy(this.connectSetup, this)); 256 | Array.prototype.push.apply(this.loadedPaths, loadedPaths); 257 | }, 258 | 259 | // Chrome Math.sign() support. 260 | sign: function (x) { 261 | return x > 0 ? 1 : x < 0 ? -1 : x; 262 | }, 263 | 264 | // https://remysharp.com/2010/07/21/throttling-function-calls 265 | throttle: function (fn, threshhold, scope) { 266 | threshhold || (threshhold = 250); 267 | var last, deferTimer; 268 | return function () { 269 | var context = scope || this; 270 | var now = +new Date, 271 | args = arguments; 272 | if (last && now < last + threshhold) { 273 | clearTimeout(deferTimer); 274 | deferTimer = setTimeout(function () { 275 | last = now; 276 | fn.apply(context, args); 277 | }, threshhold); 278 | } else { 279 | last = now; 280 | fn.apply(context, args); 281 | } 282 | }; 283 | }, 284 | }); 285 | 286 | // A really lightweight plugin wrapper around the constructor, 287 | // preventing against multiple instantiations 288 | $.fn[pluginName] = function (options) { 289 | var args = arguments; 290 | if (options === undefined || typeof options === 'object') { 291 | // Creates a new plugin instance, for each selected element, and 292 | // stores a reference within the element's data 293 | return this.each(function() { 294 | if (!$.data(this, 'plugin_' + pluginName)) { 295 | $.data(this, 'plugin_' + pluginName, new Plugin(this, options)); 296 | } 297 | }); 298 | } else if (typeof options === 'string' && options[0] !== '_' && options !== 'init') { 299 | // Call a public plugin method (not starting with an underscore) for each 300 | // selected element. 301 | return this.each(function() { 302 | var instance = $.data(this, 'plugin_' + pluginName); 303 | if (instance instanceof Plugin && typeof instance[options] === 'function') { 304 | instance[options].apply(instance, Array.prototype.slice.call(args, 1)); 305 | } 306 | }); 307 | } 308 | }; 309 | 310 | })(jQuery, window, document); 311 | --------------------------------------------------------------------------------