D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
AlexDaGr8
Full window
Github gist
Interactive and attempting to be more DRY with the code
<!DOCTYPE html> <body> <style> .wrapper { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); grid-gap: 10px; width: 95%; margin: auto; } .chart { box-shadow: 3px 3px 3px 3px #777; width: 100%; } </style> <div class="wrapper"> <div class="chart" id="chart1"></div> <div class="chart" id="chart2"></div> </div> <script src='https://d3js.org/d3.v5.min.js'></script> <script> d3.csv('WMATA2.csv') .then(data => { data.forEach(d => { d.Time = new Date(d.Time); }) var scatter, stacked; var colors = d3.scaleOrdinal() .range(['#DD3565', '#3BB273']) .domain(['Exit', 'Entry']) // chart1 scatter = new createSVG('#chart1', d => d.Time, d => d.Time.getHours() + (d.Time.getMinutes()/60), null); scatter.data = data.filter(d => d.Description === 'Exit' || d.Description === 'Entry'); scatter.setXTest('scaleTime'); scatter.setYTest('scaleLinear') scatter.colors = colors; scatter.scatter('Description'); // chart1.setX(d3.scaleTime() // .range([0, chart1.width]) // .domain(d3.extent(data, d => d.Time))) // chart1.setY(d3.scaleLinear() // .range([chart1.height, 0]) // .domain(d3.extent(data, d => d.Time.getHours() + (d.Time.getMinutes()/60))).nice()) // chart1.elems = chart1.svg.selectAll('circle') // .data(chart1.data) // .join('circle') // .attr('cx', d => chart1.x(d.Time)) // .attr('cy', d => chart1.y(d.Time.getHours() + (d.Time.getMinutes()/60))) // .attr('r', 4) // .attr('fill', d => colors(d.Description)) // .attr('opacity', '.7') // chart1.updateChart = function() { // extent = d3.event.selection // var filtered = chart1.elems // .filter(function(d){ return isBrushed(extent, chart1.x(d.Time), chart1.y(d.Time.getHours() + (d.Time.getMinutes()/60)) ); }) // .attr('opacity', .9) // .attr('stroke', brushedColor) // .attr('stroke-width', 2) // .attr('data-brushed', true) // var notFilterd = chart1.elems // .filter(function(d){ return !isBrushed(extent, chart1.x(d.Time), chart1.y(d.Time.getHours() + (d.Time.getMinutes()/60)) ); }) // .attr('opacity', .7) // .attr('stroke', function(d) { // return d3.select(this).attr('data-clicked') === 'true' ? clickedColor : null; // }) // .attr('data-brushed', false) // stacked.update(filtered.data().length === 0 ? notFilterd.data() : filtered.data()) // // A function that return TRUE or FALSE according if a dot is in the selection or not // function isBrushed(brush_coords, cx, cy) { // var x0 = brush_coords[0][0], // x1 = brush_coords[1][0], // y0 = brush_coords[0][1], // y1 = brush_coords[1][1]; // return x0 <= cx && cx <= x1 && y0 <= cy && cy <= y1; // } // } // chart1.svg.call( // d3.brush() // .extent([[0,0], [chart1.width, chart1.height]]) // .on('start brush', chart1.updateChart) // ) // stacked chart stacked = new createSVG('#chart2',{left: 30, top: 20, right: 20, bottom: 30}) Object.defineProperty(stacked, 'data', { set: function(val) { var tempNest = [...new Set(val.map(d => d.EntryLocation || d.ExitLocation))] .map(d => { return { key: d, values: [ {key: 'Exit', values: val.filter(v => v.ExitLocation === d && v.Description === 'Exit')}, {key: 'Entry', values: val.filter(v => v.EntryLocation === d && v.Description === 'Entry')}, ] } }) tempNest.forEach(d => { var d0 = 0; d.values.sort((a,b) => a.key - b.key).forEach(v => { v.data = d; v.d0 = d0; v.d1 = v.values.length + d0; d0 += v.values.length; }) }) var tempData; if (this._data) { tempData = this._data; tempData.forEach(function (v) { var filter = tempNest.filter(k => k.key === v.key); v.values = tempNest.filter(k => k.key === v.key).length > 0 ? tempNest.filter(k => k.key === v.key)[0].values : [ {key: 'Entry', values: [], data: {}, d0: 0, d1: 0}, {key: 'Exit', values: [], data: {}, d0: 0, d1: 0} ]; }) } else { tempData = tempNest; } this._data = tempData; } }) stacked.data = data; stacked.setX(d3.scaleBand() .range([0,stacked.width]) .domain(stacked.data.map(d => d.key)) .padding(0.05)) stacked.setY(d3.scaleLinear() .range([stacked.height, 0]) .domain([0, d3.max(stacked.data, d => d3.sum(d.values, v => v.values.length))])) stacked.xAxis .call(function(g) { g.selectAll('.tick text').each(function(d,i) { if (i % 2 === 0) { d3.select(this).attr('y', d3.select(this).attr('y') * 2 + 2) } }) }) stacked.elems = stacked.svg.selectAll('g.series') .data(stacked.data) .join('g') .attr('class', 'series') .attr('transform', d => 'translate(' + stacked.x(d.key) + ',0)') stacked.rects = stacked.elems.selectAll('rect.series-rect') .data(d => d.values) .join('rect') .attr('class', 'series-rect') .attr('x', 0) .attr('width', stacked.x.bandwidth()) .attr('y', d => stacked.y(d.d1)) .attr('height', d => stacked.y(d.d0) - stacked.y(d.d1)) .attr('fill', d => colors(d.key)) .attr('stroke', null) .on('mouseover', function(d) { d3.select(this) .style('opacity', 0.7) }) .on('mouseout', function(d) { d3.select(this) .style('opacity', 1) }) .on('click', function(d) { let clicked = d3.select(this).attr('stroke') ? true : false; stacked.rects.attr('stroke', null) if (!clicked) { d3.select(this) .attr('stroke', '#aaa') .attr('stroke-width', 2) chart1.elems.each(function(v) { d3.select(this) .transition() .attr('stroke', function(d) { return d3.select(this).attr('data-brushed') === 'true' ? brushedColor : null; }) .attr('data-clicked', false) if (v[d.key + 'Location'] === d.data.key && v.Description === d.key) { d3.select(this) .transition().duration(250) .attr('r', 8) .attr('stroke', function(d) { return d3.select(this).attr('data-brushed') === 'true' ? brushedColor : clickedColor; }) .attr('stroke-width', 2) .transition().duration(250) .attr('r', 4) .attr('data-clicked', true) } }) } else { chart1.elems.each(function(v) { d3.select(this) .transition() .attr('stroke', function(d) { return d3.select(this).attr('data-brushed') === 'true' ? brushedColor : null; }) .attr('data-clicked', false) }) } }) stacked.update = function(_data) { this.data = _data; this.elems.data(this.data) this.elems.selectAll('rect.series-rect') .data(d => d.values) .transition() .attr('y', d => this.y(d.d1)) .attr('height', d => this.y(d.d0) - this.y(d.d1)) } let dash = new dashboard(); dash.addChart(scatter); dash.addChart(stacked) }) function createSVG(el,xAcc,yAcc,m) { this.chartID = 'chart-' + Math.floor(Math.random() * 1000); this.htmlElem = document.querySelector(el ? el : 'body'); this.container = d3.select(el ? el : 'body'); this.margin = m ? m : {left: 30, top: 20, right: 20, bottom: 20}; this.width = window.getComputedStyle(this.htmlElem).width.split('px')[0] - this.margin.left - this.margin.right; this.height = 450 - this.margin.top - this.margin.bottom; this._svg = this.container.append('svg') .attr('id', this.chartID) .attr('width', this.width + this.margin.left + this.margin.right) .attr('height', this.height + this.margin.top + this.margin.bottom) this.svg = this._svg.append('g') .attr('transform', 'translate(' + [this.margin.left, this.margin.top] + ')'); this.colors = d3.scaleOrdinal(d3.schemeCategory10); this.colorVal = null; this.linkCharts = []; this._data; Object.defineProperty(this, 'data', { get: function() { return this._data; }, set: function(val) { var temp = val; if (xAcc !== undefined && yAcc !== undefined) { var temp = val.map(function(d, i) { return {key: xAcc.call(val, d, i), value: yAcc.call(val, d, i), data: d} }); } this._data = temp; }, configurable: true }) window.addEventListener('resize', () => { this.resize() }) } createSVG.prototype.setX = function(scale) { this.x = scale; this.xType = scale.name; this.xAxis = this.svg.append('g') .attr('transform', 'translate(0,' + this.height + ')') .call(d3.axisBottom(this.x)) } createSVG.prototype.setY = function(scale) { this.y = scale; this.yAxis = this.svg.append('g') .call(d3.axisLeft(this.y)) } createSVG.prototype.setXTest = function(scale) { this.x = d3[scale]() .range([0, this.width]) .domain(d3.extent(this.data, d => d.key)); this.xType = scale.name; this.xAxis = this.svg.append('g') .attr('transform', 'translate(0,' + this.height + ')') .call(d3.axisBottom(this.x)) } createSVG.prototype.setYTest = function(scale) { this.y = d3[scale]() .range([this.height, 0]) .domain(d3.extent(this.data, d => d.value)) .nice(); this.yAxis = this.svg.append('g') .call(d3.axisLeft(this.y)) } createSVG.prototype.link = function(links) { links.forEach(d => { this.linkCharts.push(d); }) console.log('linked', this.linkCharts) } createSVG.prototype.scatter = function(z) { const that = this; this.clickedColor = '#29506d', this.brushedColor = '#222'; this.elems = this.svg.selectAll('circle') .data(this.data) .join('circle') .attr('cx', d => this.x(d.key)) .attr('cy', d => this.y(d.value)) .attr('r', 4) .attr('fill', d => z ? this.colors(d.data[z]) : 'steelblue') .attr('opacity', '.7') this.brushed = function() { extent = d3.event.selection var filtered = that.elems .filter(function(d){ return isBrushed(extent, that.x(d.key), that.y(d.value) ); }) .attr('opacity', .9) .attr('stroke', that.brushedColor) .attr('stroke-width', 2) .attr('data-brushed', true) var notFiltered = that.elems .filter(function(d){ return !isBrushed(extent, that.x(d.key), that.y(d.value) ); }) .attr('opacity', .7) .attr('stroke', function(d) { return d3.select(this).attr('data-clicked') === 'true' ? that.clickedColor : null; }) .attr('data-brushed', false) that.linkCharts.forEach(d => { d.update(filtered.data().length === 0 ? notFiltered.data().map(d => d.data) : filtered.data().map(d => d.data)) }) //stacked.update(filtered.data().length === 0 ? notFilterd.data() : filtered.data()) // A function that return TRUE or FALSE according if a dot is in the selection or not function isBrushed(brush_coords, cx, cy) { var x0 = brush_coords[0][0], x1 = brush_coords[1][0], y0 = brush_coords[0][1], y1 = brush_coords[1][1]; return x0 <= cx && cx <= x1 && y0 <= cy && cy <= y1; } } this.svg.call( d3.brush() .extent([[0,0], [this.width, this.height]]) .on('start brush', this.brushed) ) } createSVG.prototype.resize = function() { this.width = window.getComputedStyle(this.htmlElem).width.split('px')[0] - this.margin.left - this.margin.right; this._svg.attr('width', this.width + this.margin.left + this.margin.right) this.x.range([0, this.width]) this.xAxis.call(d3.axisBottom(this.x)) if (this.xType === 'i') { this.elems.transition() .attr('width', this.x.bandwidth()) .attr('x', d => this.x(d.key)) } else { this.elems.transition() .attr('cx', d => this.x(d.Time)) } } function dashboard () { this.charts = []; this.addChart = function(chart) { chart.link(this.charts.map(d => d.chart)); this.charts.forEach(d => { d.chart.link([chart]) }); this.charts.push({id: Math.floor(Math.random() * 100), chart: chart}); } } </script> </body>
https://d3js.org/d3.v5.min.js