├── README.md ├── jquery.subwayMap-0.5.3.js └── subwayMap.htm /README.md: -------------------------------------------------------------------------------- 1 | # subwayMap 2 | A jquery plugin to render data as a subway map visualization 3 | 4 | ![image](https://user-images.githubusercontent.com/1822081/50102208-c1804d00-0224-11e9-8a8c-c5f5a83939cc.png) 5 | 6 | # Usage 7 | 8 | Read the [step-by-step guide](https://kalyani.com/blog/2010/10/08/subway-map-visualization-jquery-plugin/) on the author blog 9 | -------------------------------------------------------------------------------- /jquery.subwayMap-0.5.3.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Copyright (c) 2010 Nik Kalyani nik@kalyani.com http://www.kalyani.com 4 | 5 | Modified work Copyright (c) 2016 Jon Burrows subwaymap@jonburrows.co.uk https://jonburrows.co.uk 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | */ 26 | 27 | (function ($) { 28 | 29 | var plugin = { 30 | 31 | defaults: { 32 | debug: false, 33 | grid: false 34 | }, 35 | 36 | options: { 37 | }, 38 | 39 | identity: function (type) { 40 | if (type === undefined) type = "name"; 41 | 42 | switch (type.toLowerCase()) { 43 | case "version": return "1.0.0"; break; 44 | default: return "subwayMap Plugin"; break; 45 | } 46 | }, 47 | _debug: function (s) { 48 | if (this.options.debug) 49 | this._log(s); 50 | }, 51 | _log: function () { 52 | if (window.console && window.console.log) 53 | window.console.log('[subwayMap] ' + Array.prototype.join.call(arguments, ' ')); 54 | }, 55 | _supportsCanvas: function () { 56 | var canvas = $(""); 57 | if (canvas[0].getContext) 58 | return true; 59 | else 60 | return false; 61 | }, 62 | _getCanvasLayer: function (el, overlay) { 63 | this.layer++; 64 | var canvas = $(""); 65 | el.append(canvas); 66 | return (canvas[0].getContext("2d")); 67 | }, 68 | _render: function (el) { 69 | 70 | this.layer = -1; 71 | var rows = el.attr("data-rows"); 72 | if (rows === undefined) 73 | rows = 10; 74 | else 75 | rows = parseInt(rows); 76 | 77 | var columns = el.attr("data-columns"); 78 | if (columns === undefined) 79 | columns = 10; 80 | else 81 | columns = parseInt(columns); 82 | 83 | var scale = el.attr("data-cellSize"); 84 | if (scale === undefined) 85 | scale = 100; 86 | else 87 | scale = parseInt(scale); 88 | 89 | var lineWidth = el.attr("data-lineWidth"); 90 | if (lineWidth === undefined) 91 | lineWidth = 10; 92 | else 93 | lineWidth = parseInt(lineWidth); 94 | 95 | var textClass = el.attr("data-textClass"); 96 | if (textClass === undefined) textClass = ""; 97 | 98 | var grid = el.attr("data-grid"); 99 | if ((grid === undefined) || (grid.toLowerCase() == "false")) 100 | grid = false; 101 | else 102 | grid = true; 103 | 104 | var legendId = el.attr("data-legendId"); 105 | if (legendId === undefined) legendId = ""; 106 | 107 | var gridNumbers = el.attr("data-gridNumbers"); 108 | if ((gridNumbers === undefined) || (gridNumbers.toLowerCase() == "false")) 109 | gridNumbers = false; 110 | else 111 | gridNumbers = true; 112 | 113 | var reverseMarkers = el.attr("data-reverseMarkers"); 114 | if ((reverseMarkers === undefined) || (reverseMarkers.toLowerCase() == "false")) 115 | reverseMarkers = false; 116 | else 117 | reverseMarkers = true; 118 | 119 | 120 | this.options.pixelWidth = columns * scale; 121 | this.options.pixelHeight = rows * scale; 122 | 123 | //el.css("width", this.options.pixelWidth); 124 | //el.css("height", this.options.pixelHeight); 125 | var self = this; 126 | var lineLabels = []; 127 | var supportsCanvas = $("")[0].getContext; 128 | if (supportsCanvas) { 129 | 130 | if (grid) this._drawGrid(el, scale, gridNumbers); 131 | $(el).children("ul").each(function (index) { 132 | var ul = $(this); 133 | 134 | var color = $(ul).attr("data-color"); 135 | if (color === undefined) color = "#000000"; 136 | 137 | var outline = $(ul).attr("data-outline"); 138 | if (outline != undefined && ((outline === true) || (outline.toLowerCase() == "true"))) 139 | outline = true; 140 | else 141 | outline = false; 142 | 143 | var dotted = $(ul).attr("data-dotted"); 144 | if (dotted != undefined && ((dotted === true) || (dotted.toLowerCase() == "true"))) 145 | dotted = true; 146 | else 147 | dotted = false; 148 | 149 | var lineTextClass = $(ul).attr("data-textClass"); 150 | if (lineTextClass === undefined) lineTextClass = ""; 151 | 152 | var shiftCoords = $(ul).attr("data-shiftCoords"); 153 | if (shiftCoords === undefined) shiftCoords = ""; 154 | 155 | var shiftX = 0.00; 156 | var shiftY = 0.00; 157 | if (shiftCoords.indexOf(",") > -1) { 158 | shiftX = parseInt(shiftCoords.split(",")[0]) * lineWidth/scale; 159 | shiftY = parseInt(shiftCoords.split(",")[1]) * lineWidth/scale; 160 | } 161 | 162 | var lineLabel = $(ul).attr("data-label"); 163 | if (lineLabel === undefined) 164 | lineLabel = "Line " + index; 165 | 166 | lineLabels[lineLabels.length] = {label: lineLabel, color: color, outline: outline, dotted: dotted }; 167 | 168 | var nodes = []; 169 | $(ul).children("li").each(function () { 170 | 171 | var coords = $(this).attr("data-coords"); 172 | if (coords === undefined) coords = ""; 173 | 174 | var dir = $(this).attr("data-dir"); 175 | if (dir === undefined) dir = ""; 176 | 177 | var labelPos = $(this).attr("data-labelPos"); 178 | if (labelPos === undefined) labelPos = "s"; 179 | 180 | var marker = $(this).attr("data-marker"); 181 | if (marker == undefined) marker = ""; 182 | 183 | var markerInfo = $(this).attr("data-markerInfo"); 184 | if (markerInfo == undefined) markerInfo = ""; 185 | 186 | var anchor = $(this).children("a:first-child"); 187 | var label = $(this).text(); 188 | if (label === undefined) label = ""; 189 | 190 | var link = ""; 191 | var title = ""; 192 | if (anchor != undefined) { 193 | link = $(anchor).attr("href"); 194 | if (link === undefined) link = ""; 195 | title = $(anchor).attr("title"); 196 | if (title === undefined) title = ""; 197 | } 198 | 199 | self._debug("Coords=" + coords + "; Dir=" + dir + "; Link=" + link + "; Label=" + label + "; labelPos=" + labelPos + "; Marker=" + marker); 200 | 201 | var x = ""; 202 | var y = ""; 203 | if (coords.indexOf(",") > -1) { 204 | x = Number(coords.split(",")[0]) + (marker.indexOf("interchange") > -1 ? 0 : shiftX); 205 | y = Number(coords.split(",")[1]) + (marker.indexOf("interchange") > -1 ? 0 : shiftY); 206 | } 207 | nodes[nodes.length] = { x: x, y: y, direction: dir, marker: marker, markerInfo: markerInfo, link: link, title: title, label: label, labelPos: labelPos }; 208 | }); 209 | 210 | if (nodes.length > 0) { 211 | self._drawLine(el, scale, rows, columns, color, (lineTextClass != "" ? lineTextClass : textClass), lineWidth, nodes, reverseMarkers, dotted); 212 | if (outline === true) 213 | self._drawLine(el, scale, rows, columns, '#FFFFFF', false, lineWidth - 2, nodes, reverseMarkers, dotted); 214 | } 215 | 216 | $(ul).remove(); 217 | }); 218 | 219 | if ((lineLabels.length > 0) && (legendId != "")) 220 | { 221 | var legend = $("#" + legendId); 222 | 223 | for(var line=0; line"; 232 | 233 | // We create a second SVG white line to create the outline effect in the legend if required by the "outline" param 234 | if (lineLabels[line].outline === true) 235 | lineSVG += ""; 236 | 237 | legend.append("
" + lineSVG + "" + lineLabels[line].label + "
"); 238 | } 239 | } 240 | 241 | } 242 | }, 243 | _drawLine: function (el, scale, rows, columns, color, textClass, width, nodes, reverseMarkers, dotted) { 244 | 245 | var ctx = this._getCanvasLayer(el, false); 246 | ctx.beginPath(); 247 | ctx.moveTo(nodes[0].x * scale, nodes[0].y * scale); 248 | var markers = []; 249 | var lineNodes = []; 250 | var node; 251 | for(node = 0; node < nodes.length; node++) 252 | { 253 | if (nodes[node].marker.indexOf("@") != 0) 254 | lineNodes[lineNodes.length] = nodes[node]; 255 | } 256 | for (var lineNode = 0; lineNode < lineNodes.length; lineNode++) { 257 | if (lineNode < (lineNodes.length - 1)) { 258 | var nextNode = lineNodes[lineNode + 1]; 259 | var currNode = lineNodes[lineNode]; 260 | 261 | // Correction for edges so lines are not running off campus 262 | var xCorr = 0; 263 | var yCorr = 0; 264 | if (nextNode.x == 0) xCorr = width / 2; 265 | if (nextNode.x == columns) xCorr = -1 * width / 2; 266 | if (nextNode.y == 0) yCorr = width / 2; 267 | if (nextNode.y == rows) yCorr = -1 * width / 2; 268 | 269 | var xVal = 0; 270 | var yVal = 0; 271 | var direction = ""; 272 | 273 | var xDiff = Math.round(Math.abs(currNode.x - nextNode.x)); 274 | var yDiff = Math.round(Math.abs(currNode.y - nextNode.y)); 275 | if ((xDiff == 0) || (yDiff == 0)) { 276 | // Horizontal or Vertical 277 | ctx.lineTo((nextNode.x * scale) + xCorr, (nextNode.y * scale) + yCorr); 278 | } 279 | else if ((xDiff == 1) && (yDiff == 1)) { 280 | // 90 degree turn 281 | if (nextNode.direction != "") 282 | direction = nextNode.direction.toLowerCase(); 283 | switch (direction) { 284 | case "s": xVal = 0; yVal = scale; break; 285 | case "e": xVal = scale; yVal = 0; break; 286 | case "w": xVal = -1 * scale; yVal = 0; break; 287 | default: xVal = 0; yVal = -1 * scale; break; 288 | } 289 | ctx.quadraticCurveTo((currNode.x * scale) + xVal, (currNode.y * scale) + yVal, 290 | (nextNode.x * scale) + xCorr, (nextNode.y * scale) + yCorr); 291 | } 292 | else if (xDiff == yDiff) { 293 | // Symmetric, angular with curves at both ends 294 | if (nextNode.x < currNode.x) { 295 | if (nextNode.y < currNode.y) 296 | direction = "nw"; 297 | else 298 | direction = "sw"; 299 | } 300 | else { 301 | if (nextNode.y < currNode.y) 302 | direction = "ne"; 303 | else 304 | direction = "se"; 305 | } 306 | var dirVal = 1; 307 | switch (direction) { 308 | case "nw": xVal = -1 * (scale / 2); yVal = 1; dirVal = 1; break; 309 | case "sw": xVal = -1 * (scale / 2); yVal = -1; dirVal = 1; break; 310 | case "se": xVal = (scale / 2); yVal = -1; dirVal = -1; break; 311 | case "ne": xVal = (scale / 2); yVal = 1; dirVal = -1; break; 312 | } 313 | this._debug((currNode.x * scale) + xVal + ", " + (currNode.y * scale) + "; " + (nextNode.x + (dirVal * xDiff / 2)) * scale + ", " + 314 | (nextNode.y + (yVal * xDiff / 2)) * scale); 315 | ctx.bezierCurveTo( 316 | (currNode.x * scale) + xVal, (currNode.y * scale), 317 | (currNode.x * scale) + xVal, (currNode.y * scale), 318 | (nextNode.x + (dirVal * xDiff / 2)) * scale, (nextNode.y + (yVal * xDiff / 2)) * scale); 319 | ctx.bezierCurveTo( 320 | (nextNode.x * scale) + (dirVal * scale / 2), (nextNode.y) * scale, 321 | (nextNode.x * scale) + (dirVal * scale / 2), (nextNode.y) * scale, 322 | nextNode.x * scale, nextNode.y * scale); 323 | } 324 | else 325 | ctx.lineTo(nextNode.x * scale, nextNode.y * scale); 326 | } 327 | } 328 | 329 | if (dotted === true) 330 | ctx.setLineDash([5, 5]); 331 | ctx.strokeStyle = color; 332 | ctx.lineWidth = width; 333 | ctx.stroke(); 334 | 335 | ctx = this._getCanvasLayer(el, true); 336 | for (node = 0; node < nodes.length; node++) { 337 | if (textClass != false) 338 | this._drawMarker(el, ctx, scale, color, textClass, width, nodes[node], reverseMarkers); 339 | } 340 | 341 | 342 | }, 343 | _drawMarker: function (el, ctx, scale, color, textClass, width, data, reverseMarkers) { 344 | 345 | if (data.label == "") return; 346 | if (data.marker == "") data.marker = "station"; 347 | 348 | // Scale coordinates for rendering 349 | var x = data.x * scale; 350 | var y = data.y * scale; 351 | 352 | // Keep it simple -- black on white, or white on black 353 | var fgColor = "#000000"; 354 | var bgColor = "#ffffff"; 355 | if (reverseMarkers) 356 | { 357 | fgColor = "#ffffff"; 358 | bgColor = "#000000"; 359 | } 360 | 361 | // Render station and interchange icons 362 | ctx.strokeStyle = fgColor; 363 | ctx.fillStyle = bgColor; 364 | ctx.beginPath(); 365 | switch(data.marker.toLowerCase()) 366 | { 367 | case "interchange": 368 | case "@interchange": 369 | ctx.lineWidth = width; 370 | if (data.markerInfo == "") 371 | ctx.arc(x, y, width * 0.7, 0, Math.PI * 2, true); 372 | else 373 | { 374 | var mDir = data.markerInfo.substr(0,1).toLowerCase(); 375 | var mSize = parseInt(data.markerInfo.substr(1,10)); 376 | if (((mDir == "v") || (mDir == "h")) && (mSize > 1)) 377 | { 378 | if (mDir == "v") 379 | { 380 | ctx.arc(x, y, width * 0.7,290 * Math.PI/180, 250 * Math.PI/180, false); 381 | ctx.arc(x, y-(width*mSize), width * 0.7,110 * Math.PI/180, 70 * Math.PI/180, false); 382 | } 383 | else 384 | { 385 | ctx.arc(x, y, width * 0.7,20 * Math.PI/180, 340 * Math.PI/180, false); 386 | ctx.arc(x+(width*mSize), y, width * 0.7,200 * Math.PI/180, 160 * Math.PI/180, false); 387 | } 388 | } 389 | else 390 | ctx.arc(x, y, width * 0.7, 0, Math.PI * 2, true); 391 | } 392 | break; 393 | case "station": 394 | case "@station": 395 | ctx.lineWidth = width/2; 396 | ctx.arc(x, y, width/2, 0, Math.PI * 2, true); 397 | break; 398 | } 399 | ctx.closePath(); 400 | ctx.stroke(); 401 | ctx.fill(); 402 | 403 | // Render text labels and hyperlinks 404 | var pos = ""; 405 | var offset = width + 4; 406 | var topOffset = 0; 407 | var centerOffset = "-50px"; 408 | switch(data.labelPos.toLowerCase()) 409 | { 410 | case "n": 411 | pos = "text-align: center; margin: 0 0 " + offset + "px " + centerOffset; 412 | topOffset = offset * 2; 413 | break; 414 | case "w": 415 | pos = "text-align: right; margin:0 " + offset + "px 0 -" + (100 + offset) + "px"; 416 | topOffset = offset; 417 | break; 418 | case "e": 419 | pos = "text-align: left; margin:0 0 0 " + offset + "px"; 420 | topOffset = offset; 421 | break; 422 | case "s": 423 | pos = "text-align: center; margin:" + offset + "px 0 0 " + centerOffset; 424 | break; 425 | case "se": 426 | pos = "text-align: left; margin:" + offset + "px 0 0 " + offset + "px"; 427 | break; 428 | case "ne": 429 | pos = "text-align: left; padding-left: " + offset + "px; margin: 0 0 " + offset + "px 0"; 430 | topOffset = offset * 2; 431 | break; 432 | case "sw": 433 | pos = "text-align: right; margin:" + offset + "px 0 0 -" + (100 + offset) + "px"; 434 | topOffset = offset; 435 | break; 436 | case "nw": 437 | pos = "text-align: right; margin: -" + offset + "px 0 0 -" + (100 + offset) + "px"; 438 | topOffset = offset; 439 | break; 440 | } 441 | var style = (textClass != "" ? "class='" + textClass + "' " : "") + "style='" + (textClass == "" ? "font-size:8pt;font-family:Verdana,Arial,Helvetica,Sans Serif;text-decoration:none;" : "") + "width:100px;" + (pos != "" ? pos : "") + ";position:absolute;top:" + (y + el.position().top - (topOffset > 0 ? topOffset : 0)) + "px;left:" + (x + el.position().left) + "px;z-index:3000;'"; 442 | if (data.link != "") 443 | $("" + data.label.replace(/\\n/g,"
") + "").appendTo(el); 444 | else 445 | $("" + data.label.replace(/\\n/g,"
") + "
").appendTo(el); 446 | 447 | }, 448 | _drawGrid: function (el, scale, gridNumbers) { 449 | 450 | var ctx = this._getCanvasLayer(el, false); 451 | ctx.fillStyle = "#000"; 452 | ctx.beginPath(); 453 | var counter = 0; 454 | for (var x = 0.5; x < this.options.pixelWidth; x += scale) { 455 | if (gridNumbers) 456 | { 457 | ctx.moveTo(x, 0); 458 | ctx.fillText(counter++, x-15, 10); 459 | } 460 | ctx.moveTo(x, 0); 461 | ctx.lineTo(x, this.options.pixelHeight); 462 | } 463 | ctx.moveTo(this.options.pixelWidth - 0.5, 0); 464 | ctx.lineTo(this.options.pixelWidth - 0.5, this.options.pixelHeight); 465 | 466 | counter = 0; 467 | for (var y = 0.5; y < this.options.pixelHeight; y += scale) { 468 | if (gridNumbers) 469 | { 470 | ctx.moveTo(0, y); 471 | ctx.fillText(counter++, 0, y-15); 472 | } 473 | ctx.moveTo(0, y); 474 | ctx.lineTo(this.options.pixelWidth, y); 475 | } 476 | ctx.moveTo(0, this.options.pixelHeight - 0.5); 477 | ctx.lineTo(this.options.pixelWidth, this.options.pixelHeight - 0.5); 478 | ctx.strokeStyle = "#eee"; 479 | ctx.lineWidth = 1; 480 | ctx.stroke(); 481 | ctx.fill(); 482 | ctx.closePath(); 483 | 484 | } 485 | }; 486 | 487 | var methods = { 488 | 489 | init: function (options) { 490 | 491 | plugin.options = $.extend({}, plugin.defaults, options); 492 | // iterate and reformat each matched element 493 | return this.each(function (index) { 494 | 495 | plugin.options = $.meta 496 | ? $.extend(plugin.options, $(this).data()) 497 | : plugin.options; 498 | 499 | plugin._debug("BEGIN: " + plugin.identity() + " for element " + index); 500 | 501 | plugin._render($(this)); 502 | 503 | plugin._debug("END: " + plugin.identity() + " for element " + index); 504 | }); 505 | 506 | }, 507 | drawLine: function (data) { 508 | plugin._drawLine(data.element, data.scale, data.rows, data.columns, data.color, data.width, data.nodes); 509 | } 510 | }; 511 | 512 | $.fn.subwayMap = function (method) { 513 | 514 | // Method calling logic 515 | if (methods[method]) { 516 | return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); 517 | } else if (typeof method === 'object' || !method) { 518 | return methods.init.apply(this, arguments); 519 | } else { 520 | $.error('Method ' + method + ' does not exist on jQuery.tooltip'); 521 | } 522 | 523 | }; 524 | 525 | })(jQuery); 526 | -------------------------------------------------------------------------------- /subwayMap.htm: -------------------------------------------------------------------------------- 1 |  2 | 3 | Subway Map 4 | 5 | 6 | 51 | 52 | 53 |
54 | 74 | 75 | 84 | 85 |
86 |
87 | 88 | 89 | 92 | 93 | 94 | --------------------------------------------------------------------------------