// extend d3 to be able to wrap long texts. textUnits: em || px d3.wrap = function(text, width, textUnits) { text.each(function() { var text = d3.select(this), words = text.text().split(/\s+/).reverse(), word, line = [], lineHeight = (textUnits == 'em'? 1.1 : 15), // ems y = text.attr("y"), parsedDy = parseFloat(text.attr("dy")), dy = (isNaN(parsedDy)? 0 : parsedDy), tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + textUnits), numPrevTspans; while (word = words.pop()) { line.push(word); tspan.text(line.join(" ")); if (tspan.node().getComputedTextLength() > width) { line.pop(); tspan.text(line.join(" ")); line = [word]; numPrevTspans = text.selectAll("tspan").size(); tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", lineHeight*numPrevTspans + dy + textUnits).text(word); } } }); } //////////////////////////////////////////////////////// // relevant stuff, here var data, listUniversities, selectedUniversity, // property attributes areaNameLabel = 'areaName', univNameLabel = 'univName', specializationIndexLabel = 'specializationIndex', //svg stuff svg, margin, width, height, x, y, xAxisBottom, xAxisTop, transition_duration, yAxis, colorAbove, colorBelow; // load data and boostrap d3.json('universities_SI.json', function(data) { setup(); prepareData(data); createCombobox(); }) var setup = function() { // svg settings margin = {top: 90, right: 50, bottom: 60, left: 0}, width = 600 - margin.left - margin.right, height = 470 - margin.top - margin.bottom, transition_duration = 500; x = d3.scale.log() .base(2) .range([0, width]); y = d3.scale.ordinal() .rangeRoundBands([0, height], 0.1) xAxisBottom = d3.svg.axis() .scale(x) .orient("bottom") .tickFormat(function(d) { return d; }); xAxisTop = d3.svg.axis() .scale(x) .orient("top") .tickFormat(function(d) { return d; }); yAxis = d3.svg.axis() .scale(y) .orient("left") .tickSize(0) .tickPadding(6); svg = d3.select("svg") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); // create top and bottom axis svg.append("g") .attr("class", "x axis bottom") .attr("transform", "translate(0," + height + ")"); svg.append("g") .attr("class", "x axis top"); }; var createCombobox = function() { //add selector var select = d3.select('#select-placeholder') .append('select') .attr('class','select') .on('change',function() { onChange(d3.select('select').property('value')); }) var options = select .selectAll('option') .data(listUniversities).enter() .append('option') .text(function (d) { return d; }); } var onChange = function(newValue) { selectedUniversity = newValue; console.log("new value"); // remove previous stuff. Not useful // to do transitions here svg.selectAll('.group_field').remove(); d3.select('svg').selectAll('.si-threshold-label').remove(); // render the viz with the filtered data render( _.filter(data, function(d) { return d[univNameLabel] == selectedUniversity; }) ); } var prepareData = function(dataset) { // add univName property to those records containing // data at possible national level dataset[0].forEach(function(d) { d[univNameLabel] = 'National level'; }); data = _.concat(dataset[0], dataset[1]); // get list of universities listUniversities = _(data) .uniqBy(univNameLabel) .map(univNameLabel) .value(); // auto select first option (national data) onChange(listUniversities[0]); } var updateBars = function(data, selection) { //update var gs = selection .data(data, function(d) { return d[areaNameLabel]; }) .enter() .append('g') .attr('class', 'group_field') .attr('transform', function(d) { // move all the 'g' to the x(1): those 'g' // with S.I value < 1 will be animated later return 'translate(' + x(1) + ' , ' + y(d[areaNameLabel]) + ')'; }) // for those bars going to the // reverse duration, animate them gs.filter( function(d) { return d[specializationIndexLabel] < 1; } ) .transition() .duration(transition_duration) .attr('transform', function(d) { return 'translate(' + x(d[specializationIndexLabel]) + ' , ' + y(d[areaNameLabel]) + ')'; }) gs.append('rect') .attr('class', 'bar') .attr('fill', function(d, i) { var multiple = d[specializationIndexLabel]; return (multiple >= 1)? colorAbove(multiple) : colorBelow(multiple); }) .attr('width', 0) .attr("height", function(d) { return y.rangeBand() ; }) // save final width of the bar as // we need this datum to positionate // the texts accordingly .attr('final_width', function(d) { return (d[specializationIndexLabel] >= 1)? x(d[specializationIndexLabel]) - x(1) : x(1) - x(d[specializationIndexLabel]); }) .transition() .duration(transition_duration) .attr("width", function(d) { return (d[specializationIndexLabel] >= 1)? x(d[specializationIndexLabel]) - x(1) : x(1) - x(d[specializationIndexLabel]); }); gs.append('text') .attr('class', 'areaName') .attr('opacity', 0) .attr('y', y.rangeBand()/2) .attr('alignment-baseline', 'middle') .attr('x', function(d) { return (d[specializationIndexLabel] >= 1)? -5 : +d3.select(this.parentNode).select('rect').attr('final_width') + 5; }) .style('text-anchor', function(d) { return (d[specializationIndexLabel] >= 1)? 'end' : 'start'; }) .text(function(d) { return d[areaNameLabel]; }) .transition() .delay(transition_duration / 2) .attr('opacity', 1); } var render = function(data) { data = _(data) .sortBy(specializationIndexLabel) .reverse() .value(); // for those values greater than 1: 2 means that the SI is 2 times more // for those values less than 1: 0.5 means that the SI is 2 times less, 0.25 means that the SI is 4 times less var extentValues = [ 0.1, 1 + d3.max(data, function(d) { return d[specializationIndexLabel]; }) ]; //two color scales for the bars: //one for values that are above 0: from grey (0) to green (values) //one for values that are below 0: from grey (0) to red (values) colorAbove = d3.scale.linear().domain([0, extentValues[1]]).range(['#F2D516', '#20B2AA']); colorBelow = d3.scale.linear().domain([0,1]).range(['#DC143C', '#F2D516']); x.domain(extentValues).nice(); y.domain(data.map(function(d) { return d[areaNameLabel]; })); // one set of bars to show specialization index updateBars(data, svg.selectAll(".bar-specialization_index")); // vertical line with at 1, which is the threshold // that defines whether you are specialized or not svg.append('line') .attr('class', 'si-threshold') .attr('x1', x(1)) .attr('x2', x(1)) .attr('y1', 0) .attr('y2', height); // contine the previous line above the top axis, // draw outside the svg group var label_y_offset = -70; svg .append('line') .attr('class', 'si-threshold-label') .attr('x1', margin.left + x(1)) .attr('x2', margin.left + x(1)) .attr('y1', label_y_offset) .attr('y2', 0); svg .append('circle') .attr('class', 'si-threshold-label') .attr('r', 3) .attr('cx', margin.left + x(1)) .attr('cy', label_y_offset) svg .append('text') .attr('class', 'si-threshold-label') .attr('x', margin.left + x(1)) .attr('y', label_y_offset) .text('The value 1 represents the reference point defining whether if there is more or less specialization'); svg.select('text.si-threshold-label') .call(d3.wrap, 200, 'px'); d3.select('svg text.si-threshold-label') .selectAll('tspan') .attr('x', margin.left + x(1) + 5); svg.select(".x.axis.top") .transition() .duration(200) .call(xAxisTop); svg.select(".x.axis.bottom") .transition() .duration(200) .call(xAxisBottom); }