D3
OG
Old school D3 from simpler times
All examples
By author
By category
About
AlexDaGr8
Full window
Github gist
5000 movie database... just playing around
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <script src="https://d3js.org/d3.v5.min.js"></script> <script> let margin = { top: 20, right: 40, bottom: 20, left: 40 }, width = 960 - margin.left - margin.right, height = 500 - margin.top - margin.bottom, padding = 20, fill = 'black'; let svg = d3.select('body').append('svg') .attr('width', width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom) .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`); d3.csv('tmdb_5000_movies.csv', (d) => { return { budget: +d.budget, popularity: +d.popularity, revenue: +d.revenue, runtime: +d.runtime, vote_average: +d.vote_average, vote_count: +d.vote_count, release_date: new Date(d.release_date), title: d.title }; }) .then((data) => { console.log(data) let scatter = new scatterPlot(svg, data, width / 2 - padding, height / 2 - padding, 'budget', 'popularity'); let bar = new barPlot(svg, data, width / 2 - padding, height / 2 - padding, 'Average Vote', 'Vote Count', { left: width / 2 + padding, top: 0 }) let time = timePlot(svg, data, width, height / 2 - padding, 'Date', 'Run Time', { left: 0, top: height / 2 + padding }) time.updateArr.push({ plot: bar, func: bar.updateData }); bar.updateArr.push({ plot: scatter, func: scatter.update }); }) function timePlot(elem, data, width, height, x_label, y_label, translate) { this.updateArr = []; let g = elem.append('g') .attr('transform', `translate(${translate.left}, ${translate.top})`); data = data.sort((a, b) => d3.descending(a.release_date, b.release_date)) let x = d3.scaleTime() .domain(d3.extent(data, d => d.release_date)) .range([0, width]) let xAxis = g.append('g') .attr('transform', `translate(0,${height})`) .call(d3.axisBottom(x)); let y = d3.scaleLinear() .domain([0, d3.max(data, d => d.runtime)]) .range([height, 0]); let yAxis = g.append('g') .call(d3.axisLeft(y)); // line config let lineData = data.map(d => ({ date: new Date(d.release_date.getFullYear(), 0, 1)})) .filter((date, i, self) => self.findIndex(d => d.date.getTime() === date.date.getTime()) === i); lineData.forEach(ld => { ld.value = data.filter(d => d.release_date.getFullYear() === ld.date.getFullYear()).length; }); // console.log('lineData', lineData); let lineY = d3.scaleLinear() .domain(d3.extent(lineData, d => d.value)) .range([height, 0]); let lineYAxis = g.append('g') .attr('transform', `translate(${width}, 0)`) .call(d3.axisRight(lineY)); let line = d3.line() .x(d => x(d.date)) .y(d => lineY(d.value)); let linePath = g.append('path') .datum(lineData) .attr('d', line) .attr('fill', 'none') .attr('stroke', 'white'); let circles = g.selectAll('circle.time') .data(data) .enter().append('circle') .attr('class', 'time') .attr('cx', x(x.domain[0])) .attr('cy', y(y.domain().reduce((a, c) => a + c, 0) / 2)) .attr('fill', fill) circles.transition().duration(100) .delay((d, i) => i + 2) .ease(d3.easeQuadIn) .attr('cx', d => x(d.release_date)) .attr('cy', d => y(d.runtime)) .attr('r', 1); addLabels(g, x_label, y_label, width, height, 'white'); let brushed = () => { let selection = d3.event.selection; if (selection === null) { // console.log('no selection') } else { const [mins, maxes] = selection; const x0 = x.invert(mins[0]), y1 = y.invert(mins[1]), x1 = x.invert(maxes[0]), y0 = y.invert(maxes[1]); circles.attr('stroke', 'none'); let brushedData = circles .filter(d => { let xScaleBool = d.release_date > x0 && d.release_date < x1; let yScaleBool = d.runtime > y0 && d.runtime < y1; return xScaleBool && yScaleBool; }) .attr('stroke-width', 1) .attr('stroke', 'white').data(); // console.log('brushedData', brushedData) this.updateArr.forEach(d => { // console.log('d', d) d.func.call(d.plot, brushedData) }) } } let brushEnd = () => { let selection = d3.event.selection; if (selection === null) { circles.attr('stroke', 'none') this.updateArr.forEach(d => { // console.log('d', circles.data()) d.func.call(d.plot, circles.data()) }) } } let brush = g.append('g') .attr('class', 'brush') .call(d3.brush() .on('brush', brushed) .on('end', brushEnd) ); return this; } timePlot.prototype.brushed = function () { return this; } function barPlot(elem, data, width, height, x_label, y_label, translate) { // console.log('transform', translate) this.ogData = data; this.height = height; this.data = this.formatData(data) this.updateArr = []; this.g = elem.append('g') .attr('transform', `translate(${translate.left}, ${translate.top})`); this.x = d3.scaleBand() .padding(.1) .domain([...new Set(this.data.sort((a, b) => a.key - b.key).map(d => d.key))]) .rangeRound([0, width]); this.xAxis = this.g.append('g') .attr('transform', `translate(0, ${this.height})`) .call(d3.axisBottom(this.x)); this.y = d3.scaleLinear() .domain(d3.extent(this.data, d => d.value)) .range([this.height, 0]) this.yAxis = this.g.append('g') .call(d3.axisLeft(this.y)); this.bars = this.g.selectAll('rect.bar') .data(this.data) .enter().append('rect') .classed('bar', true) .attr('x', d => this.x(d.key)) .attr('width', this.x.bandwidth()) .attr('y', this.height) .attr('height', 0) this.bars.transition().duration(1000) .ease(d3.easeBounce) .attr('y', d => this.y(d.value)) .attr('height', d => this.height - this.y(d.value)) .attr('fill', fill); this.clickBars = this.g.selectAll('rect.clickable') .data(this.data) .enter().append('rect') .classed('clickable', true) .attr('x', d => this.x(d.key)) .attr('width', this.x.bandwidth()) .attr('y', 0) .attr('height', this.height) .style('opacity', 0) this.clickBars.on('mouseover', () => { d3.select(d3.event.target).style('opacity', .2) }) this.clickBars.on('mouseout', () => { d3.select(d3.event.target).style('opacity', 0) }) this.clickBars.on('click', () => { const clickElem = d3.select(d3.event.target); this.bars.attr('stroke', 'none'); let updateData = []; // console.log('dataclicked', clickElem.attr('data-clicked')) if (clickElem.attr('data-clicked') === 'true') { clickElem.attr('data-clicked', false); } else { this.clickBars.attr('data-clicked', false); clickElem.attr('data-clicked', true); let barData = this.bars.filter(d => d.key === clickElem.data()[0].key).attr('stroke', 'white').data(); updateData = this.ogData.filter(d => Math.floor(d.vote_average) === barData[0].key); } this.updateArr.forEach(d => { d.func.call(d.plot, updateData); }) }) addLabels(this.g, 'Average Vote', 'Vote Count', width, this.height, 'white'); // // axis labels // let xLabel = svg.append('text') // .text('Average Vote') // .attr('fill', 'seagreen') // .attr('text-anchor', 'end') // .attr('x', width - margin.right) // .attr('y', height - margin.bottom) // let yLabel = svg.append('text') // .text('Vote Count') // .attr('fill', 'seagreen') // .attr('text-anchor', 'end') // .attr('transform', 'rotate(-90)') // .attr('x', -margin.top) // .attr('y', margin.right) return this; } barPlot.prototype.formatData = function (newData) { return [...new Set(newData.map(d => Math.floor(d.vote_average)))] .map(d => { return { key: d, value: newData.filter(g => g.vote_average === d).length, } }); } barPlot.prototype.updateData = function (newData) { this.data = this.formatData(newData); this.bars.data(this.data.sort((a,b) => a.key - b.key)); //this.bars.exit().remove(); this.bars.enter(); this.bars.transition().duration(500) .attr('x', d => this.x(d.key)) .attr('y', d => this.y(d.value)) .attr('height', d => this.height - this.y(d.value)) } function scatterPlot(elem, data, width, height, x_label, y_label, translate) { this.g = elem.append('g') .attr('transform', translate); this.data = data.sort((a, b) => d3.ascending(a[x_label], b[x_label])) // x things this.x = // d3.scalePow() // .exponent(0.2) d3.scaleLinear() .domain(d3.extent(this.data, d => d[x_label])) .range([0, width]); this.xAxis = this.g.append('g') .attr('transform', `translate(0, ${height})`) //.call(d3.axisBottom(x)); .call(d3.axisBottom(this.x).tickFormat(d3.format("($.2s")).ticks(3)); // y things this.y = d3.scaleLinear() .domain(d3.extent(this.data, d => d[y_label])) .range([height, 0]); this.yAxis = this.g.append('g') .call(d3.axisLeft(this.y)); this.circles = this.g.selectAll('circle.movie') .data(this.data) .enter() .append('circle') .attr('class', 'movie') .attr('r', 1) .attr('cx', width / 2) .attr('cy', this.y(this.y.domain()[1])) .attr('fill', fill) this.circles.transition().duration(100) .delay((d, i) => i + 2) .ease(d3.easeBounce) .attr('cx', d => this.x(d[x_label])) .attr('cy', d => this.y(d[y_label])); addLabels(this.g, x_label, y_label, width, height, 'white'); // // axis labels // let xLabel = g.append('text') // .text(x_label) // .attr('text-anchor', 'end') // .attr('x', width - margin.right) // .attr('y', height - margin.bottom) // let yLabel = g.append('text') // .text(y_label) // .attr('text-anchor', 'end') // .attr('transform', 'rotate(-90)') // .attr('x', -margin.top) // .attr('y', margin.right) } scatterPlot.prototype.update = function(newData) { // console.log('data', newData); this.circles.attr('stroke', 'none'); newData.forEach(d => { this.circles.filter(f => f === d).attr('stroke', 'white'); }) } function addLabels(elem, x_label, y_label, width, height, fill = 'black') { // axis labels let xLabel = elem.append('text') .text(x_label) .attr('fill', fill) .attr('text-anchor', 'end') .attr('x', width - margin.right/2) .attr('y', height - margin.bottom) let yLabel = elem.append('text') .text(y_label) .attr('fill', fill) .attr('text-anchor', 'end') .attr('transform', 'rotate(-90)') .attr('x', -margin.top) .attr('y', margin.right/2) } svg.append('rect') .attr('transform', `translate(${-margin.left}, ${-margin.top})`) .attr('width', width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom) .attr('fill', '#777') .attr('stroke-width', 3); </script> </body> </html>
https://d3js.org/d3.v5.min.js