var ValueChart = function(data,elementId,width,height,margin) { //Set Defaults margin = margin || {top:10,left:80,bottom:40,right:30}; width = width || 960 - margin.left - margin.right; height = height || 500 - margin.top - margin.bottom; //Object to store selection var s = {}; //Draw Plot Area s.svg = d3.select("#" + elementId) .append("svg") .attr("height", height + margin.top + margin.bottom) .attr("width", width + margin.left + margin.right) .style("-webkit-user-select","none") // Disable Highlighting .style("cursor","default"); // Disable cursor style changes s.chart = s.svg.append("g") .attr("transform","translate(" + margin.left + "," + margin.top + ")"); s.chart.append("clipPath") .attr("id","regLineClip") .append("rect") .attr("width", width) .attr("height", height); // Object to store scales var scale = {}; var ytv = [2.5,3,3.5,4,4.5,5]; scale.x = d3.scaleLinear() .domain(d3.extent(data.map(function(d) { return d.cost;}))) .range([0,width]) .nice(); scale.y = d3.scaleLinear() .domain(d3.extent(ytv)) .range([height,0]) scale.color = d3.scaleOrdinal() .domain(d3.set(data.map(function(d) { return d.name;})).values()); scale.color.range(scale.color.domain().map(function(d,i) { return d3.interpolateSpectral(i/scale.color.domain().length)})); //Object to store axes var axis = {}; axis.x = d3.axisBottom(scale.x).tickFormat(function(d) { if(d<=4) { return Array(d+1).join('$') } else { return ('$') + "×" + (d); } }) axis.y = d3.axisLeft(scale.y).tickValues(ytv).tickFormat(function(d) { return Array(Math.floor(d) + 1).join('★') + Array(Math.ceil(d)-Math.floor(d) + 1).join('½') }) //Object to store grids var grid = {}; grid.x = d3.axisBottom(scale.x) .tickSizeInner(-height) .tickFormat(""); grid.y = d3.axisLeft(scale.y) .tickSizeInner(-width) .tickFormat("") .tickValues(ytv); // Calculate reg line and draw line, polygons and labels var regCoefs = lsReg( data.map(function(d) { return scale.x(d.cost)}), data.map(function(d) { return scale.y(d.quality)}) ); s.chart.append("polygon") .attr("clip-path", "url(#regLineClip)") .style("fill","#5cb85c") .style("fill-opacity",.25) .attr("points", "0,0 0," + regCoefs[1] + " " + width + "," + (width*regCoefs[0] + regCoefs[1]) ) s.chart.append("polygon") .attr("clip-path", "url(#regLineClip)") .style("fill","#d9534f") .style("fill-opacity",.25) .attr("points", "0," + regCoefs[1] + " 0," + height + " " + width + "," + height + " " + width + "," + (width*regCoefs[0] + regCoefs[1]) ) s.line = s.chart.append("line") .attr("clip-path", "url(#regLineClip)") .attr("x1", 0) .attr("y1", regCoefs[1] ) .attr("x2", width) .attr("y2", width*regCoefs[0] + regCoefs[1]) .style("stroke","white") .attr("stroke-dasharray","5 5") // Draw reg line label s.chart.append("g") .attr("transform","translate(" +scale.x(5) + "," + scale.y(3.75) + ")") .append("text") .attr("dy", -15) .attr("dx",26) .text("More ★★ per $") .attr("transform","rotate(-19.0)") .style("fill","white") .style("font-weight","bold") .style("font-size","1.5em"); s.chart.append("g") .attr("transform","translate(" +scale.x(10.0) + "," + scale.y(4.2) + ")") .append("text") .attr("dy",30) .attr("dx",-26) .text("More $$ per ★") .attr("transform","rotate(-19.0)") .style("fill","white") .style("font-weight","bold") .style("font-size","1.5em"); // Draw Grids // Object to store grid selections s.grid = {}; s.grid.x = s.chart.append("g") .attr("transform","translate(0," + height + ")") .call(grid.x) .each(function(d) { d3.select(this) .style("opacity", .15) .select(".domain") .style("display","none") }); s.grid.y = s.chart.append("g") .call(grid.y) .each(function(d) { d3.select(this) .style("opacity", .15) .select(".domain") .style("display","none") }); // Draw Axes // Object to store axis selection s.axis = {}; s.axis.x = s.chart.append("g") .attr("transform","translate(0," + (height + 16) + ")") .call(axis.x); s.axis.x.append("text") .text("Cost") .style("fill","black") .attr("dx", width) .attr("dy", -3) .attr("text-anchor","end") s.axis.y = s.chart.append("g") .attr("transform","translate(-16,0)") .call(axis.y) s.axis.y.append("text") .attr("transform","rotate(90)") .text("Quality") .style("fill","black") .attr("dy", -5) .attr("text-anchor","start"); //Simulate Forces to jitter points var force = d3.forceSimulation(data) .force("x", d3.forceX(function(d) { return scale.x(d.cost); }).strength(1)) .force("y", d3.forceY(function(d) { return scale.y(d.quality); }).strength(1)) .force("collide", d3.forceCollide(6)) .stop(); for (var i = 0; i < 120; ++i) force.tick(); // Draw Points s.points = s.chart.selectAll(".point") .data(data) .enter() .append("g") .attr("transform",function(d) { return "translate(" + d.x + "," + d.y+ ")"}) .on("mouseenter", mouseenterPoint) .on("mouseleave", mouseleavePoint); s.points.append("circle") .attr("r",6) .style("fill", function(d) { return scale.color(d.name)}) .style("stroke","gray"); //Setup the readout // Object to store readout components s.readout = {}; s.readout.g = s.chart.append("g") .attr("transform","translate(" + (width*2/3) + "," + height*(4/5) + ")") .style("display","none"); s.readout.underlay = s.readout.g.append("rect") s.readout.rect = s.readout.g.append("rect") s.readout.name = s.readout.g.append("text") .attr("y",6) .attr("text-anchor","middle"); s.readout.scores = s.readout.g.append("text") .attr("dy",24) .attr("text-anchor","middle") // Functions to update the readout function mouseenterPoint(d) { var rx,ry; var rw = 0; s.readout.g.style("display",null) s.readout.name.text(d.name) .style("fill","black") .each(function(d) { var bb = this.getBBox(); bb.width > rw ? rw = bb.width : null; ry = bb.y; }); // Quality in stars, cost in $, +/- for cost var quality, cost, costSign; // Calculate how many stars to display quality = Array(Math.floor(d.quality) + 1).join('★') + Array(Math.ceil(d.quality)-Math.floor(d.quality) + 1).join('½'); // Calculate whether to include a +/- sign if(d.cost - Math.floor(d.cost) >= .75) { costSign = "+"; } else if(d.cost - Math.floor(d.cost) >= .25) { costSign = "-"; } else { costSign = ""; } // Calculate how many $ to display if(d.cost<6) { cost = Array(Math.floor(d.cost)+1).join('$') } else { cost = ('$') + "×" + (Math.floor(d.cost)); } s.readout.scores.text("Quality: " + quality + " Cost: " + cost+costSign + " Room: " + d.room) .attr("xml:space", "preserve") .style("fill","black") .each(function(d) { var bb = this.getBBox(); bb.width > rw ? rw = bb.width : null; }); s.readout.underlay.attr("x", -(rw+8)/2) .attr("y", ry) .attr("width", rw+8) .attr("height",38) .attr("rx",4) .style("fill", "white") .style("fill-opacity",.5) .style("stroke-width",2); s.readout.rect.attr("x", -(rw+8)/2) .attr("y", ry) .attr("width", rw+8) .attr("height",38) .attr("rx",4) .style("fill", scale.color(d.name)) .style("fill-opacity",.5) .style("stroke", scale.color(d.name)) .style("stroke-width",2); } function mouseleavePoint() { s.readout.g.style("display","none") } function lsReg(X,Y) { var meanX = d3.mean(X), meanY = d3.mean(Y); var ssXX = d3.sum(X.map(function(d) { return Math.pow(d - meanX, 2); })), ssYY = d3.sum(Y.map(function(d) { return Math.pow(d - meanY, 2); })); var ssXY = d3.sum(X.map(function(d, i) { return (d - meanX) * (Y[i] - meanY);})) var slope = ssXY / ssXX; var intercept = meanY - (meanX * slope); return [slope, intercept]; } }