|
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]; |
|
} |
|
|
|
} |