Skip to content

Instantly share code, notes, and snippets.

@TennisVisuals
Last active March 21, 2018 19:49
Show Gist options
  • Save TennisVisuals/c591445c3e6773c6eb6f to your computer and use it in GitHub Desktop.
Save TennisVisuals/c591445c3e6773c6eb6f to your computer and use it in GitHub Desktop.
reusable updating radarChart

reusable updating radarChart

Design based on original by NadiehBremer at Visual Cinnamon

Coding philosophy inspired by Rob Moore

I figured out enter/exit/update for nested objects by studying Lee Mendelowitz's block and reviewing Mike's comments

Features:

  • Separate layer for hover objects which need to remain on top of chart
  • Separate layer for tooltip which needs to be uppermost layer
  • Events are configurable as options such that external functions can be seamlessly integrated
  • e.g. external tooltips can be configured for mouseover events

Accessors:

by default accessors with no parameters return current values

  • chart.duration(transition-time) // duration of transitions
  • chart.options(object) // many options don't have accessors
  • chart.width(width)
  • chart.height(height)
  • chart.margins(object)
  • chart.data(data) // set data
  • chart.update() // update chart
  • chart.maxValue(value) // maxValue of axis values
  • chart.levels(value) // number of ring levels
  • chart.opacity(value) // opacity of unselected areas
  • chart.borderWidth(value) // width of border surrounding areas
  • chart.rounded(true/false) // select rounded areas
  • chart.events(object) -- chart.events({'legend': {'mouseover': null}})
  • chart.color(d3.scale)
  • chart.colors(object) -- chart.colors({'axis1': 'color1', 'axis2': 'color2'})
operations on data held in chart instance
  • chart.pop()
  • chart.push(row) -or- chart.push([row, row])
  • chart.shift()
  • chart.unshift(row) -or- chart.unshift([row, row])
  • chart.slice(begin, end)
operations performed on data prior to display
  • chart.invert(axis) -or- chart.invert([axis, axis]) // index or "axis label"
  • chart.ranges({'axis': [min, max]})
  • chart.filterAxes('axis') -or- chart.filterAxes(['axis', 'axis'])
  • chart.filterAreas('key') -or- chart.filterAreas(['key1', 'key2'])
