I have a project in which I am using nvd3.js and want to create a semi-circular gauge. How do I create it?
I did try to use guage.js for the same but the problem I am facing there is that I cannot add customized/tags labels for the gauge that are of string type and I could not find a way around the problem. Any help is appreciated.
This is what I came up with. It's not perfect, the tooltips are rather meaningless. Also I've been trying to come up with a gradient fill to give it a 3D look, but haven't managed to yet.
<div style="height: 200px; width: 350px;">
<div style="height: 350px; width: 350px;">
<svg id="gaugeContainer"></svg>
</div>
</div>
<script>
//d3.json("/api/gauge", updateChart);
updateChart({ "max": 100, "current": 90 });
function updateChart(data) {
var containerSvg = "gaugeContainer";
var height = 350;
var width = 350;
// The first graph section always starts at 0 degrees, but we
// want it to be horizontal so we rotate the whole thing -90
// degrees, then correct the labels.
// The formula for needle angle is ((current/max) * 180) - 90
// current / max will always be 1.0 or less,
// the fraction of max that the needle indicates,
// * 180 converts to degrees and -90 compensates for rotation.
var indicatorAngle = ((data.current / data.max) * 180) - 90;
var colors = ["green", "orange", "red", "white"];
// Apparently after 4 sections are defined, the fifth one is
// dead, as in not graphed. We play a little trick, defining a
// very small white section so we can display
// the max value at the end of its visible range.
// The key values could just as easily be strings.
var graphData = [
{ key: Math.round((data.max / 8) * 1), y: 1 },
{ key: Math.round((data.max / 8) * 3), y: 1 },
{ key: Math.round((data.max / 8) * 6), y: 2 },
{ key: Math.round((data.max / 8) * 8), y: 0.2 },
{ key: "", y: 3.8 }
];
var arcRadius = [
{ inner: 0.6, outer: 1 },
{ inner: 0.6, outer: 1 },
{ inner: 0.6, outer: 1 },
{ inner: 0.6, outer: 1 }
];
nv.addGraph(function () {
var chart = nv.models.pieChart()
.x(function (d) { return d.key })
.y(function (d) { return d.y })
.donut(true)
.showTooltipPercent(false)
.width(width)
.height(height)
.color(colors)
.arcsRadius(arcRadius)
.donutLabelsOutside(true)
.showLegend(false)
.growOnHover(false)
.labelSunbeamLayout(false);
d3.select("#" + containerSvg)
.datum(graphData)
.transition().duration(1)
.attr('width', width)
.attr('height', height)
.attr("transform", "rotate(-90)")
.call(chart);
// draw needle
d3.select("#" + containerSvg)
.append("path")
.attr("class", "line")
.attr("fill", "none")
.style("stroke", "gray")
.style("stroke-width", "1")
.attr("d", "M0, -3 L153, 0 0,3 Z")
.attr("transform", "translate(175,179) rotate(" + indicatorAngle + ")");
d3.select("#" + containerSvg)
.append("circle")
.attr("r", "6")
.attr("cx", "0")
.attr("cy", "0")
.style("stroke", "gray")
.style("stroke-width", "1")
.attr("transform", "translate(175,179) rotate(" + indicatorAngle + ")");
// correct text rotation
d3.select("#" + containerSvg).selectAll("text")
.attr("transform", "rotate(90)");
return chart;
});
}
</script>
Related
When using neo4j, if you click the node, the circle tooltip will be shown with three new features.
I am just wondering how to have this kind of tooltip by using d3. Does anyone has any ideas how to implement it?
Thanks.
Here is a very basic example to get you started.
It displays a circle and its concentric "tooltip" arcs when it's clicked:
var width = 400;
var height = 200;
var svg =
d3.select("svg").attr("width", width).attr("height", height)
.append("g").attr("transform", "translate(" + (width / 2) + "," + (height / 2) + ")");
var clicked = false;
svg.append("circle")
.attr("cx", 0)
.attr("cy", 0)
.attr("r", 45)
.attr("fill", "green")
.attr("stroke", "light-grey")
.on("click", addOrRemoveTooltip)
.attr("cursor", "pointer");
function addOrRemoveTooltip() {
if (clicked) {
d3.selectAll("path").remove();
clicked = false;
} else {
var arc = d3.arc().innerRadius(47).outerRadius(80);
svg.selectAll("arcs")
.data([
{ start: 0, end: 1/3 },
{ start: 1/3, end: 2/3 },
{ start: 2/3, end: 1 }
])
.enter().append("path")
.attr("d", d => arc({
"startAngle": d.start * 2 * Math.PI + 0.01,
"endAngle": d.end * 2 * Math.PI - 0.01
}))
.attr("fill", "lightgrey");
clicked = true;
}
}
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
The circle is given a listener on clicks:
svg.append("circle").on("click", doSomething)
When the circle is clicked, this listener is activated and draws 3 arcs this way:
var arc = d3.arc().innerRadius(47).outerRadius(80);
svg.selectAll("arcs")
.data([
{ start: 0, end: 1/3 },
{ start: 1/3, end: 2/3 },
{ start: 2/3, end: 1 }
])
.enter().append("path")
.attr("d", d => arc({
"startAngle": d.start * 2 * Math.PI + 0.01,
"endAngle": d.end * 2 * Math.PI - 0.01
}))
We then need a global variable which will store the status of the button: if it's clicked or not.
This way when the circle's click listener is activated again, we know that its previous state was clicked, which means the tooltip arcs should be removed:
d3.selectAll("path").remove();
I'm pretty new to d3js and feeling a little overwhelmed here. I'm trying to figure out how to query a rotated rectangle's corner coordinates so i can place a circle on that location (eventually I'm going to use that as a starting coordinate for a line to link to other nodes).
Here is an image showing what I'm trying to do:
Currently I'm getting the circle on the left of the svg boundary below, I'm trying to place it roughly where the x is below.
Here is my code for the circle:
let rx = node.attr("x");
let ry = node.attr("y");
g.append("circle")
.attr("cx",rx)
.attr("cy",ry)
.attr("r",5);
Here is my jsFiddle: jsFiddle and a Stack Overflow snippet
let d3Root = 'd3-cpm';
let w = document.documentElement.clientWidth;
let h = document.documentElement.clientHeight;
//TODO put type any
let eData = {
width: 180,
height: 180,
padding: 80,
fill: '#E0E0E0',
stroke: '#c3c5c5',
strokeWidth: 3,
hoverFill: '#1958b5',
hoverStroke: '#0046ad',
hoverTextColor: '#fff',
rx: 18,
ry: 18,
rotate: 45,
label: 'Decision Node',
textFill: 'black',
textHoverFill: 'white'
};
let cWidth;
let cHeight = h;
d3.select(d3Root)
.append("div")
.attr("id", "d3-root")
.html(function () {
let _txt = "Hello From D3! <br/>Frame Width: ";
let _div = d3.select(this);
let _w = _div.style("width");
cWidth = parseInt(_div.style("width"));
_txt += cWidth + "<br/> ViewPort Width: " + w;
return _txt;
});
let svg = d3.select(d3Root)
.append("svg")
.attr("width", cWidth)
.attr("height", cHeight)
.call(d3.zoom()
//.scaleExtent([1 / 2, 4])
.on("zoom", zoomed));
;
let g = svg.append("g")
.on("mouseover", function (d) {
d3.select(this)
.style("cursor", "pointer");
d3.select(this).select("rect")
.style("fill", eData.hoverFill)
.style("stroke", eData.hoverStroke);
d3.select(this).select("text")
.style("fill", eData.textHoverFill);
})
.on("mouseout", function (d) {
d3.select(this)
.style("cursor", "default");
d3.select(this).select("rect")
.style("fill", eData.fill)
.style("stroke", eData.stroke);
d3.select(this).select("text")
.style("fill", eData.textFill);
});
let node = g.append("rect")
.attr("width", eData.width)
.attr("height", eData.height)
.attr("fill", eData.fill)
.attr("stroke", eData.stroke)
.attr("stroke-width", eData.strokeWidth)
.attr("rx", eData.rx)
.attr("ry", eData.ry)
.attr("y", eData.padding)
.attr('transform', function () {
let _x = calcXLoc();
console.log(_x);
return "translate(" + _x + "," + "0) rotate(45)";
})
.on("click", ()=> {
console.log("rect clicked");
d3.event.stopPropagation();
//this.nodeClicked();
});
let nText = g.append('text')
.text(eData.label)
.style('fill', eData.textFill)
.attr('x', calcXLoc() - 50)
.attr('y', eData.width + 10)
.attr("text-anchor", "middle")
.on("click", ()=> {
console.log("text clicked");
d3.event.stopPropagation();
//this.nodeClicked();
});
let rx = node.attr("x");
let ry = node.attr("y");
g.append("circle")
.attr("cx",rx)
.attr("cy",ry)
.attr("r",5);
function calcXLoc() {
return (cWidth / 2 - eData.width / 2) + eData.width;
}
function zoomed() {
g.attr("transform", d3.event.transform);
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<d3-cpm></d3-cpm>
You're applying a transform to your rect to position and rotate it. It has no x attribute, so that comes back as undefined. This gets you slightly closer:
let rx = parseInt(node.attr("x"), 10) | 0;
let ry = parseInt(node.attr("y"), 10) | 0;
let height = parseInt(node.attr("height"), 10) | 0;
let transform = node.attr("transform");
g.append("circle")
.attr("cx",rx + height)
.attr("cy",ry + height)
.attr("transform", transform)
.attr("r",5);
But note that this is going to get kind of clunky and difficult to deal with - it'd be better if your data was modeled in such a way that the circular points were handled in there as well and could be somehow derived/transformed consistently....
Updated fiddle: https://jsfiddle.net/dcw48tk6/7/
Image:
Thisis an example by Mike Bostock of a "simple" hive graph (as he refers to it in this article ). It has three "axis" created by this code
svg.selectAll(".axis")
.data(d3.range(3))
.enter().append("line")
.attr("class", "axis")
.attr("transform", function(d) { return "rotate(" + degrees(angle(d)) + ")"; })
.attr("x1", radius.range()[0])
.attr("x2", radius.range()[1]);
As you can see from the first link, the three "axes" form a circle, which seems to be accomplished by the rotation in the "transform" of the code above and use of these angle and degrees functions
var angle = d3.scale.ordinal().domain(d3.range(4)).rangePoints([0, 2 * Math.PI]),
function degrees(radians) {
return radians / Math.PI * 180 - 90;
}
Question: if there were only two "axes", how would it be possible (using a "translate") to stack the "axes" on top of each other (i.e. as two horizontal lines parallel to each other?
In my attempt to do this, I tried to remove the rotation of the "axis" and then to space them vertically. To stop the rotation,I removed the call to "degrees" like this
.attr("transform", function(d) { return "rotate(" + angle(d) + ")"; })
and I also set the range of the angles to be 0,0
d3.scale.ordinal().domain(["one", "two"]).range([0,0]);
then , to space the axes, I included a "translate" like this
.attr("transform", function(d) {return "translate(" + width /2 + "," + height/d + ")"});
The result is that there is one visible horizontal axis, and it seems the other one exists but is only detectable when I run the mouse over it ( and the nodes and lines haven't been repositioned)
Not sure if this is what you are after but two "axis" stacked vertically can be achieved with:
var angle = d3.scale.ordinal()
.domain(d3.range(3)) //<-- only calculate angles for 2 [-90, 90]
.rangePoints([0, 2 * Math.PI]),
...
svg.selectAll(".axis")
.data(d3.range(2)) //<-- 2 lines
EDITS
What are you are describing is not really a hive plot and attempting to re-purpose the layout is probably more trouble then it's worth. If you just want linked points on a line, here's an off-the-cuff implementation:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.link {
fill: none;
stroke-width: 1.5px;
}
.axis, .node {
stroke: #000;
stroke-width: 1.5px;
}
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="d3.hive.min.js"></script>
<script>
var width = 500,
height = 500;
var lineSep = 200,
lineLen = 400,
color = d3.scale.category10().domain(d3.range(20)),
margin = [50,50];
var nodes = [
{x: 0, y: .1},
{x: 0, y: .9},
{x: 0, y: .2},
{x: 1, y: .3},
{x: 1, y: .1},
{x: 1, y: .8},
{x: 1, y: .4},
{x: 1, y: .6},
{x: 1, y: .2},
{x: 1, y: .7},
{x: 1, y: .8}
];
var links = [
{source: nodes[0], target: nodes[3]},
{source: nodes[1], target: nodes[3]},
{source: nodes[2], target: nodes[4]},
{source: nodes[2], target: nodes[9]},
{source: nodes[3], target: nodes[0]},
{source: nodes[4], target: nodes[0]},
{source: nodes[5], target: nodes[1]}
];
var nodeNest = d3.nest().key(function(d){ return d.x }).entries(nodes);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + margin[0] + "," + margin[1] + ")");
var axis = svg.selectAll(".axis")
.data(nodeNest);
var g = axis
.enter().append("g")
.attr("class", "axis")
.attr("transform", function(d,i) {
var t = "translate(0," + (i * lineSep) + ")";
return t;
})
.append("line")
.attr("x1", 0)
.attr("x2", lineLen);
axis.selectAll(".node")
.data(function(d){
d.values.forEach(function(q){
q.len = d.values.length;
})
return d.values;
})
.enter().append("circle")
.attr("class", "node")
.attr("cx", function(d, i, j) {
d.cx = ((i + 0.5) * (lineLen / d.len));
d.cy = (j * lineSep);
return d.cx;
})
.attr("r", 5)
.style("fill", function(d) { return color(d.x); });
svg.selectAll(".link")
.data(links)
.enter().append("path")
.attr("class", "link")
.attr("d", function(d){
console.log(d);
var p = "";
p += "M" + d.source.cx + "," + d.source.cy;
p += "Q" + "0," + ((margin[1] / 2) + (lineSep/2));
p += " " + d.target.cx + "," + d.target.cy;
return p;
})
.style("stroke", function(d) {
return color(d.source.x);
});
function degrees(radians) {
return radians / Math.PI * 180 - 90;
}
</script>
I have this code which outputs a straight line composed of a bunch of line segments:
var width = 400;
var height = 100;
var data = [1,1,1,1,1,1,1,1,1,1];
var lineSegments = data.length + 1;
d3.select("body").append("svg")
.attr({
width: width,
height: height
})
.selectAll("line")
.data(data)
.enter()
.append("line")
.attr({
x1: function (d, i) { return i * (width / lineSegments); },
x2: function (d, i) { return (i + 1) * (width / lineSegments); },
y1: function (d, i) { return height / 2; },
y2: function (d, i) { return height / 2; },
stroke: "black",
"stroke-width": 2
});
I want every 3d segment to be offset in the y direction by, say 10px, and I need the offset to be cumlative, i.e. the 3d element should have offset 10px, the 6th element should be offset 20px, etc.
This should result in a line like this:
How should I modify the code to get it to work? Is there a special d3 way of doing this?
You can use d3's selection.each() to target every 3d segment. This method has some useful variables for performing various functions.
line.each(function(d, i) {
console.log(i); // prints the current index
console.log(d); // prints the data at the current index
console.log(this); // prints the current DOM element
}
In your specific case, the i variable is useful to you. You can use this inside of an if statement to change the attributes of every 3rd element. Your revised code might be...
var width = 400;
var height = 100;
var data = [1,1,1,1,1,1,1,1,1,1];
var lineSegments = data.length + 1;
var offset = 5;
d3.select("body").append("svg")
.attr({
width: width,
height: height
})
.selectAll("line")
.data(data)
.enter()
.append("line")
.attr({
x1: function (d, i) { return i * (width / lineSegments); },
x2: function (d, i) { return (i + 1) * (width / lineSegments); },
y1: function (d, i) { return height / 2; },
y2: function (d, i) { return height / 2; },
stroke: "black",
"stroke-width": 2
})
.each(function(d,i) {
if (i !== 0 && i % 3 === 0) {
d3.select(this).attr('transform', 'translate(0,' + offset + ')');
offset += 5;
}
})
Here is a working codepen.
I've built a chart with a series of "small multiples", each representing a percentage of completion, arranged in rows and columns. Each row has a different number that represents "complete".
You can view the chart in this fiddle: http://jsfiddle.net/rolfsf/Lhnm9a9m/
I'm using transitions to grow the bars from 0 to x% width.
I also want to use a transition to show the actual count of each measure (e.g. 10 out of 17) incrementing from 0.
I've got all the numbers incrementing from 0 using a text tween function, but all of the measures stop at the same count, rather than their correct count.
I must be either using the wrong data in the tween, or placing the tween in the wrong part of the script... but I can't figure out where the problem is.
How do I get the numbers to increment properly??
My data looks like this:
var sets = [
{"title": "Set-1", "count": 17, "measures": [10, 13, 16, 14]},
{"title": "Set-2", "count": 23, "measures": [12, 18, 19, 23]},
{"title": "Set-3", "count": 25, "measures": [19, 22, 23, 20]},
{"title": "Set-4", "count": 4, "measures": [4, 4, 4, 4]},
{"title": "Set-5", "count": 8, "measures": [5, 7, 8, 6]}
];
The chart is called like this:
d3 .select('#overview-graph')
.datum(sets)
.call(relativeCompletionChart()
//options
);
and here's the reusable chart script:
function relativeCompletionChart() {
var width = 1200,
margin = {top: 16, right: 16, bottom: 16, left: 16},
onSetMouseOver = null,
onSetClick = null;
function chart(selection) {
selection.each(function(data) {
var titleColWidth = 0.3*width,
setHeight = 24,
barHeight = 22,
setCount = data.length,
colCount = data[0].measures.length,
colWidth = (width - titleColWidth)/colCount,
rangeWidth = colWidth - 4,
height = ((setHeight * setCount) + margin.top + margin.bottom);
var svg = d3.select(this)
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", "0 0 " + width + " " + height )
.attr("preserveAspectRatio", "xMidYMin meet");
svg .append('rect')
.attr("x", 0)
.attr('y', 0)
.attr('height', height)
.attr('width', width)
.attr('class', 'chart-bg');
/**
* Tween functions
*/
function tweenText( newValue ) {
return function() {
// get current value as starting point for tween animation
var currentValue = +this.textContent;
// create interpolator and do not show nasty floating numbers
var i = d3.interpolateRound( currentValue, newValue );
return function(t) {
this.textContent = i(t);
};
}
}
function update(data) {
var set = svg.selectAll("g.set")
.data(data);
set.exit().remove();
var setEnter = set
.enter().append("g")
.attr("class", "set")
.attr('transform', function (d, i) {
return 'translate(0, ' + (margin.top + i*setHeight) + ')';
});
set.append("text")
.attr("class", "title")
.attr("x", (titleColWidth - 80))
.attr("y", 16)
.attr("text-anchor", "end")
.text(function (d){return d.title;});
set.append("text")
.attr("class", "count")
.attr("x", (titleColWidth - 32))
.attr("y", 16)
.attr("text-anchor", "end")
.text(function (d){return d.count;});
var ranges = set.selectAll("rect.range")
.data(function(d, i){return d.measures})
.enter().append('rect')
.attr("class", "range")
.attr("x",2)
.attr("y",0)
.attr('rx', 2)
.attr('ry', 2)
.attr("width", rangeWidth)
.attr("height",barHeight)
.attr("fill", "#CCCCCC")
.attr('transform', function (d, i) {
return 'translate(' + (titleColWidth + i*colWidth) + ', 0)';
});
var measures = set.selectAll("rect.measure")
.data(function(d, i){return d.measures})
.enter().append('rect')
.attr("class", "measure")
.attr('rx', 2)
.attr('ry', 2)
.attr("x",2)
.attr("y",0)
.attr("width", 1)
.attr("height",barHeight)
.attr('transform', function (d, i) {
return 'translate(' + (titleColWidth + i*colWidth) + ', 0)';
});
var markers = set.selectAll("line.marker")
.data(function(d, i){return d.measures})
.enter().append('line')
.attr("class", "marker")
.attr('x1', 2)
.attr('y1', 0)
.attr("x2",2)
.attr("y2",barHeight)
.attr('transform', function (d, i) {
return 'translate(' + (titleColWidth + i*colWidth) + ', 0)';
});
var values = set.selectAll("text.value")
.data(function(d, i){return d.measures})
.enter().append('text')
.text('0')
.attr("class", "value")
.attr("x", 8)
.attr("y", 16)
.attr("text-anchor", "start")
.attr('transform', function (d, i) {
return 'translate(' + (titleColWidth + i*colWidth) + ', 0)';
});
//update widths
set.selectAll("rect.measure")
.transition()
.duration(1000)
.delay(function(d, i) { return i * 20; })
.attr("width", function(d, i) { return d3.round((d/d3.select(this.parentNode).datum().count)*rangeWidth);});
set.selectAll("line.marker")
.transition()
.duration(1000)
.delay(function(d, i) { return i * 20; })
.attr("x1", function(d, i) { return d3.round((d/d3.select(this.parentNode).datum().count)*rangeWidth + 1);})
.attr("x2", function(d, i) { return d3.round((d/d3.select(this.parentNode).datum().count)*rangeWidth + 1);});
set.selectAll('text.value')
.transition()
.duration(1000)
.tween( 'text', function() {
// get current value as starting point for tween animation
var currentValue = +this.textContent;
// create interpolator and do not show nasty floating numbers
var interpolator = d3.interpolateRound( currentValue, 10 );
// this returned function will be called a couple
// of times to animate anything you want inside
// of your custom tween
return function( t ) {
// set new value to current text element
this.textContent = interpolator(t) + '/' + d3.select(this.parentNode).datum().count;
};
});
}
update(data);
});
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.onSetClick = function(_) {
if (!arguments.length) return onSetClick;
onSetClick = _;
return chart;
};
chart.onSetMouseOver = function(_) {
if (!arguments.length) return onSetMouseOver;
onSetMouseOver = _;
return chart;
};
return chart;
}
The relevant code for the tweening is pulled out here:
set.selectAll('text.value')
.transition()
.duration(1000)
.tween( 'text', function() {
// get current value as starting point for tween animation
var currentValue = +this.textContent;
// create interpolator and do not show nasty floating numbers
var interpolator = d3.interpolateRound( currentValue, 10 );
// this returned function will be called a couple
// of times to animate anything you want inside
// of your custom tween
return function( t ) {
// set new value to current text element
this.textContent = interpolator(t) + '/' + d3.select(this.parentNode).datum().count;
};
});
Though I've also got an unused helper function in the script that I couldn't get to work:
/**
* Tween functions
*/
function tweenText( newValue ) {
return function() {
// get current value as starting point for tween animation
var currentValue = +this.textContent;
// create interpolator and do not show nasty floating numbers
var i = d3.interpolateRound( currentValue, newValue );
return function(t) {
this.textContent = i(t);
};
}
}