├── 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 | [](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 |
--------------------------------------------------------------------------------