├── MIT-LICENSE.txt ├── test.html ├── README.md └── assets └── js └── jquery.spidergraph.js /MIT-LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Jason M. Striegel (@jmstriegel) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | 23 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Spidergraph Demo 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 25 |

Spidergraph Demo

26 |

View source for example usage

27 |
28 | 29 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jQuery Spidergraph Plugin - Dynamic, interactive spidergraphs in HTML5 2 | ======================================================================== 3 | 4 | jquery.spidergraph is a simple module that creates nice looking, interactive spidergraphs in HTML5, using the canvas element. 5 | 6 | What are spidergraphs good for? 7 | --------------------------------- 8 | 9 | - illustrating scaled quantitative data for several subjective attributes 10 | - overlaying multiple data measurements for attribute comparison 11 | - visualizing the intersection of several data measurements 12 | 13 | 14 | How do I use it? 15 | ---------------- 16 | 17 | First make a div to contain your graph: 18 | 19 | 25 |
26 | 27 | 28 | Apply the spidergraph and tell it what the data fields are. The spidergraph and labels will be drawn to fill the specified div. 29 | 30 | $('#spidergraphcontainer').spidergraph({ 31 | 'fields': ['a','b','c','d','e'], 32 | 'gridcolor': 'rgba(20,20,20,1)' 33 | }); 34 | 35 | 36 | Add static layers to your data set. Opacity can be used to see through multiple layers. 37 | 38 | $('#spidergraphcontainer').spidergraph('addlayer', { 39 | 'strokecolor': 'rgba(230,204,0,0.8)', 40 | 'fillcolor': 'rgba(230,204,0,0.6)', 41 | 'data': [5, 4, 9, 8, 1] 42 | }); 43 | $('#spidergraphcontainer').spidergraph('addlayer', { 44 | 'strokecolor': 'rgba(230,204,230,0.8)', 45 | 'fillcolor': 'rgba(230,204,230,0.6)', 46 | 'data': [5, 4, 9, 8, 1] 47 | }); 48 | 49 | Add a dynamic layer that allows mouse and iPad touch input to control the graph. 50 | 51 | 52 | $('#spidergraphcontainer').spidergraph('setactivedata', { 53 | 'strokecolor': 'rgba(0,204,230,0.8)', 54 | 'fillcolor': 'rgba(0,204,230,0.6)', 55 | 'data': [5, 5, 5, 5, 5] 56 | }); 57 | 58 | 59 | Respond to changes in user input on the active layer. 60 | 61 | $('#spidergraphcontainer').bind('spiderdatachange', function( ev, data ) { 62 | alert( 'first field value is ' + data[0] ); 63 | }); 64 | 65 | 66 | Reset all of the data layers in the spidergraph. 67 | 68 | $('#spidergraphcontainer').spidergraph('resetdata'); 69 | 70 | 71 | If you wish to use a linear style layer (no curves), simply add attribute 'linear' with value true. 72 | 73 | $('#spidergraphcontainer').spidergraph('addlayer', { 74 | 'strokecolor': 'rgba(230,204,230,0.8)', 75 | 'fillcolor': 'rgba(230,204,230,0.6)', 76 | 'data': [5, 4, 9, 8, 1], 77 | 'linear': true 78 | }); 79 | 80 | 81 | 82 | Can I see an example? 83 | ----------------------- 84 | 85 | Yes you can. Right over here: 86 | http://jmstriegel.github.com/jquery.spidergraph/ 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /assets/js/jquery.spidergraph.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 3 | var methods = { 4 | 5 | init: function( options ) { 6 | 7 | var settings = { 8 | 'fields': [ 'Field 1', 'Field 2', 'Field 3' ], 9 | 'gridcolor': 'rgba(0,0,0,0.4)', 10 | 'dotcolor': 'rgba(0,0,0,.6)', 11 | 'strokewidth': 4, 12 | 'handlewidth': 4, 13 | 'increments': 10, 14 | 'minrad': 10 15 | } 16 | 17 | return this.each( function() { 18 | 19 | if ( this.tagName.toUpperCase() != 'DIV' ) { 20 | return false; 21 | } 22 | if ( options ) { 23 | $.extend( settings, options ); 24 | } 25 | 26 | var canvasfg; 27 | var canvasbg; 28 | var canvasfixed; 29 | var outercontainer; 30 | var container; 31 | var contextfg; 32 | var contextbg; 33 | var contextfixed; 34 | var cx; 35 | var cy; 36 | var radius; 37 | var minrad = settings['minrad']; 38 | var activedata = {}; 39 | var fixeddata = []; 40 | var increments = settings['increments']; 41 | var stroke = settings['strokewidth']; 42 | var handle = settings['handlewidth']; 43 | var mousepressed = false; 44 | var activedrag = 0; 45 | var mouseinmove = false; 46 | 47 | outercontainer = $(this).get(0); 48 | container = $('
').get(0); 49 | $(outercontainer).children().remove(); 50 | $(outercontainer).append($(container)); 51 | 52 | cx = Math.floor( ($(container).width() / 2) - (handle / 2) ); 53 | cy = Math.floor( ($(container).height() / 2) - (handle / 2) ); 54 | radius = $(container).width(); 55 | if ( $(container).height() < $(container).width() ) { 56 | radius = $(container).height(); 57 | } 58 | radius = Math.floor( radius / 2 - 50 ); 59 | 60 | canvasfg = $('').get(0); 61 | $(canvasfg).css( { position: 'absolute', top: '0px', left: '0px' } ); 62 | canvasbg = $('').get(0); 63 | $(canvasbg).css( { position: 'absolute', top: '0px', left: '0px' } ); 64 | canvasfixed = $('').get(0); 65 | $(canvasfixed).css( { position: 'absolute', top: '0px', left: '0px' } ); 66 | canvasbg.width = $(container).width(); 67 | canvasfg.width = $(container).width(); 68 | canvasfixed.width = $(container).width(); 69 | canvasbg.height = $(container).height(); 70 | canvasfg.height = $(container).height(); 71 | canvasfixed.height = $(container).height(); 72 | $(container).append( $( canvasbg ) ); 73 | $(container).append( $( canvasfixed ) ); 74 | $(container).append( $( canvasfg ) ); 75 | 76 | contextfg = canvasfg.getContext("2d"); 77 | contextbg = canvasbg.getContext("2d"); 78 | contextfixed = canvasbg.getContext("2d"); 79 | 80 | 81 | $(this).data('spidergraph', { 82 | 'settings': settings, 83 | 'outercontainer': outercontainer, 84 | 'activedata': activedata, 85 | 'fixeddata': fixeddata, 86 | 'canvasbg': canvasbg, 87 | 'canvasfg': canvasfg, 88 | 'canvasfixed': canvasfixed, 89 | 'cx': cx, 90 | 'cy': cy, 91 | 'radius': radius 92 | }); 93 | 94 | var data = $(this).data('spidergraph'); 95 | var $sg = $(this); 96 | 97 | 98 | var setactivedrag = function(e) { 99 | var mx = Math.floor((e.pageX-$(container).offset().left)); 100 | var my = Math.floor((e.pageY-$(container).offset().top)); 101 | 102 | var dx = mx-cx; 103 | var dy = my-cy; 104 | 105 | var dr = Math.sqrt( dx * dx + dy * dy ); 106 | var rad = Math.atan( dx / (0-dy) ); 107 | var deg = rad * 180 / Math.PI; 108 | 109 | 110 | if ( dy > 0 ) { 111 | //bottom 112 | deg += 180; 113 | } 114 | if ( dy <= 0 && dx < 0 ) { 115 | //top left; 116 | deg += 360; 117 | } 118 | 119 | var closest = 180; 120 | var closestidx = 0; 121 | degstep = 360 / (settings.fields.length); 122 | for ( var x=0; x 180 ) { 129 | diff = 360 - diff; 130 | } 131 | 132 | if ( diff < closest ) { 133 | closestidx = x; 134 | closest = diff; 135 | } 136 | } 137 | 138 | activedrag = closestidx; 139 | 140 | } 141 | 142 | var handlemove = function(e) { 143 | var mx = Math.floor((e.pageX-$(container).offset().left)); 144 | var my = Math.floor((e.pageY-$(container).offset().top)); 145 | 146 | var dx = mx-cx; 147 | var dy = my-cy; 148 | 149 | var dr = Math.sqrt( dx * dx + dy * dy ); 150 | var rad = Math.atan( dx / (0-dy) ); 151 | var deg = rad * 180 / Math.PI; 152 | 153 | 154 | if ( dy > 0 ) { 155 | //bottom 156 | deg += 180; 157 | } 158 | if ( dy <= 0 && dx < 0 ) { 159 | //top left; 160 | deg += 360; 161 | } 162 | 163 | var closest = 180; 164 | var closestidx = 0; 165 | degstep = 360 / (settings.fields.length); 166 | for ( var x=0; x 180 ) { 173 | diff = 360 - diff; 174 | } 175 | 176 | if ( diff < closest ) { 177 | closestidx = x; 178 | closest = diff; 179 | } 180 | } 181 | 182 | if ( closestidx != activedrag ) { 183 | // return; 184 | } 185 | 186 | var newval = (dr / radius) * increments; 187 | newval = Math.floor((newval) ); 188 | if ( newval > increments ) { 189 | newval = increments; 190 | } else if ( newval < 0 ) { 191 | newval = 0; 192 | }; 193 | 194 | if ( data.activedata.data ) { 195 | data.activedata.data[activedrag] = newval; 196 | $sg.trigger('spiderdatachange', [ data.activedata.data ]); 197 | drawActiveData( $sg, data.canvasfg ); 198 | } 199 | 200 | } 201 | 202 | 203 | var supportsTouch = 'createTouch' in document; 204 | var clickEvent = supportsTouch ? 'click' : 'click'; 205 | var pressEvent = supportsTouch ? 'touchstart' : 'mousedown'; 206 | var moveEvent = supportsTouch ? 'touchmove' : 'mousemove'; 207 | var releaseEvent = supportsTouch ? 'touchend' : 'mouseup'; 208 | 209 | var correctCoordinates = function( ev ) { 210 | if ( supportsTouch ) { 211 | ev.pageX = ev.originalEvent.targetTouches[0].pageX; 212 | ev.pageY = ev.originalEvent.targetTouches[0].pageY; 213 | } 214 | }; 215 | 216 | $(container).bind( pressEvent, function(e) { 217 | mousepressed = true; 218 | correctCoordinates( e ); 219 | 220 | if ( e.correctedX != 0 && e.correctedY != 0 ) { 221 | mouseinmove = true; 222 | setactivedrag( e ); 223 | handlemove( e ); 224 | } 225 | /*correctCoordinates( e ); 226 | setactivedrag( e ); 227 | handlemove( e ); 228 | */ 229 | }); 230 | $(container).bind( releaseEvent, function(e) { 231 | mousepressed = false; 232 | }); 233 | $(window).bind( releaseEvent, function(e) { 234 | mousepressed = false; 235 | }); 236 | $(container).bind( moveEvent, function(e) { 237 | correctCoordinates( e ); 238 | //alert('touches: ' + e.pageX + ' ' + e.pageY ); 239 | if ( mousepressed && mouseinmove ) { 240 | handlemove( e ); 241 | } else if ( mousepressed ) { 242 | alert('loc: ' + ev.pageX + ' ' + ev.pageY ); 243 | mouseinmove = true; 244 | setactivedrag( e ); 245 | handlemove( e ); 246 | } 247 | }); 248 | 249 | 250 | 251 | drawGrid( $(this), canvasbg ); 252 | 253 | 254 | 255 | 256 | }); //end return this.each 257 | 258 | 259 | }, 260 | 261 | addlayer: function( layerdata ) { 262 | 263 | var data = $(this).data('spidergraph'); 264 | data.fixeddata[data.fixeddata.length] = layerdata; 265 | 266 | drawFixedData( $(this), data.canvasfixed ); 267 | 268 | }, 269 | 270 | redraw: function( ) { 271 | 272 | var data = $(this).data('spidergraph'); 273 | 274 | drawGrid( $(this), data.canvasbg ); 275 | drawFixedData( $(this), data.canvasfixed ); 276 | drawActiveData( $(this), data.canvasfg ); 277 | 278 | }, 279 | 280 | resetdata: function() { 281 | var data = $(this).data('spidergraph'); 282 | data.fixeddata = []; 283 | data.activedata = {}; 284 | drawGrid( $(this), data.canvasbg ); 285 | drawFixedData( $(this), data.canvasfixed ); 286 | drawActiveData( $(this), data.canvasfg ); 287 | }, 288 | 289 | setactivedata: function( activedata ) { 290 | var data = $(this).data('spidergraph'); 291 | data.activedata = activedata; 292 | drawActiveData( $(this), data.canvasfg ); 293 | } 294 | 295 | 296 | }; //end spidergraph methods 297 | 298 | jQuery.fn.spidergraph = function( method ) { 299 | if ( methods[method] ) { 300 | return methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 )); 301 | } else if ( typeof method === 'object' || ! method ) { 302 | return methods.init.apply( this, arguments ); 303 | } else { 304 | $.error( 'Method ' + method + ' does not exist on jQuery.spidergraph' ); 305 | } 306 | }; 307 | 308 | 309 | 310 | 311 | function drawGrid( $sg, canvas ) { 312 | 313 | var context = canvas.getContext("2d"); 314 | var data = $sg.data('spidergraph'); 315 | var degstep = 360 / data.settings.fields.length; 316 | var cx = data.cx; 317 | var cy = data.cy; 318 | var radius = data.radius; 319 | var increments = data.settings.increments; 320 | var minrad = data.settings.minrad; 321 | 322 | context.clearRect( 0, 0, canvas.width, canvas.height ); 323 | for ( var x=0; x'); 347 | $newtext.text(text); 348 | $newtext.addClass('diagramText'); 349 | 350 | $newtext.css( { display:'block', position: 'absolute', left: (adjx + dx) + 'px', top: adjy + dy + 'px' 351 | } ); 352 | $sg.prepend( $newtext ); 353 | 354 | //var adjustx = Math.cos( deg ); 355 | var adjustx = ($newtext.width() / 2) + ( Math.cos( deg + Math.PI/2 ) * $newtext.width() / 2 ); 356 | var adjusty = ($newtext.height() / 2) + ( Math.cos( deg ) * $newtext.height() / 2 ); 357 | 358 | //alert( Math.cos( deg + Math.PI/2 ) ); 359 | 360 | $newtext.css( { display:'block', position: 'absolute', left: (adjx + dx - adjustx) + 'px', top: (adjy + 361 | dy - adjusty ) + 'px' } ); 362 | 363 | } 364 | 365 | drawDot( context, cx, cy, data.settings.handlewidth, data.settings.dotcolor ); 366 | 367 | } 368 | 369 | function drawFixedData( $sg, canvas ) { 370 | var context = canvas.getContext("2d"); 371 | var data = $sg.data('spidergraph'); 372 | 373 | context.clearRect( 0, 0, canvas.width, canvas.height ); 374 | 375 | for ( var i in data.fixeddata ) { 376 | 377 | layerdata = data.fixeddata[i]; 378 | drawDataSet( $sg, canvas, layerdata ); 379 | 380 | } 381 | } 382 | 383 | function drawActiveData( $sg, canvas ) { 384 | var context = canvas.getContext("2d"); 385 | var data = $sg.data('spidergraph'); 386 | context.clearRect( 0, 0, canvas.width, canvas.height ); 387 | 388 | if ( data == null || data.activedata == null || data.activedata.data == null ) { 389 | return; 390 | } 391 | 392 | layerdata = data.activedata; 393 | drawDataSet( $sg, canvas, layerdata ); 394 | } 395 | 396 | function drawDataSet( $sg, canvas, layerdata ) { 397 | var context = canvas.getContext("2d"); 398 | var data = $sg.data('spidergraph'); 399 | var degstep = 360 / data.settings.fields.length; 400 | var cx = data.cx; 401 | var cy = data.cy; 402 | var radius = data.radius; 403 | var increments = data.settings.increments; 404 | var minrad = data.settings.minrad; 405 | 406 | context.strokeStyle = layerdata.strokecolor; 407 | context.fillStyle = layerdata.fillcolor; 408 | context.lineWidth = data.settings.strokewidth; 409 | context.beginPath(); 410 | 411 | 412 | //draw bezier 413 | for ( var x=0; x< data.settings.fields.length ; x++ ) { 414 | var coords = getCoords($sg, layerdata.data, x); 415 | var ncoords = getCoords($sg, layerdata.data, (x+1) % data.settings.fields.length ); 416 | var nncoords = getCoords($sg, layerdata.data, (x+2) % data.settings.fields.length ); 417 | var pcoords = getCoords($sg, layerdata.data, x-1 >= 0 ? x-1 : data.settings.fields.length -1 ); 418 | var curvemage = Math.sqrt( (nncoords.x-coords.x) * (nncoords.x-coords.x) + (nncoords.y-coords.y) * (nncoords.y-coords.y) ); 419 | curvemage = (curvemage*.2) * (0.2*( ((layerdata.data[x] + 2)/increments + 4)) ); 420 | var curvemaga = Math.sqrt( (ncoords.x-pcoords.x) * (ncoords.x-pcoords.x) + (ncoords.y-pcoords.y) * (ncoords.y-pcoords.y) ); 421 | curvemaga = (curvemaga*.2) * (0.2*(((layerdata.data[(x+1) %data.settings.fields.length] + 2)/increments + 4)) ); 422 | 423 | var exitnorm = Math.sqrt( (ncoords.x - pcoords.x) * (ncoords.x - pcoords.x) + (ncoords.y - pcoords.y) * (ncoords.y - pcoords.y) ); 424 | exitnorm = curvemage/exitnorm; 425 | var appnorm = Math.sqrt( (nncoords.x - coords.x) * (nncoords.x - coords.x) + (nncoords.y - coords.y) * (nncoords.y - coords.y) ); 426 | appnorm = curvemaga/appnorm; 427 | 428 | exit = { x: (ncoords.x - pcoords.x)*exitnorm + coords.x, y: (ncoords.y - pcoords.y)*exitnorm + coords.y}; 429 | approach = { x: ncoords.x - (nncoords.x - coords.x)*appnorm, y: ncoords.y - (nncoords.y - coords.y)*appnorm }; 430 | 431 | //alert ( exitnorm ); 432 | //exit = ncoords; 433 | //exit = approach; 434 | 435 | if ( x == 0 ) { 436 | context.moveTo( Math.floor(coords.x+cx), Math.floor(coords.y+cy) ); 437 | } 438 | drawBezierShapePart( context, 439 | coords.x + cx, coords.y + cy, 440 | exit.x + cx, exit.y + cy, 441 | approach.x + cx, approach.y + cy, 442 | ncoords.x + cx, ncoords.y + cy, 443 | layerdata.linear ? true : false 444 | ); 445 | //drawDot( contextfg, exit.x + cx, exit.y + cy, "#000000" ); 446 | //drawDot( contextfg, approach.x + cx, approach.y + cy, "#000000" ); 447 | 448 | 449 | } 450 | 451 | 452 | 453 | context.fill(); 454 | context.stroke(); 455 | 456 | 457 | } 458 | 459 | 460 | function drawCircle( context, x, y, r, color, lw ) { 461 | context.strokeStyle = color; 462 | context.lineWidth = lw; 463 | context.beginPath(); 464 | context.arc( Math.floor(x), Math.floor(y), r, 0, Math.PI*2, true ); 465 | context.closePath(); 466 | context.stroke(); 467 | } 468 | 469 | 470 | function drawDot( context, x, y, width, color ) { 471 | context.fillStyle = color; 472 | context.beginPath(); 473 | context.arc( Math.floor(x), Math.floor(y), width, 0, Math.PI*2, true ); 474 | context.closePath(); 475 | context.fill(); 476 | } 477 | 478 | function getCoords( $sg, dataset, idx ) { 479 | var data = $sg.data('spidergraph'); 480 | degstep = 360 / (dataset.length); 481 | var deg = (idx*degstep) * Math.PI / 180; 482 | var dr = dataset[idx] / data.settings.increments * data.radius + data.settings.minrad; 483 | var dx = Math.sin( deg ) * dr; 484 | var dy = 0 - (Math.cos( deg ) * dr); 485 | 486 | return { x: dx, y: dy, r: dr }; 487 | } 488 | 489 | 490 | function drawTriangle( context, x1, y1, x2, y2, x3, y3, color ) { 491 | context.fillStyle = color; 492 | context.beginPath(); 493 | context.moveTo( Math.floor(x1), Math.floor(y1) ); 494 | context.lineTo( Math.floor(x2), Math.floor(y2) ); 495 | context.lineTo( Math.floor(x3), Math.floor(y3) ); 496 | context.lineTo( Math.floor(x1), Math.floor(y1) ); 497 | context.fill(); 498 | } 499 | function drawLine( context, x1, y1, x2, y2, color, width ) { 500 | context.strokeStyle = color; 501 | context.lineWidth = width; 502 | context.beginPath(); 503 | context.moveTo( Math.floor(x1), Math.floor(y1) ); 504 | context.lineTo( Math.floor(x2), Math.floor(y2) ); 505 | context.stroke(); 506 | } 507 | function drawBezier( context, x1, y1, cx1, cy1, cx2, cy2, x2, y2, color ) { 508 | context.strokeStyle = color; 509 | context.lineWidth = stroke; 510 | context.beginPath(); 511 | context.moveTo( Math.floor(x1), Math.floor(y1) ); 512 | context.bezierCurveTo( Math.floor(cx1), Math.floor(cy1), Math.floor(cx2), Math.floor(cy2), Math.floor(x2), Math.floor(y2) ); 513 | context.stroke(); 514 | } 515 | function drawBezierShapePart( context, x1, y1, cx1, cy1, cx2, cy2, x2, y2, linear ) { 516 | if (!linear){ 517 | context.bezierCurveTo( Math.floor(cx1), Math.floor(cy1), Math.floor(cx2), Math.floor(cy2), Math.floor(x2), Math.floor(y2) ); 518 | } 519 | else { 520 | context.lineTo(Math.floor(x2), Math.floor(y2)); 521 | } 522 | } 523 | 524 | 525 | 526 | })(jQuery); 527 | 528 | 529 | 530 | 531 | --------------------------------------------------------------------------------