Skip to content

Instantly share code, notes, and snippets.

@Kuerzibe
Last active March 15, 2019 09:55
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 Kuerzibe/b2c717e17de660f712107f2a37dee6ba to your computer and use it in GitHub Desktop.
Save Kuerzibe/b2c717e17de660f712107f2a37dee6ba to your computer and use it in GitHub Desktop.
reusable updating radarChart
license: mit

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

forked from TennisVisuals's block: reusable updating radarChart

<!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