|
// The top-100 artist chart. |
|
// Reusable chart definition - see http://bost.ocks.org/mike/chart/. |
|
// |
|
function artistChart() { |
|
var margin = {top: 30, right: 40, bottom: 30, left: 200}, |
|
width = 1400, |
|
height = 1400, |
|
dataValues = function (d) { |
|
return d; |
|
}, |
|
yearScale = d3.time.scale(), |
|
artistScale = d3.scale.ordinal(), |
|
positionScale = d3.scale.linear().domain([1, 100]), |
|
colourScale = d3.scale.linear().domain([1, 50, 100]).range(["red", "yellow", "white"]), |
|
summary = [], |
|
xAxis1 = d3.svg.axis().scale(yearScale).orient("top").ticks(d3.time.year, 1), |
|
xAxis2 = d3.svg.axis().scale(yearScale).orient("bottom").ticks(d3.time.year, 1), |
|
glyph = d3.svg.symbol(), |
|
radius = 5; |
|
|
|
function chart(selection) { |
|
selection.each(function (data) { |
|
|
|
// Convert data to standard representation greedily; |
|
// this is needed for non-deterministic accessors. |
|
data = data.map(function (d, i) { |
|
return dataValues.call(data, d, i); |
|
}); |
|
|
|
// Calculate counts and medians. |
|
summary = d3.nest() |
|
.key(function (d) { |
|
return d.Artist; |
|
}) |
|
.rollup(function (d) { |
|
return { |
|
count: d.length, median: d3.median(d, function (d) { |
|
return d.Position; |
|
}) |
|
}; |
|
}) |
|
.entries(data); |
|
|
|
// Update the year scale. |
|
yearScale |
|
.domain(d3.extent(data, function (d) { |
|
return d.Year; |
|
})) |
|
.range([width - margin.left - margin.right, 0]); |
|
|
|
// Update the positionScale. |
|
var diff = Math.abs(yearScale(new Date(2000, 1, 1)) - yearScale(new Date(2001, 1, 1))) * 0.9; |
|
positionScale |
|
.range([-diff / 2, diff / 2]); |
|
|
|
// Update the artist scale. |
|
var artists = summary.map(function (d) { |
|
return d.key; |
|
}); |
|
artistScale |
|
.domain(artists) |
|
.rangePoints([0, height - margin.top - margin.bottom]); |
|
|
|
// Select the svg element, if it exists. |
|
var svg = d3.select(this).selectAll("svg").data([data]); |
|
|
|
// Otherwise, create the skeletal chart. |
|
var gEnter = svg.enter().append("svg").append("g"); |
|
gEnter.append("g").attr("class", "legend"); |
|
gEnter.append("g").attr("class", "x axis top"); |
|
gEnter.append("g").attr("class", "x axis bottom"); |
|
gEnter.append("g").attr("class", "y axis"); |
|
gEnter.append("g").attr("class", "glyphs"); |
|
|
|
// Update the outer dimensions. |
|
svg.attr("width", width) |
|
.attr("height", height); |
|
|
|
// Update the inner dimensions. |
|
var g = svg.select("g") |
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
|
|
// X-axis. |
|
g.select(".x.axis.top") |
|
.attr("transform", "translate(0," + artistScale.range()[0] + ")") |
|
.call(xAxis1) |
|
.on("mouseover", yearMouseOver) |
|
.on("mouseout", axisMouseOut); |
|
g.select(".x.axis.bottom") |
|
.attr("transform", "translate(0," + artistScale.range()[artists.length - 1] + ")") |
|
.call(xAxis2) |
|
.on("mouseover", yearMouseOver) |
|
.on("mouseout", axisMouseOut); |
|
|
|
// Y-axis: use a custom axis as d3.axis doesn't work well with mouse-over. |
|
g.select(".y.axis") |
|
.attr("transform", "translate(" + yearScale.range()[1] + ",0)") |
|
.selectAll("text") |
|
.data(artists) |
|
.enter() |
|
.append("text") |
|
.text(function (d) { |
|
return d; |
|
}) |
|
.attr("x", -30) |
|
.attr("y", function (d) { |
|
return artistScale(d) |
|
}) |
|
.attr("dy", "0.3em") |
|
.on("mouseover", artistMouseOver) |
|
.on("mouseout", axisMouseOut); |
|
|
|
// Plot glyphs. |
|
g.select(".glyphs") |
|
.selectAll("path") |
|
.data(function (d) { |
|
return d; |
|
}) |
|
.enter() |
|
.append("path") |
|
.attr("transform", function (d) { |
|
return "translate(" + X(d) + "," + Y(d) + ")"; |
|
}) |
|
.attr("d", glyph) |
|
.attr("class", function (d) { |
|
return "artist" + artistScale.domain().indexOf(d.Artist) + " year" + d.Year.getFullYear(); |
|
}) |
|
.style("fill", function (d) { |
|
return colourScale(d.Position) |
|
}) |
|
.on("mouseover", glyphMouseOver) |
|
.on("mouseout", glyphMouseOut); |
|
|
|
// Legend: text. |
|
g.select(".legend") |
|
.attr("transform", "translate(-170,-20)") |
|
.selectAll("text") |
|
.data(colourScale.domain()) |
|
.enter() |
|
.append("text") |
|
.text(function (d) { |
|
return d; |
|
}) |
|
.attr("x", function (d) { |
|
return d; |
|
}) |
|
.attr("dx", "0.3em") |
|
.attr("dy", "0.3em"); |
|
|
|
// Legend: glyphs. |
|
g.select(".legend") |
|
.selectAll("circle") |
|
.data(colourScale.domain()) |
|
.enter() |
|
.append("circle") |
|
.attr("cx", function (d) { |
|
return d - radius; |
|
}) |
|
.attr("cy", 0) |
|
.attr("r", radius) |
|
.style("fill", function (d) { |
|
return colourScale(d) |
|
}); |
|
}); |
|
} |
|
|
|
// Display tooltip on mouse over glyph. |
|
function glyphMouseOver(d) { |
|
|
|
// Create and display tooltip. |
|
var x = d3.event.pageX + 5; |
|
var y = d3.event.pageY + 5; |
|
d3.select("#tooltip.track") |
|
.style("left", x + "px") |
|
.style("top", y + "px"); |
|
d3.select("#tooltip.track #title") |
|
.text(d.Title); |
|
d3.select("#tooltip.track #artist") |
|
.text(d.Artist); |
|
d3.select("#tooltip.track #posyr") |
|
.text('#' + d.Position + ' (' + d.Year.getFullYear() + ')'); |
|
d3.select("#tooltip.track").classed("hidden", false); |
|
} |
|
|
|
// Hide tooltip on mouse exit from glyph. |
|
function glyphMouseOut() { |
|
d3.select("#tooltip.track").classed("hidden", true); |
|
} |
|
|
|
// Handle mouse-over year axis label. |
|
function yearMouseOver() { |
|
|
|
// Get the year. |
|
var date = yearScale.invert(d3.mouse(this)[0]); |
|
var year = date.getMonth() > 6 ? date.getFullYear() + 1 : date.getFullYear(); |
|
|
|
// Highlight this year's glyphs. |
|
d3.select(".glyphs") |
|
.selectAll("path") |
|
.style("opacity", 0.1); |
|
d3.selectAll(".year" + year) |
|
.style("opacity", 1.0); |
|
} |
|
|
|
// Handle mouse-over artist axis. |
|
function artistMouseOver(d, i) { |
|
|
|
// Highlight glyphs. |
|
d3.select(".glyphs") |
|
.selectAll("path") |
|
.style("opacity", 0.1); |
|
d3.selectAll(".artist" + i) |
|
.style("opacity", 1.0); |
|
|
|
// Create and display tooltip. |
|
var x = d3.event.pageX + 5; |
|
var y = d3.event.pageY + 20; |
|
var s = summary[i]; |
|
d3.select("#tooltip.artist") |
|
.style("left", x + "px") |
|
.style("top", y + "px"); |
|
d3.select("#tooltip.artist #artist") |
|
.text('#' + (i + 1) + ' ' + s.key); |
|
d3.select("#tooltip.artist #entries") |
|
.text(s.values.count + " entries"); |
|
d3.select("#tooltip.artist #median") |
|
.text(s.values.median + " median"); |
|
d3.select("#tooltip.artist").classed("hidden", false); |
|
} |
|
|
|
function axisMouseOut() { |
|
|
|
// Reset glyph opacity. |
|
d3.select(".glyphs") |
|
.selectAll("path") |
|
.style("opacity", null); |
|
|
|
// Hide tooltips. |
|
d3.select("#tooltip.artist").classed("hidden", true); |
|
} |
|
|
|
// The x-accessor for the path generator; yearScale ∘ xValue. |
|
function X(d) { |
|
return yearScale(d.Year) + positionScale(d.Position); |
|
} |
|
|
|
// The x-accessor for the path generator; artistScale ∘ yValue. |
|
function Y(d) { |
|
return artistScale(d.Artist); |
|
} |
|
|
|
chart.margin = function (_) { |
|
if (!arguments.length) return margin; |
|
margin = _; |
|
return chart; |
|
}; |
|
|
|
chart.width = function (_) { |
|
if (!arguments.length) return width; |
|
width = _; |
|
return chart; |
|
}; |
|
|
|
chart.height = function (_) { |
|
if (!arguments.length) return height; |
|
height = _; |
|
return chart; |
|
}; |
|
|
|
chart.x = function (_) { |
|
if (!arguments.length) return xValue; |
|
xValue = _; |
|
return chart; |
|
}; |
|
|
|
chart.y = function (_) { |
|
if (!arguments.length) return yValue; |
|
yValue = _; |
|
return chart; |
|
}; |
|
|
|
chart.tracks = function (_) { |
|
if (!arguments.length) return dataValues; |
|
dataValues = _; |
|
return chart; |
|
}; |
|
|
|
return chart; |
|
} |
|
|
|
// Process raw data. |
|
function transformData(data) { |
|
var format = d3.time.format("%Y"); |
|
data.forEach(function (d) { |
|
d.Year = format.parse(d.Year); |
|
d.Position = +d.Position; |
|
}); |
|
|
|
return data; |
|
} |
|
|
|
// Call-back function after loading data. |
|
function display(error, data) { |
|
|
|
if (error) { |
|
console.log(error); |
|
return; |
|
} |
|
|
|
var chart = artistChart() |
|
.tracks(function (d) { |
|
return d; |
|
}); |
|
d3.select("#chart").datum(transformData(data)).call(chart); |
|
} |
|
|
|
// Read the data file. |
|
queue() |
|
.defer(d3.csv, "jjj_hottest_100_artists.csv") |
|
.await(display); |