├── .gitignore ├── README.md ├── bower.json ├── css └── style.css ├── examples.png ├── js └── lib.js └── test ├── test.html └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | bower_components 3 | */.DS_Store 4 | plot.html -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | D3xter 2 | ====== 3 |  4 | 5 | ## About 6 | 7 | - Simple and powerful syntax to make common charts with minimal code. 8 | - Highly flexible plotting for deep customization. 9 | - Sensible defaults but easy to configure when desired. 10 | - Easily extendable via familiar D3.js syntax. 11 | 12 | ## Install 13 | 14 | `bower install d3xter` 15 | 16 | ## Documentation 17 | 18 | For full documentation complete with examples, visit this page. 19 | 20 | ## Testing and Contribution 21 | 22 | Run unit tests by opening test/test.html in the browser. 23 | 24 | Pull requests welcome! 25 | 26 | ## License 27 | 28 | **The MIT License (MIT)** 29 | 30 | > Copyright (c) 2014 Nathan Epstein 31 | > 32 | > Permission is hereby granted, free of charge, to any person obtaining a copy 33 | > of this software and associated documentation files (the "Software"), to deal 34 | > in the Software without restriction, including without limitation the rights 35 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 36 | > copies of the Software, and to permit persons to whom the Software is 37 | > furnished to do so, subject to the following conditions: 38 | > 39 | > The above copyright notice and this permission notice shall be included in 40 | > all copies or substantial portions of the Software. 41 | > 42 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 43 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 44 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 45 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 46 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 47 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 48 | > THE SOFTWARE. 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "d3xter", 3 | "version": "2.0.2", 4 | "homepage": "https://github.com/NathanEpstein/D3xter", 5 | "authors": [ 6 | "Nathan Epstein (ne2210@columbia.edu)" 7 | ], 8 | "description": "D3 wrapper for simply creating standard charts", 9 | "main": "js/lib.js", 10 | "keywords": [ 11 | "D3", 12 | "Plotting", 13 | "Chart", 14 | "Histogram", 15 | "Scatterplot", 16 | "Graphs", 17 | "Bubbleplot" 18 | ], 19 | "license": "MIT", 20 | "dependencies": { 21 | "d3": "~3.4.13", 22 | "mocha": "~2.0.1", 23 | "assert": "https://github.com/Jxck/assert", 24 | "chai": "~1.9.2" 25 | }, 26 | "devDependencies": { 27 | "mocha": "~2.0.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | svg .tick line, svg .domain { 2 | fill: none; 3 | stroke: black; 4 | } 5 | 6 | svg .title { 7 | font-size: 20; 8 | font-weight: bold; 9 | } 10 | 11 | svg .label { 12 | font-size: 15; 13 | } 14 | 15 | svg text { 16 | font-family: sans-serif; 17 | font-size: 11px; 18 | stroke: none; 19 | } 20 | -------------------------------------------------------------------------------- /examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NathanEpstein/D3xter/4030bdce6ba38641b2a9021975d7a653ae129988/examples.png -------------------------------------------------------------------------------- /js/lib.js: -------------------------------------------------------------------------------- 1 | function D3xter(config) { 2 | var self = this, 3 | config = config || {}; 4 | 5 | var height = config.height || 500, 6 | width = config.width || 700, 7 | margin = { 8 | top: 100, 9 | bottom: 100, 10 | left: 100, 11 | right: 100 12 | }, 13 | innerHeight = height - margin.top - margin.bottom, 14 | innerWidth = width - margin.left - margin.right; 15 | 16 | self.plot = function(input) { 17 | buildPlot(input); 18 | renderPlot(input.datasets); 19 | 20 | return self; 21 | }; 22 | 23 | self.pie = function(input) { 24 | buildPie(input); 25 | renderPie(input); 26 | 27 | return self; 28 | }; 29 | 30 | self.bar = function(input) { 31 | buildBar(input); 32 | renderBar(input); 33 | 34 | return self; 35 | }; 36 | 37 | self.hist = function(dataset) { 38 | var histData = buildHist(dataset); 39 | 40 | return self.bar({ 41 | groups: histData.bins.map(function(bin) { return round2(bin) }), 42 | datasets: [ { values: histData.values } ] 43 | }); 44 | }; 45 | 46 | self.timeline = function(events) { 47 | var formattedEvents = formatEvents(events); 48 | buildTimeline(formattedEvents); 49 | renderTimeline(formattedEvents); 50 | 51 | return self; 52 | }; 53 | 54 | function buildCanvas() { 55 | self.canvas = d3.select(config.selector || 'body') 56 | .append('svg') 57 | .attr('height', height) 58 | .attr('width', width); 59 | }; 60 | 61 | function buildXMap(datasets) { 62 | var values = datasets.map(function(d) { return d.x }).reduce(function(a, b) { return a.concat(b) }); 63 | var ordinalValues = (typeof values[0] == 'string'); 64 | 65 | if (ordinalValues) { 66 | var xDomain = getUniqueValues(values); 67 | self.xMap = d3.scale.ordinal() 68 | .domain(xDomain) 69 | .rangePoints([margin.left, width - margin.right]); 70 | } 71 | else { 72 | var xDomain = getBoundaries(values); 73 | self.xMap = d3.scale.linear() 74 | .domain(xDomain) 75 | .range([margin.left, width - margin.right]); 76 | }; 77 | }; 78 | 79 | function buildYMap(datasets) { 80 | var values = datasets.map( 81 | function(d) { return d.y } 82 | ).reduce(function(a, b) { return a.concat(b) }, []); 83 | 84 | var yDomain = getBoundaries(values); 85 | self.yMap = d3.scale.linear() 86 | .domain(yDomain) 87 | .range([height - margin.bottom, margin.top]); 88 | }; 89 | 90 | function buildZMap(datasets) { 91 | var basePointSize = 3; 92 | var values = datasets.map(function(d) { return d.z }) 93 | .reduce(function(a, b) { return a.concat(b) }, []) 94 | .filter(function(a) { return (typeof a !== 'undefined') }); 95 | 96 | if (values.length == 0) { 97 | self.zMap = function() { return basePointSize }; 98 | } 99 | else { 100 | var zDomain = getBoundaries(values); 101 | self.zMap = function(value) { 102 | if (typeof value === 'undefined') return basePointSize; 103 | sizeBonus = 9 * (value - zDomain[0]) / (zDomain[1] - zDomain[0]); 104 | return basePointSize * (1 + sizeBonus); 105 | }; 106 | }; 107 | }; 108 | 109 | function buildXAxis() { 110 | var xAxis = d3.svg.axis() 111 | .scale(self.xMap); 112 | 113 | self.canvas.append('g') 114 | .attr('transform','translate(0,' + (height - margin.bottom) + ')') 115 | .call(xAxis) 116 | .selectAll('text') 117 | .style("text-anchor", "end") 118 | .attr("dx", "-.8em") 119 | .attr("dy", "-0.5em") 120 | .attr("transform", "rotate(-90)" ); 121 | }; 122 | 123 | function buildYAxis() { 124 | var yAxis = d3.svg.axis() 125 | .tickFormat(d3.format('s')) 126 | .scale(self.yMap) 127 | .orient('left'); 128 | 129 | self.canvas.append('g') 130 | .attr('transform','translate(' + margin.left + ', 0)') 131 | .call(yAxis); 132 | }; 133 | 134 | function buildAxes() { 135 | buildXAxis(); 136 | buildYAxis(); 137 | }; 138 | 139 | function buildLabels() { 140 | var xLabel = self.canvas.append('text') 141 | .attr('x', margin.left + innerWidth / 2) 142 | .attr('y', margin.top + innerHeight + margin.bottom / 2) 143 | .text(config.xLab) 144 | .attr('text-anchor', 'middle') 145 | .attr('class', 'label'); 146 | 147 | var yLabel = self.canvas.append('text') 148 | .attr('x', - (margin.top + innerHeight / 2)) 149 | .attr('y', margin.left / 2) 150 | .attr('transform', 'rotate(-90)') 151 | .text(config.yLab) 152 | .attr('text-anchor', 'middle') 153 | .attr('class', 'label'); 154 | 155 | var title = self.canvas.append('text') 156 | .attr('x', margin.left + innerWidth / 2) 157 | .attr('y', margin.top / 2) 158 | .text(config.title) 159 | .attr('text-anchor', 'middle') 160 | .attr('class', 'title'); 161 | }; 162 | 163 | function buildPlot(input) { 164 | buildCanvas(); 165 | buildXMap(input.datasets); 166 | buildYMap(input.datasets); 167 | buildZMap(input.datasets); 168 | buildAxes(); 169 | buildLabels(); 170 | buildLegend(input.datasets, input.labels); 171 | }; 172 | 173 | function buildLegend(datasets, labels) { 174 | if (typeof labels === 'undefined' || config.legend == false) return; 175 | 176 | var colors = parseColors(datasets); 177 | 178 | var legend = self.canvas.append("g") 179 | .attr("class", "legend"); 180 | 181 | colors.forEach(function(color, index) { 182 | legend.append("rect") 183 | .attr("x", width - 18) 184 | .attr("y", index * 20) 185 | .attr("width", 18) 186 | .attr("height", 18) 187 | .style("fill", color); 188 | 189 | legend.append("text") 190 | .attr("x", width - 24) 191 | .attr("y", index * 20 + 9) 192 | .attr("dy", ".35em") 193 | .style("text-anchor", "end") 194 | .text(labels[index]); 195 | }); 196 | }; 197 | 198 | function buildBar(input) { 199 | var structuredData = [ 200 | { 201 | x: input.groups.map(String), 202 | y: input.datasets 203 | .map(function(dataset) { return dataset.values }) 204 | .reduce(function(a, b) { return a.concat(b) }, []) 205 | .concat([0]) 206 | } 207 | ]; 208 | 209 | buildCanvas(); 210 | buildYMap(structuredData); 211 | buildXMapBar(input); 212 | buildAxes(); 213 | buildLabels(); 214 | buildLegend(input.datasets, input.labels); 215 | }; 216 | 217 | function buildXMapBar(input) { 218 | var datasetIndexes = input.datasets.map(function(dataset, index) { return index }); 219 | 220 | self.xMap = d3.scale.ordinal() 221 | .domain(input.groups) 222 | .rangeRoundBands([margin.left, width - margin.right], .1); 223 | 224 | self.innerXMap = d3.scale.ordinal() 225 | .domain(datasetIndexes) 226 | .rangeRoundBands([0, self.xMap.rangeBand()], .05); 227 | }; 228 | 229 | function buildHist(dataset) { 230 | var domain = getBoundaries(dataset), 231 | binCount = Math.round(Math.sqrt(dataset.length)), 232 | binSize = (domain[1] - domain[0]) / binCount, 233 | bins = [], 234 | values = []; 235 | 236 | for (var i = 0; i < binCount; i++) { 237 | bins.push(domain[0] + i * binSize); 238 | values.push(0); 239 | }; 240 | 241 | dataset.forEach(function(value) { values[bindex(bins, value)] += 1 }); 242 | 243 | return { 244 | binSize: binSize, 245 | bins: bins, 246 | values: values 247 | }; 248 | }; 249 | 250 | function buildArcs(input) { 251 | var radius = Math.min(innerWidth, innerHeight) / 2; 252 | 253 | self.arc = d3.svg.arc() 254 | .outerRadius(radius - 10) 255 | .innerRadius(0); 256 | 257 | var pie = d3.layout.pie() 258 | .sort(null) 259 | .value(function(d) { return d }); 260 | 261 | self.arcGroup = self.canvas.selectAll('.arc') 262 | .data(pie(input.values)) 263 | .enter().append('g') 264 | .attr('class', 'arc'); 265 | }; 266 | 267 | function buildPie(input) { 268 | buildCanvas(); 269 | buildLabels(); 270 | buildArcs(input); 271 | buildLegend(input.values, input.labels); 272 | }; 273 | 274 | function buildYMapTimeline(maxFreq) { 275 | self.yMap = d3.scale.linear() 276 | .domain([0, maxFreq]) 277 | .range([height - margin.bottom, margin.top]); 278 | }; 279 | 280 | function buildXMapTimeline(formattedEvents) { 281 | var sortedDates = Object.keys(formattedEvents).sort(function(a, b) { 282 | return Date.parse(a) - Date.parse(b); 283 | }); 284 | 285 | var minDate = sortedDates[0] 286 | maxDate = sortedDates[sortedDates.length - 1]; 287 | 288 | self.xMap = d3.time.scale() 289 | .domain([ 290 | Date.parse(minDate), 291 | Date.parse(maxDate) 292 | ]) 293 | .nice(d3.time[timeScale(minDate, maxDate)]) 294 | .range([margin.left, width - margin.right]); 295 | }; 296 | 297 | function buildTimeline(formattedEvents) { 298 | var maxFreq = maxDateFreq(formattedEvents) 299 | 300 | formatTimelineCanvas(maxFreq); 301 | buildCanvas(); 302 | buildLabels(); 303 | buildYMapTimeline(maxFreq); 304 | buildXMapTimeline(formattedEvents); 305 | buildXAxis(); 306 | }; 307 | 308 | function renderPlot(datasets) { 309 | var colors = parseColors(datasets); 310 | 311 | datasets.forEach(function(dataset, index) { 312 | if (dataset.hasOwnProperty('labels')) { 313 | plotText(dataset, colors[index]); 314 | } 315 | else if (dataset.line) { 316 | plotLine(dataset, colors[index]); 317 | } 318 | else { 319 | plotPoints(dataset, colors[index]); 320 | }; 321 | }); 322 | }; 323 | 324 | function renderBar(input) { 325 | var colors = parseColors(input.datasets); 326 | 327 | input.datasets.forEach(function(dataset, dataIndex) { 328 | dataset.values.forEach(function(value, labelIndex) { 329 | self.canvas.append('rect') 330 | .attr("width", self.innerXMap.rangeBand()) 331 | .attr("x", self.xMap(input.groups[labelIndex]) + self.innerXMap(dataIndex)) 332 | .attr("y", self.yMap(Math.max(value, 0))) 333 | .attr("height", Math.abs(self.yMap(value) - self.yMap(0))) 334 | .style("fill", colors[dataIndex]); 335 | }); 336 | }); 337 | }; 338 | 339 | function renderPie(input) { 340 | var colors = parseColors(input.values); 341 | var total = input.values.reduce(function(a, b) { 342 | return a + b; 343 | }); 344 | 345 | self.arcGroup.append('path') 346 | .attr('d', self.arc) 347 | .style('fill', function(d, i) { return colors[i] }); 348 | 349 | self.arcGroup.append('text') 350 | .attr('transform', function(d) { return 'translate(' + self.arc.centroid(d) + ')' }) 351 | .attr('dy', '.35em') 352 | .style('text-anchor', 'middle') 353 | .text(function(d, i) { 354 | return Math.round(100 * input.values[i] / total) + '%'; 355 | }); 356 | 357 | self.arcGroup.attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')'); 358 | }; 359 | 360 | function renderTimeline(formattedEvents) { 361 | Object.keys(formattedEvents).forEach(function(date) { 362 | formattedEvents[date].forEach(function(label, index) { 363 | self.canvas.append('text') 364 | .attr('class', 'timeline-text') 365 | .attr('x', self.xMap(Date.parse(date))) 366 | .attr('y', self.yMap(index + 1)) 367 | .text(label) 368 | .attr('text-anchor', 'middle') 369 | .attr('stroke', 'black'); 370 | }); 371 | }); 372 | }; 373 | 374 | function plotPoints(dataset, color) { 375 | for (var i = 0; i < dataset.x.length; i++) { 376 | self.canvas.append('circle') 377 | .attr('class', 'plot-circle') 378 | .attr('cx', self.xMap(dataset.x[i])) 379 | .attr('cy', self.yMap(dataset.y[i])) 380 | .attr('r', function() { 381 | if (dataset.hasOwnProperty('z')) return self.zMap(dataset.z[i]); 382 | return self.zMap(); 383 | }) 384 | .attr('opacity', 0.5) 385 | .attr('fill', color); 386 | }; 387 | }; 388 | 389 | function plotLine(dataset, color) { 390 | for (var i = 1; i < dataset.x.length; i++) { 391 | self.canvas.append('line') 392 | .attr('class', 'plot-line') 393 | .attr('stroke-width', 1) 394 | .attr('stroke', color) 395 | .attr('x1', self.xMap(dataset.x[i - 1])) 396 | .attr('x2', self.xMap(dataset.x[i])) 397 | .attr('y1', self.yMap(dataset.y[i - 1])) 398 | .attr('y2', self.yMap(dataset.y[i])); 399 | }; 400 | }; 401 | 402 | function plotText(dataset, color) { 403 | for (var i = 0; i < dataset.x.length; i++) { 404 | self.canvas.append('text') 405 | .attr('class', 'plot-text') 406 | .attr('x', self.xMap(dataset.x[i])) 407 | .attr('y', self.yMap(dataset.y[i])) 408 | .text(dataset.labels[i]) 409 | .attr('text-anchor', 'middle') 410 | .attr('fill', color) 411 | }; 412 | }; 413 | 414 | function getBoundaries(data) { 415 | var min = Math.min.apply(null, data); 416 | var max = Math.max.apply(null, data); 417 | return [min, max]; 418 | }; 419 | 420 | function getUniqueValues(data) { 421 | var seenValues = {}, uniques = []; 422 | data.forEach(function(val) { 423 | if (seenValues[val] != 1) { 424 | seenValues[val] = 1; 425 | uniques.push(val); 426 | }; 427 | }); 428 | return uniques; 429 | }; 430 | 431 | function parseColors(datasets) { 432 | var defaultColor = d3.scale.category10(); 433 | 434 | return datasets.map(function(dataset, index) { 435 | if (isNaN(dataset)) { 436 | return dataset.color || defaultColor(index); 437 | } 438 | else { 439 | return defaultColor(index); 440 | }; 441 | }); 442 | }; 443 | 444 | function bindex(bins, value) { 445 | var bindex = 0; 446 | while (true) { 447 | if (bindex == bins.length - 1) return bindex; 448 | if (value < bins[bindex + 1]) { 449 | return bindex; 450 | } 451 | else { 452 | bindex++; 453 | }; 454 | }; 455 | }; 456 | 457 | function round2(num) { 458 | return Math.round(num * 100) / 100; 459 | }; 460 | 461 | function maxDateFreq(formattedEvents) { 462 | var maxFreq = Object.keys(formattedEvents).map(function(date) { 463 | return formattedEvents[date].length; 464 | }).reduce(function(a, b) { return Math.max(a, b) }, -Infinity); 465 | 466 | return maxFreq; 467 | }; 468 | 469 | function timeScale(minDate, maxDate) { 470 | var periods = { 471 | second: 1000, 472 | minute: 60000, 473 | hour: 3600000, 474 | day: 86400000, 475 | week: 604800000, 476 | month: 2678400000, 477 | year: 31536000000 478 | }; 479 | 480 | var range = Date.parse(maxDate) - Date.parse(minDate), 481 | scale = 'second'; 482 | Object.keys(periods).forEach(function(period) { 483 | if (range > periods[period]) scale = period; 484 | }); 485 | 486 | return scale; 487 | }; 488 | 489 | function formatEvents(events) { 490 | var formattedEvents = {}; 491 | events.forEach(function(ev) { 492 | formattedEvents[ev.date] = formattedEvents[ev.date] || []; 493 | formattedEvents[ev.date].push(ev.label); 494 | }); 495 | 496 | return formattedEvents; 497 | }; 498 | 499 | function formatTimelineCanvas(maxFreq) { 500 | if (!config.hasOwnProperty('height')) { 501 | height = 200 + maxFreq * 50; 502 | }; 503 | }; 504 | 505 | return self; 506 | }; -------------------------------------------------------------------------------- /test/test.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |