forked from AlexDaGr8's block: Interactive and attempting to be more DRY with the code
xxxxxxxxxx
<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