convenience
  • chart.keys() // return a list of data keys
  • chart.axes() // return labels of all axes
  • chart.nodes() // return svg object
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>D3.js Updating Radar Chart</title>
<!-- Google fonts -->
<link href='http://fonts.googleapis.com/css?family=Open+Sans:400,300' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Raleway' rel='stylesheet' type='text/css'>
<!-- D3.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.min.js" charset="utf-8"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/1.3.0/d3-legend.js" charset="utf-8"></script>
<style>
body {
font-family: 'Open Sans', sans-serif;
font-size: 11px;
font-weight: 300;
fill: #242424;
text-align: center;
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, -1px 0 0 #fff, 0 -1px 0 #fff;
cursor: default;
}
.legend {
font-family: 'Raleway', sans-serif;
fill: #333333;
}
.tooltip {
fill: #333333;
}
</style>
</head>
<body>
<div id="radarChart"></div>
<script src="radarChart.js"></script>
<script>
var color = d3.scale.ordinal()
.range(["#EDC951","#CC333F","#00A0B0"]);
var radarChartOptions = {
width: 500,
height: 500,
color: color
};
radarChart = RadarChart()
d3.select('#radarChart')
.call(radarChart);
radarChart.options(radarChartOptions).update();
</script>
<script src="radarDemo.js"></script>
</body>
</html>
function RadarChart() {
// TODO:
// wrapWidth should probably be calculated rather than an option
// slider to change maxValue on the fly
// filter update make sure there is an element with URL
//
// options which should be accessible via ACCESSORS
var data = [];
var _data = [];
var options = {
filter: 'glow', // define your own filter; false = no filter;
width: window.innerWidth,
height: window.innerHeight,
// Margins for the SVG
margins: {
top: 100,
right: 100,
bottom: 100,
left: 100
},
circles: {
levels: 8,
maxValue: 0,
labelFactor: 1.25,
opacity: 0.1,
fill: "#CDCDCD",
color: "#CDCDCD"
},
areas: {
colors: {}, // color lookup by key
opacity: 0.35,
borderWidth: 2,
rounded: true,
dotRadius: 4,
sort: true, // sort layers by approximation of size, smallest on top
filter: []
},
axes: {
lineColor: "white",
lineWidth: "2px",
wrapWidth: 60, // The number of pixels after which a label needs to be given a new line
filter: [],
invert: [],
ranges: {"Large Screen": [0, 1]} // { axisname: [min, max], axisname: [min, max] }
},
legend: {
display: false,
symbol: 'cross', // 'circle', 'cross', 'diamond', 'triangle-up', 'triangle-down'
toggle: 'circle',
position: { x: 25, y: 25 }
},
color: d3.scale.category10() //Color function
}
// nodes layered such that radarInvisibleCircles always on top of radarAreas
// and tooltip layer is at topmost layer
var chart_node; // parent node for this instance of radarChart
var hover_node; // parent node for invisibleRadarCircles
var tooltip_node; // parent node for tooltip, to keep on top
var legend_node; // parent node for tooltip, to keep on top
// DEFINABLE EVENTS
// Define with ACCESSOR function chart.events()
var events = {
'update': { 'begin': null, 'end': null },
'gridCircle': { 'mouseover': null, 'mouseout': null, 'mouseclick': null },
'axisLabel': { 'mouseover': null, 'mouseout': null, 'mouseclick': null },
'line': { 'mouseover': null, 'mouseout': null, 'mouseclick': null },
'legend': { 'mouseover': legendMouseover, 'mouseout': areaMouseout, 'mouseclick': legendClick },
'axis_legend': { 'mouseover': null, 'mouseout': null, 'mouseclick': null },
'radarArea': { 'mouseover': areaMouseover, 'mouseout': areaMouseout, 'mouseclick': null },
'radarInvisibleCircle': { 'mouseover': tooltip_show, 'mouseout': tooltip_hide, 'mouseclick': null }
};
// functions which should be accessible via ACCESSORS
var updateData;
// helper functions
var tooltip;
// programmatic
var radial_calcs = {};
var Format = d3.format('%'); // Percentage formatting
var transition_time = 0;
var delay = 0;
var keys;
var keyScale;
var colorScale;
function chart(selection) {
selection.each(function () {
dataCalcs();
radialCalcs();
var dom = d3.select(this);
//////////// Create the container SVG and children g /////////////
var svg = dom.append('svg')
.attr('class', 'svg-class')
.attr('width', options.width)
.attr('height', options.height);
// append parent g for chart
chart_node = svg.append('g').attr('class', 'radar_node');
hover_node = svg.append('g').attr('class', 'hover_node');
tooltip_node = svg.append('g').attr('class', 'tooltip_node');
legend_node = svg.append("g").attr("class", "legendOrdinal");
// Wrapper for the grid & axes
var axisGrid = chart_node.append("g").attr("class", "axisWrapper");
////////// Glow filter for some extra pizzazz ///////////
var filter = chart_node.append('defs').append('filter').attr('id','glow'),
feGaussianBlur = filter.append('feGaussianBlur').attr('stdDeviation','2.5').attr('result','coloredBlur'),
feMerge = filter.append('feMerge'),
feMergeNode_1 = feMerge.append('feMergeNode').attr('in','coloredBlur'),
feMergeNode_2 = feMerge.append('feMergeNode').attr('in','SourceGraphic');
// Set up the small tooltip for when you hover over a circle
tooltip = tooltip_node.append("text")
.attr("class", "tooltip")
.style("opacity", 0);
// update
updateData = function() {
var duration = transition_time;
dataCalcs();
radialCalcs();
keys = _data.map(function(m) { return m.key; });
keyScale = d3.scale.ordinal()
.domain(_data.map(function(m) { return m._i; }))
.range(_data.map(function(m) { return m.key; }));
colorScale = d3.scale.ordinal()
.domain(_data.map(function(m) {
return options.areas.colors[keyScale(m._i)] ?
keyScale(m._i)
: m._i.toString();
}))
.range(_data.map(function(m) { return setColor(m); }));
svg.transition().delay(delay).duration(duration)
.attr('width', options.width)
.attr('height', options.height)
chart_node.transition().delay(delay).duration(duration)
.attr('width', options.width)
.attr('height', options.height)
.attr("transform",
"translate(" + ((options.width - (options.margins.left + options.margins.right)) / 2 + options.margins.left) + ","
+ ((options.height - (options.margins.top + options.margins.bottom)) / 2 + options.margins.top) + ")")
hover_node.transition().delay(delay).duration(duration)
.attr('width', options.width)
.attr('height', options.height)
.attr("transform",
"translate(" + ((options.width - (options.margins.left + options.margins.right)) / 2 + options.margins.left) + ","
+ ((options.height - (options.margins.top + options.margins.bottom)) / 2 + options.margins.top) + ")")
tooltip_node.transition().delay(delay).duration(duration)
.attr('width', options.width)
.attr('height', options.height)
.attr("transform",
"translate(" + ((options.width - (options.margins.left + options.margins.right)) / 2 + options.margins.left) + ","
+ ((options.height - (options.margins.top + options.margins.bottom)) / 2 + options.margins.top) + ")")
legend_node
.attr("transform", "translate(" + options.legend.position.x + "," + options.legend.position.y + ")");
var update_gridCircles = axisGrid.selectAll(".gridCircle")
.data(d3.range(1, (options.circles.levels + 1)).reverse())
update_gridCircles
.transition().duration(duration)
.attr("r", function(d, i) { return radial_calcs.radius / options.circles.levels * d; })
.style("fill", options.circles.fill)
.style("fill-opacity", options.circles.opacity)
.style("stroke", options.circles.color)
.style("filter" , function() { if (options.filter) return "url(#" + options.filter + ")" });
update_gridCircles.enter()
.append("circle")
.attr("class", "gridCircle")
.attr("r", function(d, i) { return radial_calcs.radius / options.circles.levels * d; })
.on('mouseover', function(d, i) { if (events.gridCircle.mouseover) events.gridCircle.mouseover(d, i); })
.on('mouseout', function(d, i) { if (events.gridCircle.mouseout) events.gridCircle.mouseout(d, i); })
.style("fill", options.circles.fill)
.style("fill-opacity", options.circles.opacity)
.style("stroke", options.circles.color)
.style("filter" , function() { if (options.filter) return "url(#" + options.filter + ")" });
update_gridCircles.exit()
.transition().duration(duration * .5)
.delay(function(d, i) { return 0; })
.remove();
var update_axisLabels = axisGrid.selectAll(".axisLabel")
.data(d3.range(1, (options.circles.levels + 1)).reverse())
update_axisLabels
.transition().duration(duration / 2)
.style('opacity', 1) // don't change to 0 if there has been no change in dimensions! possible??
.transition().duration(duration / 2)
.text(function(d, i) { if (radial_calcs.maxValue) return Format(radial_calcs.maxValue * d / options.circles.levels); })
.attr("y", function(d) { return -d * radial_calcs.radius / options.circles.levels; })
.style('opacity', 1)
update_axisLabels.enter()
.append("text")
.attr("class", "axisLabel")
.attr("x", 4)
.attr("y", function(d) { return -d * radial_calcs.radius / options.circles.levels; })
.attr("dy", "0.4em")
.style("font-size", "10px")
.attr("fill", "#737373")
.on('mouseover', function(d, i) { if (events.axisLabel.mouseover) events.axisLabel.mouseover(d, i); })
.on('mouseout', function(d, i) { if (events.axisLabel.mouseout) events.axisLabel.mouseout(d, i); })
.text(function(d, i) { if (radial_calcs.maxValue) return Format(radial_calcs.maxValue * d / options.circles.levels); });
update_axisLabels.exit()
.transition().duration(duration * .5)
.remove();
var update_axes = axisGrid.selectAll(".axis")
.data(radial_calcs.axes, get_axis)
update_axes
.enter().append("g")
.attr("class", "axis")
.attr("key", function(d) { return d.axis; });
update_axes.exit()
.transition().duration(duration)
.style('opacity', 0)
.remove()
var update_lines = update_axes.selectAll(".line")
.data(function(d) { return [d]; }, get_axis)
update_lines.enter()
.append("line")
.attr("class", "line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", function(d, i, j) { return calcX(null, 1.1, j); })
.attr("y2", function(d, i, j) { return calcY(null, 1.1, j); })
.on('mouseover', function(d, i, j) { if (events.line.mouseover) events.line.mouseover(d, j); })
.on('mouseout', function(d, i, j) { if (events.line.mouseout) events.line.mouseout(d, j); })
.style("stroke", options.axes.lineColor)
.style("stroke-width", "2px")
update_lines.exit()
.transition().duration(duration * .5)
.delay(function(d, i) { return 0; })
.remove();
update_lines
.transition().duration(duration)
.style("stroke", options.axes.lineColor)
.style("stroke-width", options.axes.lineWidth)
.attr("x2", function(d, i, j) { return calcX(null, 1.1, j); })
.attr("y2", function(d, i, j) { return calcY(null, 1.1, j); })
var update_axis_legends = update_axes.selectAll(".axis_legend")
.data(function(d) { return [d]; }, get_axis)
update_axis_legends.enter()
.append("text")
.attr("class", "axis_legend")
.style("font-size", "11px")
.attr("text-anchor", "middle")
.attr("dy", "0.35em")
.attr("x", function(d, i, j) { return calcX(null, options.circles.labelFactor, j); })
.attr("y", function(d, i, j) { return calcY(null, options.circles.labelFactor, j); })
.on('mouseover', function(d, i, j) { if (events.axis_legend.mouseover) events.axis_legend.mouseover(d, i, j); })
.on('mouseout', function(d, i, j) { if (events.axis_legend.mouseout) events.axis_legend.mouseout(d, i, j); })
.call(wrap, options.axes.wrapWidth)
update_axis_legends.exit()
.transition().duration(duration * .5)
.delay(function(d, i) { return 0; })
.remove();
update_axis_legends
.transition().duration(duration)
.attr("x", function(d, i, j) { return calcX(null, options.circles.labelFactor, j); })
.attr("y", function(d, i, j) { return calcY(null, options.circles.labelFactor, j); })
.selectAll('tspan')
.attr("x", function(d, i, j) { return calcX(null, options.circles.labelFactor, j); })
.attr("y", function(d, i, j) { return calcY(null, options.circles.labelFactor, j); })
var radarLine = d3.svg.line.radial()
.interpolate( options.areas.rounded ?
"cardinal-closed" :
"linear-closed" )
.radius(function(d) { return radial_calcs.rScale(d.value); })
.angle(function(d,i) { return i * radial_calcs.angleSlice; });
var update_blobWrapper = chart_node.selectAll(".radarWrapper")
.data(_data, get_key)
update_blobWrapper.enter()
.append("g")
.attr("class", "radarWrapper")
.attr("key", function(d) { return d.key; });
update_blobWrapper.exit()
.transition().duration(duration)
.style('opacity', 0)
.remove()
var update_radarArea = update_blobWrapper.selectAll('.radarArea')
.data(function(d) { return [d]; }, get_key);
update_radarArea.enter()
.append("path")
.attr("class", function(d) { return "radarArea " + d.key.replace(/\s+/g, '') })
.attr("d", function(d, i) { return radarLine(d.values); })
.style("fill", function(d, i, j) { return setColor(d); })
.style("fill-opacity", 0)
.on('mouseover', function(d, i) { if (events.radarArea.mouseover) events.radarArea.mouseover(d, i, this); })
.on('mouseout', function(d, i) { if (events.radarArea.mouseout) events.radarArea.mouseout(d, i, this); })
update_radarArea.exit().remove()
update_radarArea
.transition().duration(duration)
.style("fill", function(d, i, j) { return setColor(d); })
.attr("d", function(d, i) { return radarLine(d.values); })
.style("fill-opacity", function(d, i) {
return options.areas.filter.indexOf(d.key) >= 0 ? 0 : options.areas.opacity;
})
var update_radarStroke = update_blobWrapper.selectAll('.radarStroke')
.data(function(d) { return [d]; }, get_key);
update_radarStroke.enter()
.append("path")
.attr("class", "radarStroke")
.attr("d", function(d, i) { return radarLine(d.values); })
.style("opacity", 0)
.style("stroke-width", options.areas.borderWidth + "px")
.style("stroke", function(d, i, j) { return setColor(d); })
.style("fill", "none")
.style("filter" , function() { if (options.filter) return "url(#" + options.filter + ")" });
update_radarStroke.exit().remove();
update_radarStroke
.transition().duration(duration)
.style("stroke", function(d, i, j) { return setColor(d); })
.attr("d", function(d, i) { return radarLine(d.values); })
.style("filter" , function() { if (options.filter) return "url(#" + options.filter + ")" })
.style("opacity", function(d, i) {
return options.areas.filter.indexOf(d.key) >= 0 ? 0 : 1;
});
update_radarCircle = update_blobWrapper.selectAll('.radarCircle')
.data(function(d, i) { return add_index(d._i, d.values); });
update_radarCircle.enter()
.append("circle")
.attr("class", "radarCircle")
.attr("r", options.areas.dotRadius)
.attr("cx", function(d, i){ return calcX(0, 0, i); })
.attr("cy", function(d, i){ return calcY(0, 0, i); })
.style("fill", function(d, i, j) { return setColor(d, d._i, _data[j].key); })
.style("fill-opacity", function(d, i) { return 0; })
.transition().duration(duration)
.attr("cx", function(d, i){ return calcX(d.value, 0, i); })
.attr("cy", function(d, i){ return calcY(d.value, 0, i); })
update_radarCircle.exit().remove();
update_radarCircle
.transition().duration(duration)
.style("fill", function(d, i, j) { return setColor(d, d._i, _data[j].key); })
.style("fill-opacity", function(d, i, j) {
var key = data.map(function(m) {return m.key})[j];
return options.areas.filter.indexOf(key) >= 0 ? 0 : 0.8;
})
.attr("r", options.areas.dotRadius)
.attr("cx", function(d, i){ return calcX(d.value, 0, i); })
.attr("cy", function(d, i){ return calcY(d.value, 0, i); })
var update_blobCircleWrapper = hover_node.selectAll(".radarCircleWrapper")
.data(_data, get_key)
update_blobCircleWrapper
.enter().append("g")
.attr("class", "radarCircleWrapper")
.attr("key", function(d) { return d.key; });
update_blobCircleWrapper.exit()
.transition().duration(duration)
.style('opacity', 0)
.remove()
update_radarInvisibleCircle = update_blobCircleWrapper.selectAll(".radarInvisibleCircle")
.data(function(d, i) { return add_index(d._i, d.values); });
update_radarInvisibleCircle.enter()
.append("circle")
.attr("class", "radarInvisibleCircle")
.attr("r", options.areas.dotRadius * 1.5)
.attr("cx", function(d, i){ return calcX(d.value, 0, i); })
.attr("cy", function(d, i){ return calcY(d.value, 0, i); })
.style("fill", "none")
.style("pointer-events", "all")
.on('mouseover', function(d, i) {
if (events.radarInvisibleCircle.mouseover) events.radarInvisibleCircle.mouseover(d, i, this);
})
.on("mouseout", function(d, i) {
if (events.radarInvisibleCircle.mouseout) events.radarInvisibleCircle.mouseout(d, i, this);
})
update_radarInvisibleCircle.exit().remove();
update_radarInvisibleCircle
.attr("cx", function(d, i){ return calcX(d.value, 0, i); })
.attr("cy", function(d, i){ return calcY(d.value, 0, i); })
if (options.legend.display) {
var shape = d3.svg.symbol().type(options.legend.symbol).size(150)();
var foo;
legend_node.selectAll('cell').remove();
var colorScale = d3.scale.ordinal()
.domain(_data.map(function(m) { return m._i; }))
.range(_data.map(function(m) { return setColor(m); }));
if (d3.legend) {
var legendOrdinal = d3.legend.color()
.shape("path", shape)
.shapePadding(10)
.scale(colorScale)
.labels(colorScale.domain().map(function(m) { return keyScale(m); } ))
.on("cellclick", function(d, i) {
if (events.legend.mouseclick) events.legend.mouseclick(d, i, this);
})
.on("cellover", function(d, i) {
if (events.legend.mouseover) events.legend.mouseover(d, i, this);
})
.on("cellout", function(d, i) {
if (events.legend.mouseout) events.legend.mouseout(d, i, this);
});
legend_node
.call(legendOrdinal);
}
}
}
});
}
// REUSABLE FUNCTIONS
// ------------------
// calculate average for sorting, add unique indices for color
// accounts for data updates and assigns unique colors when possible
function dataCalcs() {
// this deep copy method has limitations which should not be encountered
// in this context
_data = JSON.parse(JSON.stringify(data));
var axes = getAxisLabels(_data);
var ranges = {};
// filter out axes
var d_indices = axes.map(function(m, i) { return (options.axes.filter.indexOf(axes[i]) >= 0) ? i : undefined; }).reverse();
_data.forEach( function(e) {
d_indices.forEach(function(i) { if (i >= 0) e.values.splice(i, 1); });
});
// determine min/max range for each axis
_data.forEach( function(e) { e.values.forEach (function(d, i) {
var range = ranges[axes[i]] ? // already started?
ranges[axes[i]]
: options.axes.ranges[axes[i]] ? // rande defined in options?
options.axes.ranges[axes[i]].slice()
: [0, 1]; // default
var max = d.value > range[1] ? d.value : range[1];
var min = d.value < range[0] ? d.value : range[0];
ranges[axes[i]] = [min, max]; // update
}) });
// convert all axes to range [0,1] (procrustean)
_data.forEach( function(e) { e.values.forEach (function(d, i) {
if (ranges[axes[i]][0] != 0 && ranges[axes[i]][1] != 1) {
var range = ranges[axes[i]];
d.original_value = Number(d.value);
d.value = (d.value - range[0]) / (range[1] - range[0]);
}
if (options.axes.invert.indexOf(axes[i]) >= 0) { d.value = 1 - d.value; }
}) })
_data.forEach( function(d) { d['_avg'] = d3.mean(d.values, function(e){ return e.value }); })
_data = options.areas.sort ?
_data.sort( function(a, b) {
var a = a['_avg'];
var b = b['_avg'];
return b - a;
})
: _data;
var color_indices = (function(a,b){while(a--)b[a]=a;return b})(10,[]);
var indices = _data.map(function (i) { return i._i });
var unassigned = color_indices.filter(function(x) { return indices.indexOf(x) < 0; }).reverse();
_data = _data.map(function(d, i) {
if (d['_i'] >= 0) {
return d;
} else {
d['_i'] = unassigned.length ? unassigned.pop() : i;
return d;
}
});
}
function getAxisLabels(dataArray) {
return dataArray.length ?
dataArray[0].values.map(function(i, j) { return i.axis;})
: [];
}
function radialCalcs() {
var axes = _data.length ?
_data[0].values.map(function(i, j) { return i;})
: [];
var axisLabels = getAxisLabels(_data);
radial_calcs = {
// Radius of the outermost circle
radius: Math.min((options.width - (options.margins.left + options.margins.right)) / 2,
(options.height - (options.margins.bottom + options.margins.top)) /2),
axes: axes,
axisLabels: axisLabels,
// If the supplied maxValue is smaller than the actual one, replace by the max in the data
maxValue: Math.max(options.circles.maxValue, d3.max(_data, function(i) {
return d3.max(i.values.map( function(o) { return o.value; }))
}))
}
radial_calcs.total = radial_calcs.axes.length;
// The width in radians of each "slice"
radial_calcs.angleSlice = radial_calcs.total > 0 ?
Math.PI * 2 / radial_calcs.total
: 1;
//Scale for the radius
radial_calcs.rScale = d3.scale.linear()
.range([0, radial_calcs.radius])
.domain([0, radial_calcs.maxValue])
}
function modifyList(list, values, valid_list) {
if ( values.constructor === Array ) {
values.forEach(function(e) { checkType(e); });
} else if (typeof values != "object") {
checkType(values);
} else {
return chart;
}
function checkType(v) {
if (!isNaN(v) && (function(x) { return (x | 0) === x; })(parseFloat(v))) {
checkValue(parseInt(v));
} else if (typeof v == "string") {
checkValue(v);
}
}
function checkValue(val) {
if ( valid_list.indexOf(val) >= 0 ) {
modify(val);
} else if ( val >= 0 && val < valid_list.length ) {
modify(valid_list[val]);
}
}
function modify(index) {
if (list.indexOf(index) >= 0) {
remove(list, index);
} else {
list.push(index);
}
}
function remove(arr, item) {
for (var i = arr.length; i--;) { if (arr[i] === item) { arr.splice(i, 1); } }
}
}
function calcX(value, scale, index) {
return radial_calcs.rScale(value ?
value
: radial_calcs.maxValue * scale) * Math.cos(radial_calcs.angleSlice * index - Math.PI/2);
}
function calcY(value, scale, index) {
return radial_calcs.rScale(value ?
value
: radial_calcs.maxValue * scale) * Math.sin(radial_calcs.angleSlice * index - Math.PI/2);
}
function setColor(d, index, key) {
index = index ? index : d._i;
key = key ? key : d.key;
return options.areas.colors[key] ? options.areas.colors[key] : options.color(index);
}
// END REUSABLE FUNCTIONS
// ACCESSORS
// ---------
chart.nodes = function() {
return { svg: svg, chart: chart_node, hover: hover_node, tooltip: tooltip_node, legend: legend_node };
}
chart.events = function(functions) {
if (!arguments.length) return events;
var fKeys = Object.keys(functions);
var eKeys = Object.keys(events);
for (var k=0; k < fKeys.length; k++) {
if (eKeys.indexOf(fKeys[k]) >= 0) events[fKeys[k]] = functions[fKeys[k]];
}
return chart;
}
chart.width = function(value) {
if (!arguments.length) return options.width;
options.width = value;
return chart;
};
chart.height = function(value) {
if (!arguments.length) return options.height;
options.height = value;
return chart;
};
chart.duration = function(value) {
if (!arguments.length) return transition_time;
transition_time = value;
return chart;
}
chart.updateDimensions = function() {
if (typeof updateDimensions === 'function') updateDimensions(transition_time);
}
chart.update = function() {
if (events.update.begin) events.update.begin(_data);
if (typeof updateData === 'function') updateData();
setTimeout(function() {
if (events.update.end) events.update.end(_data);
}, transition_time);
}
chart.data = function(value) {
if (!arguments.length) return data;
data = value;
return chart;
};
chart.pop = function() {
return data.pop();
};
chart.push = function(row) {
if ( row && row.constructor === Array ) {
for (var i=0; i < row.length; i++) {
check_key(row[i]);
}
} else {
check_key(row);
}
function check_key(one_row) {
if (one_row.key && data.map(function(m) { return m.key }).indexOf(one_row.key) < 0) {
data.push(one_row);
}
}
return chart;
};
chart.shift = function() {
return data.shift();
};
chart.unshift = function(row) {
if ( row && row.constructor === Array ) {
for (var i=0; i < row.length; i++) {
check_key(row[i]);
}
} else {
check_key(row);
}
function check_key(one_row) {
if (one_row.key && data.map(function(m) { return m.key }).indexOf(one_row.key) < 0) {
data.unshift(one_row);
}
}
return chart;
};
chart.slice = function(begin, end) {
return data.slice(begin, end);
};
// allows updating individual options and suboptions
// while preserving state of other options
chart.options = function(values) {
if (!arguments.length) return options;
var vKeys = Object.keys(values);
var oKeys = Object.keys(options);
for (var k=0; k < vKeys.length; k++) {
if (oKeys.indexOf(vKeys[k]) >= 0) {
if (typeof(options[vKeys[k]]) == 'object') {
var sKeys = Object.keys(values[vKeys[k]]);
var osKeys = Object.keys(options[vKeys[k]]);
for (var sk=0; sk < sKeys.length; sk++) {
if (osKeys.indexOf(sKeys[sk]) >= 0) {
options[vKeys[k]][sKeys[sk]] = values[vKeys[k]][sKeys[sk]];
}
}
} else {
options[vKeys[k]] = values[vKeys[k]];
}
}
}
return chart;
}
chart.margins = function(value) {
if (!arguments.length) return options.margins;
var vKeys = Object.keys(values);
var mKeys = Object.keys(options.margins);
for (var k=0; k < vKeys.length; k++) {
if (mKeys.indexOf(vKeys[k]) >= 0) options.margins[vKeys[k]] = values[vKeys[k]];
}
return chart;
}
chart.levels = function(value) {
if (!arguments.length) return options.circles.levels;
options.circles.levels = value;
return chart;
}
chart.maxValue = function(value) {
if (!arguments.length) return options.circles.maxValue;
options.circles.maxValue = value;
return chart;
}
chart.opacity = function(value) {
if (!arguments.length) return options.areas.opacity;
options.areas.opacity = value;
return chart;
}
chart.borderWidth = function(value) {
if (!arguments.length) return options.areas.borderWidth;
options.areas.borderWidth = value;
return chart;
}
chart.rounded = function(value) {
if (!arguments.length) return options.areas.rounded;
options.areas.rounded = value;
return chart;
}
// range of colors to set color based on index
chart.color = function(value) {
if (!arguments.length) return options.color;
options.color = value;
return chart;
}
// colors set according to data keys
chart.colors = function(colores) {
if (!arguments.length) return options.areas.colors;
options.areas.colors = colores;
return chart;
}
chart.keys = function() {
return data.map(function(m) {return m.key});
}
chart.axes = function() {
return getAxisLabels(data);
}
// add or remove keys (or key indices) to filter axes
chart.filterAxes = function(values) {
if (!arguments.length) return options.axes.filter;
var axes = getAxisLabels(data);
modifyList(options.axes.filter, values, axes);
return chart;
}
// add or remove keys (or key indices) to filter areas
chart.filterAreas = function(values) {
if (!arguments.length) return options.areas.filter;
var keys = data.map(function(m) {return m.key});
modifyList(options.areas.filter, values, keys);
return chart;
}
// add or remove keys (or key indices) to invert
chart.invert = function(values) {
if (!arguments.length) return options.axes.invert;
var axes = getAxisLabels(data);
modifyList(options.axes.invert, values, axes);
return chart;
}
// add or remove ranges for keys
chart.ranges = function(values) {
if (!arguments.length) return options.axes.ranges;
if (typeof values == "string") return chart;
var axes = getAxisLabels(data);
if ( values && values.constructor === Array ) {
values.forEach(function(e) { checkRange(e); } );
} else {
checkRange(values);
}
function checkRange(range_declarations) {
var keys = Object.keys(range_declarations);
for (var k=0; k < keys.length; k++) {
if ( axes.indexOf(keys[k]) >= 0 // is valid axis
&& range_declarations[keys[k]] // range array not undefined
&& range_declarations[keys[k]].constructor === Array
&& checkValues(keys[k], range_declarations[keys[k]]) ) {
options.axes.ranges[keys[k]] = range_declarations[keys[k]];
}
}
}
function checkValues(key, range) {
if (range.length == 2 && !isNaN(range[0]) && !isNaN(range[1])) {
return true;
} else if (range.length == 0) {
delete options.axes.ranges[key];
}
return false;
}
return chart;
}
// END ACCESSORS
// DEFAULT EVENTS
// --------------
function areaMouseover(d, i, self) {
//Dim all blobs
d3.selectAll(".radarArea")
.transition().duration(200)
.style("fill-opacity", function(d, i, j) {
return options.areas.filter.indexOf(d.key) >= 0 ? 0 : 0.1;
});
//Bring back the hovered over blob
d3.select(self)
.transition().duration(200)
.style("fill-opacity", function(d, i, j) {
return options.areas.filter.indexOf(d.key) >= 0 ? 0 : 0.7;
});
}
function areaMouseout(d, i, self) {
//Bring back all blobs
d3.selectAll(".radarArea")
.transition().duration(200)
.style("fill-opacity", function(d, i, j) {
return options.areas.filter.indexOf(d.key) >= 0 ? 0 : options.areas.opacity;
});
}
// on mouseover for the legend symbol
function legendMouseover(d, i, self) {
var area = keys.indexOf(d) >= 0 ? d : keyScale(d);
//Dim all blobs
d3.selectAll(".radarArea")
.transition().duration(200)
.style("fill-opacity", function(d, i, j) {
return options.areas.filter.indexOf(d.key) >= 0 ? 0 : 0.1;
});
//Bring back the hovered over blob
d3.selectAll(".radarArea." + area.replace(/\s+/g, ''))
.transition().duration(200)
.style("fill-opacity", function(d, i, j) {
return options.areas.filter.indexOf(d.key) >= 0 ? 0 : 0.7;
});
}
function legendClick(d, i, self) {
var keys = data.map(function(m) {return m.key});
modifyList(options.areas.filter, keys[d], keys);
updateData();
var state = d3.select(self).select('path').attr('toggle');
if (state == 'true') {
var shape = d3.svg.symbol().type(options.legend.symbol).size(150)()
} else {
var shape = d3.svg.symbol().type(options.legend.toggle).size(150)()
}
d3.select(self).select('path')
.attr('toggle', state == 'true' ? 'false' : 'true' )
.attr('d', function(d, i) { return shape; });
}
function tooltip_show(d, i, self) {
var value = d.original_value ? d.original_value : Format(d.value);
newX = parseFloat(d3.select(self).attr('cx')) - 10;
newY = parseFloat(d3.select(self).attr('cy')) - 10;
tooltip
.attr('x', newX)
.attr('y', newY)
.text(value)
.transition().duration(200)
.style('opacity', 1);
}
function tooltip_hide() {
tooltip
.transition().duration(200)
.style("opacity", 0);
}
// Helper Functions
// ----------------
function add_index(key, values) {
for (var v=0; v<values.length; v++) {
values[v]['_i'] = key;
}
return values;
}
var get_key = function(d) { return d && d.key; };
var get_axis = function(d) { return d && d.axis; };
// Wraps SVG text
// modification of: http://bl.ocks.org/mbostock/7555321
function wrap(text, width) {
text.each(function(d, i, j) {
var text = d3.select(this);
var words = d.axis.split(/\s+/).reverse();
var word;
var line = [];
var lineNumber = 0;
var lineHeight = 1.4; // ems
var x = calcX(null, options.circles.labelFactor, j);
var y = calcY(null, options.circles.labelFactor, j);
var dy = parseFloat(text.attr("dy"));
var tspan = text.text(null).append("tspan").attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
return chart;
}
!function() {
var operation = d3.select('body').append('div').append('h2');
data =
[
{
"key":"Nokia Smartphone",
"values":[
{ "axis":"Battery Life", "value":0.26 }, { "axis":"Brand", "value":0.10 },
{ "axis":"Contract Cost", "value":0.30 }, { "axis":"Design And Quality", "value":0.14 },
{ "axis":"Have Internet Connectivity", "value":0.22 }, { "axis":"Large Screen", "value":0.04 },
{ "axis":"Price Of Device", "value":0.41 }, { "axis":"To Be A Smartphone", "value":0.30 }
]
},
{
"key":"Samsung",
"values":[
{ "axis":"Battery Life", "value":0.27 }, { "axis":"Brand", "value":0.16 },
{ "axis":"Contract Cost", "value":0.35 }, { "axis":"Design And Quality", "value":0.13 },
{ "axis":"Have Internet Connectivity", "value":0.20 }, { "axis":"Large Screen", "value":0.13 },
{ "axis":"Price Of Device", "value":0.35 }, { "axis":"To Be A Smartphone", "value":0.38 }
]
},
{
"key":"iPhone",
"values":[
{ "axis":"Battery Life", "value":0.22 }, { "axis":"Brand", "value":0.28 },
{ "axis":"Contract Cost", "value":0.29 }, { "axis":"Design And Quality", "value":0.17 },
{ "axis":"Have Internet Connectivity", "value":0.22 }, { "axis":"Large Screen", "value":0.02 },
{ "axis":"Price Of Device", "value":0.21 }, { "axis":"To Be A Smartphone", "value":0.50 }
]
}
];
setTimeout(function() {
operation.text(' radarChart.data(data).duration(1000).update(); ');
radarChart.data(data).duration(1000).update();
}, 200);
setTimeout(function() {
operation.html(" radarChart.options({'legend': {display: true}}); <br> radarChart.colors({'iPhone': 'blue', 'Samsung': 'red', 'Nokia Smartphone': 'yellow'}).update(); ");
radarChart.options({'legend': {display: true}});
radarChart.colors({'iPhone': 'blue', 'Samsung': 'red', 'Nokia Smartphone': 'yellow'}).update();
}, 4000);
setTimeout(function() {
operation.html(" radarChart.filterAxes(7); <br> radarChart.options({circles: {maxValue: 1, levels: 4}}).update(); ");
radarChart.filterAxes(7);
radarChart.options({circles: {maxValue: 1, levels: 4}}).update();
}, 8000);
setTimeout(function() {
operation.text(" radarChart.maxValue(.5).levels(7).update(); ");
radarChart.maxValue(.5).levels(7).update();
}, 12000);
setTimeout(function() {
operation.text(" radarChart.invert(4).update(); ");
radarChart.invert(4).update();
}, 16000);
setTimeout(function() {
operation.text(" radarChart.ranges({'Contract Cost': [-1, 2]}).update(); ");
radarChart.ranges({'Contract Cost': [-1, 2]}).update();
}, 20000);
setTimeout(function() {
operation.html(" data.forEach(function(e) { e.values.forEach(function(v) { v.value = (Math.random() * .6) + .2; }) })<br> radarChart.data(data).update(); ");
chart_data = JSON.parse(JSON.stringify(data));
chart_data.forEach(function(e) { e.values.forEach(function(v) { v.value = (Math.random() * .6) + .2; }) })
radarChart.data(chart_data).update();
}, 24000);
setTimeout(function() {
operation.html(" var one = radarChart.slice(1, 2); <br> radarChart.data(one).update(); ");
var one = radarChart.slice(1, 2);
radarChart.data(one).update();
}, 28000);
setTimeout(function() {
operation.html(" radarChart.ranges({'Contract Cost': []}).invert(4); <br> radarChart.data(data).update(); ");
radarChart.ranges({'Contract Cost': []}).invert(4);
radarChart.data(data).update();
}, 32000);
setTimeout(function() {
operation.html(" radarChart.options({circles: {fill: 'violet'}}); <br> radarChart.options({axes: {lineColor: 'lightyellow'}}); <br> radarChart.options({circles: {color: '#FF99CC'}}); <br> radarChart.colors({'iPhone': 'black', 'Samsung': 'green', 'Nokia Smartphone': 'purple'}); ");
radarChart.options({circles: {fill: 'violet', color: '#FF99CC'}});
radarChart.options({axes: {lineColor: "lightyellow"}});
radarChart.colors({'iPhone': 'black', 'Samsung': 'green', 'Nokia Smartphone': 'purple'});
radarChart.update();
}, 36000);
setTimeout(function() {
operation.text(" radarChart.options({circles: {maxValue: 1, levels: 3}, legend: {symbol: 'circle'}, filter: false}).update(); ");
radarChart.options({circles: {maxValue: 1, levels: 3}, legendSymbol: 'circle', filter: false}).update();
}, 40000);
setTimeout(function() {
operation.text(" radarChart.height(300).width(300).options({'areas': {'dotRadius': 2}}).update(); ");
radarChart.height(300).width(300).options({'areas': {'dotRadius': 2}}).update();
}, 44000);
setTimeout(function() {
operation.text(" radarChart.height(500).width(500).options({'areas': {'dotRadius': 4}}).update(); ");
radarChart.height(600).width(600).options({'areas': {'dotRadius': 4}}).update();
}, 48000);
setTimeout(function() {
operation.html(" radarChart.options({circles: {fill: '#CDCDCD', color: '#CDCDCD'}}); <br> radarChart.options({axes: {lineColor: 'white'}}); <br> radarChart.colors({}).data(data).update(); <br> radarChart.maxValue(.5).levels(7).filterAxes(7);");
radarChart.options({circles: {fill: '#CDCDCD', color: '#CDCDCD'}});
radarChart.options({axes: {lineColor: 'white'}, filter: 'glow'});
radarChart.maxValue(.5).levels(7).filterAxes(7);
radarChart.colors({}).data(data).update();
}, 52000);
}();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment