Skip to content

Instantly share code, notes, and snippets.

@josiahdavis
Last active October 3, 2015 02:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save josiahdavis/30202dea3bb3e06b9748 to your computer and use it in GitHub Desktop.
Save josiahdavis/30202dea3bb3e06b9748 to your computer and use it in GitHub Desktop.
How do people feel about Coffee?

Not bad, apparently.

This chart plots the average emtional valence contained within Yelp reviews for coffee shops across 8 emotions, four positive emotions on the right, and four negative emotions on the left. Interestingly, Starbucks seems to provide a less emotionally rich experience than going to other coffee shops. Possibly, this is due to the fact that Starbucks customers dissproportionally dilute their coffee with cream and sugar, and thus dilute their coffee-drinking experience. The greatest joy seems to be coming from the Other category, possibly a testament to the romance often associated with Independent Coffee shops. Apparently, the most anticipated place is Krispy Kreme. It makes sense, people likely eat donuts much less than they drink coffee, so when they go, it's a specical event.

Notes

This chart is built using the the radar-chart-d3 plugin created Alvaro Graves. The emotional valence was estimated using the syuzhet R package created by Matthew Jockers.

(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
var helper = require('./legend');
module.exports = function(){
var scale = d3.scale.linear(),
shape = "rect",
shapeWidth = 15,
shapeHeight = 15,
shapeRadius = 10,
shapePadding = 2,
cells = [5],
labels = [],
useClass = false,
labelFormat = d3.format(".01f"),
labelOffset = 10,
labelAlign = "middle",
labelDelimiter = "to",
orient = "vertical",
ascending = false,
path,
legendDispatcher = d3.dispatch("cellover", "cellout", "cellclick");
function legend(svg){
var type = helper.d3_calcType(scale, ascending, cells, labels, labelFormat, labelDelimiter);
var cell = svg.selectAll(".cell").data(type.data),
cellEnter = cell.enter().append("g", ".cell").attr("class", "cell").style("opacity", 1e-6);
shapeEnter = cellEnter.append(shape).attr("class", "swatch"),
shapes = cell.select("g.cell " + shape);
//add event handlers
helper.d3_addEvents(cellEnter, legendDispatcher);
cell.exit().transition().style("opacity", 0).remove();
helper.d3_drawShapes(shape, shapes, shapeHeight, shapeWidth, shapeRadius, path);
helper.d3_addText(svg, cellEnter, type.labels)
// sets placement
var text = cell.select("text"),
shapeSize = shapes[0].map( function(d){ return d.getBBox(); });
//sets scale
//everything is fill except for line which is stroke,
if (!useClass){
if (shape == "line"){
shapes.style("stroke", type.feature);
} else {
shapes.style("fill", type.feature);
}
} else {
shapes.attr("class", function(d){ return "swatch " + type.feature(d); });
}
var cellTrans,
textTrans,
textAlign = (labelAlign == "start") ? 0 : (labelAlign == "middle") ? 0.5 : 1;
//positions cells and text
if (orient === "vertical"){
cellTrans = function(d,i) { return "translate(0, " + (i * (shapeSize[i].height + shapePadding)) + ")"; };
textTrans = function(d,i) { return "translate(" + (shapeSize[i].width + shapeSize[i].x +
labelOffset) + "," + (shapeSize[i].y + shapeSize[i].height/2 + 5) + ")"; };
} else if (orient === "horizontal"){
cellTrans = function(d,i) { return "translate(" + (i * (shapeSize[i].width + shapePadding)) + ",0)"; }
textTrans = function(d,i) { return "translate(" + (shapeSize[i].width*textAlign + shapeSize[i].x) +
"," + (shapeSize[i].height + shapeSize[i].y + labelOffset + 8) + ")"; };
}
helper.d3_placement(orient, cell, cellTrans, text, textTrans, labelAlign);
cell.transition().style("opacity", 1);
}
legend.scale = function(_) {
if (!arguments.length) return legend;
scale = _;
return legend;
};
legend.cells = function(_) {
if (!arguments.length) return legend;
if (_.length > 1 || _ >= 2 ){
cells = _;
}
return legend;
};
legend.shape = function(_, d) {
if (!arguments.length) return legend;
if (_ == "rect" || _ == "circle" || _ == "line" || (_ == "path" && (typeof d === 'string')) ){
shape = _;
path = d;
}
return legend;
};
legend.shapeWidth = function(_) {
if (!arguments.length) return legend;
shapeWidth = +_;
return legend;
};
legend.shapeHeight = function(_) {
if (!arguments.length) return legend;
shapeHeight = +_;
return legend;
};
legend.shapeRadius = function(_) {
if (!arguments.length) return legend;
shapeRadius = +_;
return legend;
};
legend.shapePadding = function(_) {
if (!arguments.length) return legend;
shapePadding = +_;
return legend;
};
legend.labels = function(_) {
if (!arguments.length) return legend;
labels = _;
return legend;
};
legend.labelAlign = function(_) {
if (!arguments.length) return legend;
if (_ == "start" || _ == "end" || _ == "middle") {
labelAlign = _;
}
return legend;
};
legend.labelFormat = function(_) {
if (!arguments.length) return legend;
labelFormat = _;
return legend;
};
legend.labelOffset = function(_) {
if (!arguments.length) return legend;
labelOffset = +_;
return legend;
};
legend.labelDelimiter = function(_) {
if (!arguments.length) return legend;
labelDelimiter = _;
return legend;
};
legend.useClass = function(_) {
if (!arguments.length) return legend;
if (_ === true || _ === false){
useClass = _;
}
return legend;
};
legend.orient = function(_){
if (!arguments.length) return legend;
_ = _.toLowerCase();
if (_ == "horizontal" || _ == "vertical") {
orient = _;
}
return legend;
};
legend.ascending = function(_) {
if (!arguments.length) return legend;
ascending = !!_;
return legend;
};
d3.rebind(legend, legendDispatcher, "on");
return legend;
};
},{"./legend":2}],2:[function(require,module,exports){
module.exports = {
d3_identity: function (d) {
return d;
},
d3_mergeLabels: function (gen, labels) {
if(labels.length === 0) return gen;
gen = (gen) ? gen : [];
var i = labels.length;
for (; i < gen.length; i++) {
labels.push(gen[i]);
}
return labels;
},
d3_linearLegend: function (scale, cells, labelFormat) {
var data = [];
if (cells.length > 1){
data = cells;
} else {
var domain = scale.domain(),
increment = (domain[domain.length - 1] - domain[0])/(cells - 1),
i = 0;
for (; i < cells; i++){
data.push(domain[0] + i*increment);
}
}
var labels = data.map(labelFormat);
return {data: data,
labels: labels,
feature: function(d){ return scale(d); }};
},
d3_quantLegend: function (scale, labelFormat, labelDelimiter) {
var labels = scale.range().map(function(d){
var invert = scale.invertExtent(d),
a = labelFormat(invert[0]),
b = labelFormat(invert[1]);
// if (( (a) && (a.isNan()) && b){
// console.log("in initial statement")
return labelFormat(invert[0]) + " " + labelDelimiter + " " + labelFormat(invert[1]);
// } else if (a || b) {
// console.log('in else statement')
// return (a) ? a : b;
// }
});
return {data: scale.range(),
labels: labels,
feature: this.d3_identity
};
},
d3_ordinalLegend: function (scale) {
return {data: scale.domain(),
labels: scale.domain(),
feature: function(d){ return scale(d); }};
},
d3_drawShapes: function (shape, shapes, shapeHeight, shapeWidth, shapeRadius, path) {
if (shape === "rect"){
shapes.attr("height", shapeHeight).attr("width", shapeWidth);
} else if (shape === "circle") {
shapes.attr("r", shapeRadius)//.attr("cx", shapeRadius).attr("cy", shapeRadius);
} else if (shape === "line") {
shapes.attr("x1", 0).attr("x2", shapeWidth).attr("y1", 0).attr("y2", 0);
} else if (shape === "path") {
shapes.attr("d", path);
}
},
d3_addText: function (svg, enter, labels){
enter.append("text").attr("class", "label");
svg.selectAll("g.cell text").data(labels).text(this.d3_identity);
},
d3_calcType: function (scale, ascending, cells, labels, labelFormat, labelDelimiter){
var type = scale.ticks ?
this.d3_linearLegend(scale, cells, labelFormat) : scale.invertExtent ?
this.d3_quantLegend(scale, labelFormat, labelDelimiter) : this.d3_ordinalLegend(scale);
type.labels = this.d3_mergeLabels(type.labels, labels);
if (ascending) {
type.labels = this.d3_reverse(type.labels);
type.data = this.d3_reverse(type.data);
}
return type;
},
d3_reverse: function(arr) {
var mirror = [];
for (var i = 0, l = arr.length; i < l; i++) {
mirror[i] = arr[l-i-1];
}
return mirror;
},
d3_placement: function (orient, cell, cellTrans, text, textTrans, labelAlign) {
cell.attr("transform", cellTrans);
text.attr("transform", textTrans);
if (orient === "horizontal"){
text.style("text-anchor", labelAlign);
}
},
d3_addEvents: function(cells, dispatcher){
var _ = this;
cells.on("mouseover.legend", function (d) { _.d3_cellOver(dispatcher, d, this); })
.on("mouseout.legend", function (d) { _.d3_cellOut(dispatcher, d, this); })
.on("click.legend", function (d) { _.d3_cellClick(dispatcher, d, this); });
},
d3_cellOver: function(cellDispatcher, d, obj){
cellDispatcher.cellover.call(obj, d);
},
d3_cellOut: function(cellDispatcher, d, obj){
cellDispatcher.cellout.call(obj, d);
},
d3_cellClick: function(cellDispatcher, d, obj){
cellDispatcher.cellclick.call(obj, d);
}
}
},{}],3:[function(require,module,exports){
var helper = require('./legend');
module.exports = function(){
var scale = d3.scale.linear(),
shape = "rect",
shapeWidth = 15,
shapePadding = 2,
cells = [5],
labels = [],
useStroke = false,
labelFormat = d3.format(".01f"),
labelOffset = 10,
labelAlign = "middle",
labelDelimiter = "to",
orient = "vertical",
ascending = false,
path,
legendDispatcher = d3.dispatch("cellover", "cellout", "cellclick");
function legend(svg){
var type = helper.d3_calcType(scale, ascending, cells, labels, labelFormat, labelDelimiter);
var cell = svg.selectAll(".cell").data(type.data),
cellEnter = cell.enter().append("g", ".cell").attr("class", "cell").style("opacity", 1e-6);
shapeEnter = cellEnter.append(shape).attr("class", "swatch"),
shapes = cell.select("g.cell " + shape);
//add event handlers
helper.d3_addEvents(cellEnter, legendDispatcher);
cell.exit().transition().style("opacity", 0).remove();
//creates shape
if (shape === "line"){
helper.d3_drawShapes(shape, shapes, 0, shapeWidth);
shapes.attr("stroke-width", type.feature);
} else {
helper.d3_drawShapes(shape, shapes, type.feature, type.feature, type.feature, path);
}
helper.d3_addText(svg, cellEnter, type.labels)
//sets placement
var text = cell.select("text"),
shapeSize = shapes[0].map(
function(d, i){
var bbox = d.getBBox()
var stroke = scale(type.data[i]);
if (shape === "line" && orient === "horizontal") {
bbox.height = bbox.height + stroke;
} else if (shape === "line" && orient === "vertical"){
bbox.width = bbox.width;
}
return bbox;
});
var maxH = d3.max(shapeSize, function(d){ return d.height + d.y; }),
maxW = d3.max(shapeSize, function(d){ return d.width + d.x; });
var cellTrans,
textTrans,
textAlign = (labelAlign == "start") ? 0 : (labelAlign == "middle") ? 0.5 : 1;
//positions cells and text
if (orient === "vertical"){
cellTrans = function(d,i) {
var height = d3.sum(shapeSize.slice(0, i + 1 ), function(d){ return d.height; });
return "translate(0, " + (height + i*shapePadding) + ")"; };
textTrans = function(d,i) { return "translate(" + (maxW + labelOffset) + "," +
(shapeSize[i].y + shapeSize[i].height/2 + 5) + ")"; };
} else if (orient === "horizontal"){
cellTrans = function(d,i) {
var width = d3.sum(shapeSize.slice(0, i + 1 ), function(d){ return d.width; });
return "translate(" + (width + i*shapePadding) + ",0)"; };
textTrans = function(d,i) { return "translate(" + (shapeSize[i].width*textAlign + shapeSize[i].x) + "," +
(maxH + labelOffset ) + ")"; };
}
helper.d3_placement(orient, cell, cellTrans, text, textTrans, labelAlign);
cell.transition().style("opacity", 1);
}
legend.scale = function(_) {
if (!arguments.length) return legend;
scale = _;
return legend;
};
legend.cells = function(_) {
if (!arguments.length) return legend;
if (_.length > 1 || _ >= 2 ){
cells = _;
}
return legend;
};
legend.shape = function(_, d) {
if (!arguments.length) return legend;
if (_ == "rect" || _ == "circle" || _ == "line" ){
shape = _;
path = d;
}
return legend;
};
legend.shapeWidth = function(_) {
if (!arguments.length) return legend;
shapeWidth = +_;
return legend;
};
legend.shapePadding = function(_) {
if (!arguments.length) return legend;
shapePadding = +_;
return legend;
};
legend.labels = function(_) {
if (!arguments.length) return legend;
labels = _;
return legend;
};
legend.labelAlign = function(_) {
if (!arguments.length) return legend;
if (_ == "start" || _ == "end" || _ == "middle") {
labelAlign = _;
}
return legend;
};
legend.labelFormat = function(_) {
if (!arguments.length) return legend;
labelFormat = _;
return legend;
};
legend.labelOffset = function(_) {
if (!arguments.length) return legend;
labelOffset = +_;
return legend;
};
legend.labelDelimiter = function(_) {
if (!arguments.length) return legend;
labelDelimiter = _;
return legend;
};
legend.orient = function(_){
if (!arguments.length) return legend;
_ = _.toLowerCase();
if (_ == "horizontal" || _ == "vertical") {
orient = _;
}
return legend;
};
legend.ascending = function(_) {
if (!arguments.length) return legend;
ascending = !!_;
return legend;
};
d3.rebind(legend, legendDispatcher, "on");
return legend;
};
},{"./legend":2}],4:[function(require,module,exports){
var helper = require('./legend');
module.exports = function(){
var scale = d3.scale.linear(),
shape = "path",
shapeWidth = 15,
shapeHeight = 15,
shapeRadius = 10,
shapePadding = 5,
cells = [5],
labels = [],
useClass = false,
labelFormat = d3.format(".01f"),
labelAlign = "middle",
labelOffset = 10,
labelDelimiter = "to",
orient = "vertical",
ascending = false,
legendDispatcher = d3.dispatch("cellover", "cellout", "cellclick");
function legend(svg){
var type = helper.d3_calcType(scale, ascending, cells, labels, labelFormat, labelDelimiter);
var cell = svg.selectAll(".cell").data(type.data),
cellEnter = cell.enter().append("g", ".cell").attr("class", "cell").style("opacity", 1e-6);
shapeEnter = cellEnter.append(shape).attr("class", "swatch"),
shapes = cell.select("g.cell " + shape);
//add event handlers
helper.d3_addEvents(cellEnter, legendDispatcher);
//remove old shapes
cell.exit().transition().style("opacity", 0).remove();
helper.d3_drawShapes(shape, shapes, shapeHeight, shapeWidth, shapeRadius, type.feature);
helper.d3_addText(svg, cellEnter, type.labels)
// sets placement
var text = cell.select("text"),
shapeSize = shapes[0].map( function(d){ return d.getBBox(); });
var maxH = d3.max(shapeSize, function(d){ return d.height; }),
maxW = d3.max(shapeSize, function(d){ return d.width; });
var cellTrans,
textTrans,
textAlign = (labelAlign == "start") ? 0 : (labelAlign == "middle") ? 0.5 : 1;
//positions cells and text
if (orient === "vertical"){
cellTrans = function(d,i) { return "translate(0, " + (i * (maxH + shapePadding)) + ")"; };
textTrans = function(d,i) { return "translate(" + (maxW + labelOffset) + "," +
(shapeSize[i].y + shapeSize[i].height/2 + 5) + ")"; };
} else if (orient === "horizontal"){
cellTrans = function(d,i) { return "translate(" + (i * (maxW + shapePadding)) + ",0)"; };
textTrans = function(d,i) { return "translate(" + (shapeSize[i].width*textAlign + shapeSize[i].x) + "," +
(maxH + labelOffset ) + ")"; };
}
helper.d3_placement(orient, cell, cellTrans, text, textTrans, labelAlign);
cell.transition().style("opacity", 1);
}
legend.scale = function(_) {
if (!arguments.length) return legend;
scale = _;
return legend;
};
legend.cells = function(_) {
if (!arguments.length) return legend;
if (_.length > 1 || _ >= 2 ){
cells = _;
}
return legend;
};
legend.shapePadding = function(_) {
if (!arguments.length) return legend;
shapePadding = +_;
return legend;
};
legend.labels = function(_) {
if (!arguments.length) return legend;
labels = _;
return legend;
};
legend.labelAlign = function(_) {
if (!arguments.length) return legend;
if (_ == "start" || _ == "end" || _ == "middle") {
labelAlign = _;
}
return legend;
};
legend.labelFormat = function(_) {
if (!arguments.length) return legend;
labelFormat = _;
return legend;
};
legend.labelOffset = function(_) {
if (!arguments.length) return legend;
labelOffset = +_;
return legend;
};
legend.labelDelimiter = function(_) {
if (!arguments.length) return legend;
labelDelimiter = _;
return legend;
};
legend.orient = function(_){
if (!arguments.length) return legend;
_ = _.toLowerCase();
if (_ == "horizontal" || _ == "vertical") {
orient = _;
}
return legend;
};
legend.ascending = function(_) {
if (!arguments.length) return legend;
ascending = !!_;
return legend;
};
d3.rebind(legend, legendDispatcher, "on");
return legend;
};
},{"./legend":2}],5:[function(require,module,exports){
d3.legend = {
color: require('./color'),
size: require('./size'),
symbol: require('./symbol')
};
},{"./color":1,"./size":3,"./symbol":4}]},{},[5]);
nameAdj anger disgust sadness fear anticipation surprise trust joy
Caribou Coffee 0.764705882352941 0.352941176470588 0.823529411764706 0.647058823529412 2.58823529411765 0.941176470588235 2.76470588235294 3.17647058823529
Dunkin' Donuts 1.06369426751592 0.75796178343949 0.961783439490446 0.796178343949045 2.78980891719745 1.25477707006369 2.64968152866242 2.68152866242038
Einstein Bros Bagels 1 0.111111111111111 0.666666666666667 0.444444444444444 3.11111111111111 1.44444444444444 3.33333333333333 2.66666666666667
Espressamente Illy 0.529411764705882 0.411764705882353 0.764705882352941 0.588235294117647 2.35294117647059 1.11764705882353 2.23529411764706 2.41176470588235
Krispy Kreme Doughnuts 1.46666666666667 0.866666666666667 0.8 0.933333333333333 3.73333333333333 2 2.93333333333333 3.26666666666667
Other 0.926389222997354 0.660091412076016 1.04041375992302 0.806591291796969 3.16815010825114 1.63002165022853 3.56483040654318 3.8587923983642
Starbucks 0.692675159235669 0.565286624203822 0.780254777070064 0.619426751592357 2.14171974522293 0.751592356687898 2.07643312101911 1.87738853503185
all 0.9012 0.649 1.0022 0.7812 3.0242 1.5046 3.3396 3.5618
<meta charset="utf-8">
<link rel="stylesheet" href="http://yui.yahooapis.com/pure/0.5.0/pure-min.css">
<link rel="stylesheet" href="radar-chart.css">
<style>
body {
padding: 20px;
}
#buttons {
vertical-align: top;
float: right;
}
#chart {
float: left;
}
#container {
width: 700px;
height: 500px;
}
.legend text{
font: 12px sans-serif;
}
.btn.btn-default{
text-align: left;
}
.radar-chart .axis .legend {
font-size: 12px;
}
.radar-chart .axis .legend .middle{
padding-bottom: 25px;
}
.radar-chart .area {
fill-opacity: 0.3;
}
.radar-chart.focus .area {
fill-opacity: 0.0;
}
.radar-chart.focus .area.focused {
fill-opacity: 0.6;
}
.area, .circle {
fill: #8c564b;
stroke: #8c564b;
stroke-width: 3px;
}
</style>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
<div id=container>
<svg id=chart></svg>
<div id=buttons class="btn-group btn-group-vertical">
<button class="btn btn-default" type="submit" value="all">All</button>
<button class="btn btn-default" type="submit" value="Starbucks">Starbucks</button>
<button class="btn btn-default" type="submit" value="Dunkin' Donuts">Dunkin' Donuts</button>
<button class="btn btn-default" type="submit" value="Einstein Bros Bagels">Einstein Bros</button>
<button class="btn btn-default" type="submit" value="Espressamente Illy">Espressamente Illy</button>
<button class="btn btn-default" type="submit" value="Krispy Kreme Doughnuts">Krispy Kreme</button>
<button class="btn btn-default" type="submit" value="Caribou Coffee">Caribou Coffee</button>
<button class="btn btn-default" type="submit" value="Other">Other</button>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script src="radar-chart.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<script src="d3-legend.js"></script>
<script>
// Set default values
RadarChart.defaultConfig.color = function() {};
RadarChart.defaultConfig.radius = 2;
RadarChart.defaultConfig.w = 400;
RadarChart.defaultConfig.h = 400;
RadarChart.defaultConfig.maxValue = 3;
RadarChart.defaultConfig.axisText = true;
RadarChart.defaultConfig.TickLength = 20;
RadarChart.defaultConfig.levels = 3;
RadarChart.defaultConfig.transitionDuration = 500;
</script>
<script>
// Initilize the chart and svg
var chart = RadarChart.chart();
var cfg = chart.config();
var svg = d3.select('#chart')
.attr('width', cfg.w)
.attr('height', cfg.h + cfg.h / 4);
// READ IN DATA
d3.csv("emotions.csv", format, function(error, csv){
var data = [];
// Create the new dataset
csv.forEach(function(currentValue, index){
data.push(
{"className": currentValue["nameAdj"],
"axes": [
{"axis": "anger", "value": currentValue["anger"]},
{"axis": "disgust", "value": currentValue["disgust"]},
{"axis": "sadness", "value": currentValue["sadness"]},
{"axis": "fear", "value": currentValue["fear"]},
{"axis": "surprise", "value": currentValue["surprise"]},
{"axis": "joy", "value": currentValue["joy"]},
{"axis": "anticipation", "value": currentValue["anticipation"]},
{"axis": "trust", "value": currentValue["trust"]}
]
})
});
// Create the initial chart
var subset = data.filter(function(el) { return el.className == "all" });
svg.append('g').classed('chart', 1).datum(subset).call(chart);
// Bind the event listener to the button
d3.selectAll("button").on("click", function() {
change(data, this.value);
});
});
// Create the update function
var change = function(data, value) {
subset = data.filter(function(el) { return el.className == value });
svg.selectAll('g.chart').datum(subset).call(chart)
}
// Creat the formatting function
function format(d){
d.anger = +d.anger
d.sadness = +d.sadness
d.fear = +d.fear
d.disgust = +d.disgust
d.trust = +d.trust
d.joy = +d.joy
d.surprise = +d.surprise
d.anticipation = +d.anticipation
return d
}
</script>
.radar-chart .level {
stroke: grey;
stroke-width: 0.5;
}
.radar-chart .axis line {
stroke: grey;
stroke-width: 1;
}
.radar-chart .axis .legend {
font-family: sans-serif;
font-size: 10px;
}
.radar-chart .axis .legend.top {
dy:1em;
}
.radar-chart .axis .legend.left {
text-anchor: start;
}
.radar-chart .axis .legend.middle {
text-anchor: middle;
}
.radar-chart .axis .legend.right {
text-anchor: end;
}
.radar-chart .tooltip {
font-family: sans-serif;
font-size: 13px;
transition: opacity 200ms;
opacity: 0;
}
.radar-chart .tooltip.visible {
opacity: 1;
}
/* area transition when hovering */
.radar-chart .area {
stroke-width: 2;
fill-opacity: 0.5;
}
.radar-chart.focus .area {
fill-opacity: 0.1;
}
.radar-chart.focus .area.focused {
fill-opacity: 0.7;
}
.radar-chart .circle {
fill-opacity: 0.9;
}
/* transitions */
.radar-chart .area, .radar-chart .circle {
transition: opacity 300ms, fill-opacity 200ms;
opacity: 1;
}
.radar-chart .d3-enter, .radar-chart .d3-exit {
opacity: 0;
}
var RadarChart = {
defaultConfig: {
containerClass: 'radar-chart',
w: 600,
h: 600,
factor: 0.95,
factorLegend: 1,
levels: 3,
levelTick: false,
TickLength: 10,
maxValue: 0,
minValue: 0,
radians: 2 * Math.PI,
color: d3.scale.category10(),
axisLine: true,
axisText: true,
circles: true,
radius: 5,
backgroundTooltipColor: "#555",
backgroundTooltipOpacity: "0.7",
tooltipColor: "white",
axisJoin: function(d, i) {
return d.className || i;
},
tooltipFormatValue: function(d) {
return d;
},
tooltipFormatClass: function(d) {
return d;
},
transitionDuration: 300
},
chart: function() {
// default config
var cfg = Object.create(RadarChart.defaultConfig);
function setTooltip(tooltip, msg){
if(msg == false || msg == undefined){
tooltip.classed("visible", 0);
tooltip.select("rect").classed("visible", 0);
}else{
tooltip.classed("visible", 1);
var container = tooltip.node().parentNode;
var coords = d3.mouse(container);
tooltip.select("text").classed('visible', 1).style("fill", cfg.tooltipColor);
var padding=5;
var bbox = tooltip.select("text").text(msg).node().getBBox();
tooltip.select("rect")
.classed('visible', 1).attr("x", 0)
.attr("x", bbox.x - padding)
.attr("y", bbox.y - padding)
.attr("width", bbox.width + (padding*2))
.attr("height", bbox.height + (padding*2))
.attr("rx","5").attr("ry","5")
.style("fill", cfg.backgroundTooltipColor).style("opacity", cfg.backgroundTooltipOpacity);
tooltip.attr("transform", "translate(" + (coords[0]+10) + "," + (coords[1]-10) + ")")
}
}
function radar(selection) {
selection.each(function(data) {
var container = d3.select(this);
var tooltip = container.selectAll('g.tooltip').data([data[0]]);
var tt = tooltip.enter()
.append('g')
.classed('tooltip', true)
tt.append('rect').classed("tooltip", true);
tt.append('text').classed("tooltip", true);
// allow simple notation
data = data.map(function(datum) {
if(datum instanceof Array) {
datum = {axes: datum};
}
return datum;
});
var maxValue = Math.max(cfg.maxValue, d3.max(data, function(d) {
return d3.max(d.axes, function(o){ return o.value; });
}));
maxValue -= cfg.minValue;
var allAxis = data[0].axes.map(function(i, j){ return {name: i.axis, xOffset: (i.xOffset)?i.xOffset:0, yOffset: (i.yOffset)?i.yOffset:0}; });
var total = allAxis.length;
var radius = cfg.factor * Math.min(cfg.w / 2, cfg.h / 2);
var radius2 = Math.min(cfg.w / 2, cfg.h / 2);
container.classed(cfg.containerClass, 1);
function getPosition(i, range, factor, func){
factor = typeof factor !== 'undefined' ? factor : 1;
return range * (1 - factor * func(i * cfg.radians / total));
}
function getHorizontalPosition(i, range, factor){
return getPosition(i, range, factor, Math.sin);
}
function getVerticalPosition(i, range, factor){
return getPosition(i, range, factor, Math.cos);
}
// levels && axises
var levelFactors = d3.range(0, cfg.levels).map(function(level) {
return radius * ((level + 1) / cfg.levels);
});
var levelGroups = container.selectAll('g.level-group').data(levelFactors);
levelGroups.enter().append('g');
levelGroups.exit().remove();
levelGroups.attr('class', function(d, i) {
return 'level-group level-group-' + i;
});
var levelLine = levelGroups.selectAll('.level').data(function(levelFactor) {
return d3.range(0, total).map(function() { return levelFactor; });
});
levelLine.enter().append('line');
levelLine.exit().remove();
if (cfg.levelTick){
levelLine
.attr('class', 'level')
.attr('x1', function(levelFactor, i){
if (radius == levelFactor) {
return getHorizontalPosition(i, levelFactor);
} else {
return getHorizontalPosition(i, levelFactor) + (cfg.TickLength / 2) * Math.cos(i * cfg.radians / total);
}
})
.attr('y1', function(levelFactor, i){
if (radius == levelFactor) {
return getVerticalPosition(i, levelFactor);
} else {
return getVerticalPosition(i, levelFactor) - (cfg.TickLength / 2) * Math.sin(i * cfg.radians / total);
}
})
.attr('x2', function(levelFactor, i){
if (radius == levelFactor) {
return getHorizontalPosition(i+1, levelFactor);
} else {
return getHorizontalPosition(i, levelFactor) - (cfg.TickLength / 2) * Math.cos(i * cfg.radians / total);
}
})
.attr('y2', function(levelFactor, i){
if (radius == levelFactor) {
return getVerticalPosition(i+1, levelFactor);
} else {
return getVerticalPosition(i, levelFactor) + (cfg.TickLength / 2) * Math.sin(i * cfg.radians / total);
}
})
.attr('transform', function(levelFactor) {
return 'translate(' + (cfg.w/2-levelFactor) + ', ' + (cfg.h/2-levelFactor) + ')';
});
}
else{
levelLine
.attr('class', 'level')
.attr('x1', function(levelFactor, i){ return getHorizontalPosition(i, levelFactor); })
.attr('y1', function(levelFactor, i){ return getVerticalPosition(i, levelFactor); })
.attr('x2', function(levelFactor, i){ return getHorizontalPosition(i+1, levelFactor); })
.attr('y2', function(levelFactor, i){ return getVerticalPosition(i+1, levelFactor); })
.attr('transform', function(levelFactor) {
return 'translate(' + (cfg.w/2-levelFactor) + ', ' + (cfg.h/2-levelFactor) + ')';
});
}
if(cfg.axisLine || cfg.axisText) {
var axis = container.selectAll('.axis').data(allAxis);
var newAxis = axis.enter().append('g');
if(cfg.axisLine) {
newAxis.append('line');
}
if(cfg.axisText) {
newAxis.append('text');
}
axis.exit().remove();
axis.attr('class', 'axis');
if(cfg.axisLine) {
axis.select('line')
.attr('x1', cfg.w/2)
.attr('y1', cfg.h/2)
.attr('x2', function(d, i) { return (cfg.w/2-radius2)+getHorizontalPosition(i, radius2, cfg.factor); })
.attr('y2', function(d, i) { return (cfg.h/2-radius2)+getVerticalPosition(i, radius2, cfg.factor); });
}
if(cfg.axisText) {
axis.select('text')
.attr('class', function(d, i){
var p = getHorizontalPosition(i, 0.5);
return 'legend ' +
((p < 0.4) ? 'left' : ((p > 0.6) ? 'right' : 'middle'));
})
.attr('dy', function(d, i) {
var p = getVerticalPosition(i, 0.5);
return ((p < 0.1) ? '1em' : ((p > 0.9) ? '0' : '0.5em'));
})
.text(function(d) { return d.name; })
.attr('x', function(d, i){ return d.xOffset+ (cfg.w/2-radius2)+getHorizontalPosition(i, radius2, cfg.factorLegend); })
.attr('y', function(d, i){ return d.yOffset+ (cfg.h/2-radius2)+getVerticalPosition(i, radius2, cfg.factorLegend); });
}
}
// content
data.forEach(function(d){
d.axes.forEach(function(axis, i) {
axis.x = (cfg.w/2-radius2)+getHorizontalPosition(i, radius2, (parseFloat(Math.max(axis.value - cfg.minValue, 0))/maxValue)*cfg.factor);
axis.y = (cfg.h/2-radius2)+getVerticalPosition(i, radius2, (parseFloat(Math.max(axis.value - cfg.minValue, 0))/maxValue)*cfg.factor);
});
});
var polygon = container.selectAll(".area").data(data, cfg.axisJoin);
polygon.enter().append('polygon')
.classed({area: 1, 'd3-enter': 1})
.on('mouseover', function (dd){
d3.event.stopPropagation();
container.classed('focus', 1);
d3.select(this).classed('focused', 1);
setTooltip(tooltip, cfg.tooltipFormatClass(dd.className));
})
.on('mouseout', function(){
d3.event.stopPropagation();
container.classed('focus', 0);
d3.select(this).classed('focused', 0);
setTooltip(tooltip, false);
});
polygon.exit()
.classed('d3-exit', 1) // trigger css transition
.transition().duration(cfg.transitionDuration)
.remove();
polygon
.each(function(d, i) {
var classed = {'d3-exit': 0}; // if exiting element is being reused
classed['radar-chart-serie' + i] = 1;
if(d.className) {
classed[d.className] = 1;
}
d3.select(this).classed(classed);
})
// styles should only be transitioned with css
.style('stroke', function(d, i) { return cfg.color(i); })
.style('fill', function(d, i) { return cfg.color(i); })
.transition().duration(cfg.transitionDuration)
// svg attrs with js
.attr('points',function(d) {
return d.axes.map(function(p) {
return [p.x, p.y].join(',');
}).join(' ');
})
.each('start', function() {
d3.select(this).classed('d3-enter', 0); // trigger css transition
});
if(cfg.circles && cfg.radius) {
var circleGroups = container.selectAll('g.circle-group').data(data, cfg.axisJoin);
circleGroups.enter().append('g').classed({'circle-group': 1, 'd3-enter': 1});
circleGroups.exit()
.classed('d3-exit', 1) // trigger css transition
.transition().duration(cfg.transitionDuration).remove();
circleGroups
.each(function(d) {
var classed = {'d3-exit': 0}; // if exiting element is being reused
if(d.className) {
classed[d.className] = 1;
}
d3.select(this).classed(classed);
})
.transition().duration(cfg.transitionDuration)
.each('start', function() {
d3.select(this).classed('d3-enter', 0); // trigger css transition
});
var circle = circleGroups.selectAll('.circle').data(function(datum, i) {
return datum.axes.map(function(d) { return [d, i]; });
});
circle.enter().append('circle')
.classed({circle: 1, 'd3-enter': 1})
.on('mouseover', function(dd){
d3.event.stopPropagation();
setTooltip(tooltip, cfg.tooltipFormatValue(dd[0].value));
//container.classed('focus', 1);
//container.select('.area.radar-chart-serie'+dd[1]).classed('focused', 1);
})
.on('mouseout', function(dd){
d3.event.stopPropagation();
setTooltip(tooltip, false);
container.classed('focus', 0);
//container.select('.area.radar-chart-serie'+dd[1]).classed('focused', 0);
//No idea why previous line breaks tooltip hovering area after hoverin point.
});
circle.exit()
.classed('d3-exit', 1) // trigger css transition
.transition().duration(cfg.transitionDuration).remove();
circle
.each(function(d) {
var classed = {'d3-exit': 0}; // if exit element reused
classed['radar-chart-serie'+d[1]] = 1;
d3.select(this).classed(classed);
})
// styles should only be transitioned with css
.style('fill', function(d) { return cfg.color(d[1]); })
.transition().duration(cfg.transitionDuration)
// svg attrs with js
.attr('r', cfg.radius)
.attr('cx', function(d) {
return d[0].x;
})
.attr('cy', function(d) {
return d[0].y;
})
.each('start', function() {
d3.select(this).classed('d3-enter', 0); // trigger css transition
});
//Make sure layer order is correct
var poly_node = polygon.node();
poly_node.parentNode.appendChild(poly_node);
var cg_node = circleGroups.node();
cg_node.parentNode.appendChild(cg_node);
// ensure tooltip is upmost layer
var tooltipEl = tooltip.node();
tooltipEl.parentNode.appendChild(tooltipEl);
}
});
}
radar.config = function(value) {
if(!arguments.length) {
return cfg;
}
if(arguments.length > 1) {
cfg[arguments[0]] = arguments[1];
}
else {
d3.entries(value || {}).forEach(function(option) {
cfg[option.key] = option.value;
});
}
return radar;
};
return radar;
},
draw: function(id, d, options) {
var chart = RadarChart.chart().config(options);
var cfg = chart.config();
d3.select(id).select('svg').remove();
d3.select(id)
.append("svg")
.attr("width", cfg.w)
.attr("height", cfg.h)
.datum(d)
.call(chart);
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment