Can't see text drawn on HTML5 canvas - d3.js

I'm using d3 and roughjs to draw some pie charts. I draw the arcs, then put some text, but the text is not "showing up". The text flashes on the screen right where it should be as soon as the pages loads, but then it gets covered by the pie charts. I looked for some z-index but I haven't been able to find a solution. What am I getting wrong?
I can see the labels in place also when debugging
Debugging:
http://i.imgur.com/jKQzcH2.png
Right after:
http://i.imgur.com/KPz5Oxa.png
Here's the code
<!DOCTYPE html>
<meta charset="utf-8">
<canvas></canvas>
<script type="text/javascript" src="https://d3js.org/d3.v4.min.js"></script>
<script type="text/javascript" src="https://roughjs.com/builds/rough.min.js"></script>
<script>
var twData = [{"concept" : "macri", "sentiment" : [0.1, 0.9]},
{"concept" : "paritarias", "sentiment" : [0.2, 0.8]},
{"concept" : "churros", "sentiment" : [0.7, 0.3]}];
var canvas = document.querySelector("canvas"),
context = canvas.getContext("2d");
rough = new RoughCanvas(canvas, 960, 500);
context.translate(width / 2, height / 2);
context.textAlign = "center";
context.textBaseline = "middle";
context.fillStyle = "#000";
context.font="25px Verdana";
var width = canvas.width,
height = canvas.height,
radius = (Math.min(width, height) / 2) / twData.length;
rough.hachureGap = 1;
rough.strokeWidth = 0.5;
rough.fillWeight = 1;
var colors = ["#009933", "#800000"];
var labelArc = d3.arc()
.outerRadius(radius - 40)
.innerRadius(radius - 40)
.context(context);
var dx = (width/2) / twData.length + 50;
dy = (height/2) / twData.length;
for(k=0;k<twData.length;k++){
var pie = d3.pie()
.sort(null)
.value(function(d) { return d; });
// Arcs
var arcs = pie(twData[k].sentiment);
arcs.forEach(function(d, i) {
var a = rough.arc(dx*(k+1), dy, radius * 2, radius * 2, d.startAngle - Math.PI/2, d.endAngle - Math.PI/2, true);
a.fill = colors[i];
//a.hachureAngle = Math.random() * 360;
});
// Labels
arcs.forEach(function(d) {
var c = labelArc.centroid(d);
context.fillText(d.data, dx*(k+1)+c[0], dy+c[1]);
console.log("Agregando label " + d.data + " en (" + Math.round(dx*(k+1)+c[0]) + "," + Math.round(dy+c[1])+")");
});
context.fillText(twData[k].concept,dx*(k+1), dy+100)
console.log("Agregando label " + twData[k].concept + " en (" + Math.round(dx*(k+1)) + "," + Math.round(dy+100)+")");
} //for
</script>
Thanks!

rough.js takes control of the canvas and clears it for it's own purposes. I found the key in a comment on this example:
// since we are going to draw on canvas directly, i.e using context2d and not rough.js,
// we call requestAnimationFrame. This will prevent our drawing to be cleared by rough.js
So, your code becomes:
<!DOCTYPE html>
<meta charset="utf-8">
<canvas></canvas>
<script type="text/javascript" src="https://d3js.org/d3.v4.min.js"></script>
<script type="text/javascript" src="https://roughjs.com/builds/rough.min.js"></script>
<script>
var twData = [{
"concept": "macri",
"sentiment": [0.1, 0.9]
}, {
"concept": "paritarias",
"sentiment": [0.2, 0.8]
}, {
"concept": "churros",
"sentiment": [0.7, 0.3]
}];
var canvas = document.querySelector("canvas"),
context = canvas.getContext("2d");
rough = new RoughCanvas(canvas, 960, 500);
context.translate(width / 2, height / 2);
context.textAlign = "center";
context.textBaseline = "middle";
context.fillStyle = "#000";
context.font = "25px Verdana";
var width = canvas.width,
height = canvas.height,
radius = (Math.min(width, height) / 2) / twData.length;
rough.hachureGap = 1;
rough.strokeWidth = 0.5;
rough.fillWeight = 1;
var colors = ["#009933", "#800000"];
var labelArc = d3.arc()
.outerRadius(radius - 40)
.innerRadius(radius - 40)
.context(context);
var dx = (width / 2) / twData.length + 50;
dy = (height / 2) / twData.length;
for (k = 0; k < twData.length; k++) {
var pie = d3.pie()
.sort(null)
.value(function(d) {
return d;
});
// Arcs
var arcs = pie(twData[k].sentiment);
arcs.forEach(function(d, i) {
var a = rough.arc(dx * (k + 1), dy, radius * 2, radius * 2, d.startAngle - Math.PI / 2, d.endAngle - Math.PI / 2, true);
a.fill = colors[i];
//a.hachureAngle = Math.random() * 360;
});
} //for
window.requestAnimationFrame(function() {
for (k = 0; k < twData.length; k++) {
// Labels
arcs.forEach(function(d) {
var c = labelArc.centroid(d);
context.fillText(d.data, dx * (k + 1) + c[0], dy + c[1]);
console.log("Agregando label " + d.data + " en (" + Math.round(dx * (k + 1) + c[0]) + "," + Math.round(dy + c[1]) + ")");
});
context.fillText(twData[k].concept, dx * (k + 1), dy + 100)
//console.log("Agregando label " + twData[k].concept + " en (" + Math.round(dx * (k + 1)) + "," + Math.round(dy + 100) + ")");
}
});
</script>

Related

D3 x3dom - 3d scatter plot

I'm trying to reproduce an 3d-ScatterPlot using the d3 library. The example is written in v3 and I'm having difficulties to recreate it in v5.
The issue is that the axis don't get drawn.
function drawAxis( axisIndex, key, duration ) {
var scale = d3.scaleLinear()
.domain( [-5,5] ) // demo data range
.range( axisRange )
scales[axisIndex] = scale;
var numTicks = 8;
var tickSize = 0.1;
var tickFontSize = 0.5;
// ticks along each axis
var ticks = scene.selectAll( "."+axisName("Tick", axisIndex) )
.data( scale.ticks( numTicks ));
var newTicks = ticks.enter()
.append("transform")
.attr("class", axisName("Tick", axisIndex));
newTicks.append("shape").call(makeSolid)
.append("box")
.attr("size", tickSize + " " + tickSize + " " + tickSize);
// enter + update
ticks.attr("translation", function(tick) {
return constVecWithAxisValue( 0, scale(tick), axisIndex ); })
ticks.exit().remove();
// tick labels
var tickLabels = ticks.selectAll("billboard shape text")
.data(function(d) { return [d]; });
var newTickLabels = tickLabels.enter()
.append("billboard")
.attr("axisOfRotation", "0 0 0")
.append("shape")
.call(makeSolid)
newTickLabels.append("text")
.attr("string", scale.tickFormat(10))
.attr("solid", "true")
.append("fontstyle")
.attr("size", tickFontSize)
.attr("family", "SANS")
.attr("justify", "END MIDDLE" );
tickLabels // enter + update
.attr("string", scale.tickFormat(10))
tickLabels.exit().remove();
// base grid lines
if (axisIndex==0 || axisIndex==2) {
var gridLines = scene.selectAll( "."+axisName("GridLine", axisIndex))
.data(scale.ticks( numTicks ));
gridLines.exit().remove();
var newGridLines = gridLines.enter()
.append("transform")
.attr("class", axisName("GridLine", axisIndex))
.attr("rotation", axisIndex==0 ? [0,1,0, -Math.PI/2] : [0,0,0,0])
.append("shape")
newGridLines.append("appearance")
.append("material")
.attr("emissiveColor", "gray")
newGridLines.append("polyline2d");
gridLines.selectAll("shape polyline2d").attr("lineSegments", "0 0, " + axisRange[1] + " 0")
gridLines.attr("translation", axisIndex==0
? function(d) { return scale(d) + " 0 0"; }
: function(d) { return "0 0 " + scale(d); }
)
}
}
My guess is that the issue is in this method.
I created a full example here: https://codepen.io/anon/pen/aevWQX
Thanks for your help!
From v4 onward, you need to .merge() your newly added .enter() selection and your existing selection, otherwise the result is an empty selection - which is why the code was executed, only not applied to any elements.
var x3d = d3.select("#divPlot")
.append("x3d")
.style("width", "500px")
.style("height", "500px")
.style("border", "none")
var scene = x3d.append("scene")
scene.append("orthoviewpoint")
.attr("centerOfRotation", [5, 5, 5])
.attr("fieldOfView", [-5, -5, 15, 15])
.attr("orientation", [-0.5, 1, 0.2, 1.12 * Math.PI / 4])
.attr("position", [8, 4, 15])
var rows = initializeDataGrid();
var axisRange = [0, 10];
var scales = [];
var initialDuration = 0;
var defaultDuration = 800;
var ease = 'linear';
var time = 0;
var axisKeys = ["x", "y", "z"]
// Helper functions for initializeAxis() and drawAxis()
function axisName(name, axisIndex) {
return ['x', 'y', 'z'][axisIndex] + name;
}
function constVecWithAxisValue(otherValue, axisValue, axisIndex) {
var result = [otherValue, otherValue, otherValue];
result[axisIndex] = axisValue;
return result;
}
// Used to make 2d elements visible
function makeSolid(selection, color) {
selection.append("appearance")
.append("material")
.attr("diffuseColor", color || "black")
return selection;
}
// Initialize the axes lines and labels.
function initializePlot() {
initializeAxis(0);
initializeAxis(1);
initializeAxis(2);
}
function initializeAxis(axisIndex) {
var key = axisKeys[axisIndex];
drawAxis(axisIndex, key, initialDuration);
var scaleMin = axisRange[0];
var scaleMax = axisRange[1];
// the axis line
var newAxisLine = scene.append("transform")
.attr("class", axisName("Axis", axisIndex))
.attr("rotation", ([
[0, 0, 0, 0],
[0, 0, 1, Math.PI / 2],
[0, 1, 0, -Math.PI / 2]
][axisIndex]))
.append("shape")
newAxisLine
.append("appearance")
.append("material")
.attr("emissiveColor", "lightgray")
newAxisLine
.append("polyline2d")
// Line drawn along y axis does not render in Firefox, so draw one
// along the x axis instead and rotate it (above).
.attr("lineSegments", "0 0," + scaleMax + " 0")
// axis labels
var newAxisLabel = scene.append("transform")
.attr("class", axisName("AxisLabel", axisIndex))
.attr("translation", constVecWithAxisValue(0, scaleMin + 1.1 * (scaleMax - scaleMin), axisIndex))
var newAxisLabelShape = newAxisLabel
.append("billboard")
.attr("axisOfRotation", "0 0 0") // face viewer
.append("shape")
.call(makeSolid)
var labelFontSize = 0.6;
newAxisLabelShape
.append("text")
.attr("class", axisName("AxisLabelText", axisIndex))
.attr("solid", "true")
.attr("string", key)
.append("fontstyle")
.attr("size", labelFontSize)
.attr("family", "SANS")
.attr("justify", "END MIDDLE")
}
// Assign key to axis, creating or updating its ticks, grid lines, and labels.
function drawAxis(axisIndex, key, duration) {
var scale = d3.scaleLinear()
.domain([-5, 5]) // demo data range
.range(axisRange)
scales[axisIndex] = scale;
var numTicks = 8;
var tickSize = 0.1;
var tickFontSize = 0.5;
// ticks along each axis
var ticks = scene.selectAll("." + axisName("Tick", axisIndex))
.data(scale.ticks(numTicks));
var newTicks = ticks.enter()
.append("transform")
.attr("class", axisName("Tick", axisIndex));
newTicks.append("shape").call(makeSolid)
.append("box")
.attr("size", tickSize + " " + tickSize + " " + tickSize);
// enter + update
ticks.attr("translation", function(tick) {
return constVecWithAxisValue(0, scale(tick), axisIndex);
})
ticks.exit().remove();
// tick labels
var tickLabels = ticks.selectAll("billboard shape text")
.data(function(d) {
return [d];
});
var newTickLabels = tickLabels.enter()
.append("billboard")
.attr("axisOfRotation", "0 0 0")
.append("shape")
.call(makeSolid)
newTickLabels.append("text")
.attr("string", scale.tickFormat(10))
.attr("solid", "true")
.append("fontstyle")
.attr("size", tickFontSize)
.attr("family", "SANS")
.attr("justify", "END MIDDLE");
tickLabels // enter + update
.attr("string", scale.tickFormat(10))
tickLabels.exit().remove();
// base grid lines
if (axisIndex == 0 || axisIndex == 2) {
debugger;
var gridLines = scene.selectAll("." + axisName("GridLine", axisIndex))
.data(scale.ticks(numTicks));
gridLines.exit().remove();
var newGridLines = gridLines.enter()
.append("transform")
.attr("class", axisName("GridLine", axisIndex))
.attr("rotation", axisIndex == 0 ? [0, 1, 0, -Math.PI / 2] : [0, 0, 0, 0])
.append("shape")
newGridLines.append("appearance")
.append("material")
.attr("emissiveColor", "gray")
newGridLines.append("polyline2d");
gridLines = newGridLines
.merge(gridLines);
gridLines.selectAll("shape polyline2d").attr("lineSegments", "0 0, " + axisRange[1] + " 0")
gridLines.attr("translation", axisIndex == 0 ?
function(d) {
return scale(d) + " 0 0";
} :
function(d) {
return "0 0 " + scale(d);
}
)
}
}
// Update the data points (spheres) and stems.
function plotData(duration) {
if (!rows) {
console.log("no rows to plot.")
return;
}
var x = scales[0],
y = scales[1],
z = scales[2];
var sphereRadius = 0.2;
// Draw a sphere at each x,y,z coordinate.
var datapoints = scene.selectAll(".datapoint").data(rows);
datapoints.exit().remove()
var newDatapoints = datapoints.enter()
.append("transform")
.attr("class", "datapoint")
.attr("scale", [sphereRadius, sphereRadius, sphereRadius])
.append("shape");
newDatapoints
.append("appearance")
.append("material");
newDatapoints
.append("sphere")
// Does not work on Chrome; use transform instead
//.attr("radius", sphereRadius)
datapoints.selectAll("shape appearance material")
.attr("diffuseColor", 'steelblue')
datapoints.transition().ease(d3.easeLinear).duration(duration)
.attr("translation", function(row) {
return x(row[axisKeys[0]]) + " " + y(row[axisKeys[1]]) + " " + z(row[axisKeys[2]])
})
}
function initializeDataGrid() {
var rows = [];
// Follow the convention where y(x,z) is elevation.
for (var x = -5; x <= 5; x += 1) {
for (var z = -5; z <= 5; z += 1) {
rows.push({
x: x,
y: 0,
z: z
});
}
}
return rows;
}
function updateData() {
time += Math.PI / 8;
if (x3d.node() && x3d.node().runtime) {
for (var r = 0; r < rows.length; ++r) {
var x = rows[r].x;
var z = rows[r].z;
rows[r].y = 5 * (Math.sin(0.5 * x + time) * Math.cos(0.25 * z + time));
}
plotData(defaultDuration);
} else {
console.log('x3d not ready.');
}
}
initializeDataGrid();
initializePlot();
setInterval(updateData, defaultDuration);
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis text {
font: 10px sans-serif;
}
<html>
<head>
<meta http-equiv="X-UA-Compatible" content="chrome=1" />
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
<title>3D Scatter Plot</title>
<!--<script type="text/javascript" src="http://d3js.org/d3.v5.min.js"></script>-->
<script type="text/javascript" src="http://d3js.org/d3.v5.js"></script>
<script type="text/javascript" src="http://x3dom.org/x3dom/dist/x3dom-full.js"></script>
<link rel="stylesheet" type="text/css" href="http://www.x3dom.org/download/dev/x3dom.css" />
</head>
<body>
<div id="divPlot" style="width: 500px; height: 500px;"></div>
</body>
</html>
The only change I made is the call to .merge somewhere inside the if statement in drawAxis. Hope this helps!

D3.js - Add label along the arc

I am trying to implement this gauge to show target and actual values.
Here the position of the target value '45%' is given by a fixed number, so that it always stays at the top of the gauge as shown in below image:
How do I make this label stick to the beginning of second arc dynamically, similar to this:
Here is a snippet of current code I am using with hardcoded translate values:
var barWidth, chart, chartInset, degToRad, repaintGauge,
height, margin, numSections, padRad, percToDeg, percToRad,
percent, radius, sectionIndx, svg, totalPercent, width;
percent = percentValue;
numSections = 1;
sectionPerc = 1 / numSections / 2;
padRad = 0.025;
chartInset = 10;
// Orientation of gauge:
totalPercent = .75;
el = d3.select('#HSFO');
margin = {
top: 12,
right: 12,
bottom: 0,
left: 12
};
width = el[0][0].offsetWidth - margin.left - margin.right;
height = width;
radius = Math.min(width, height) / 2;
barWidth = 40 * width / 300;
//Utility methods
percToDeg = function(perc) {
return perc * 360;
};
percToRad = function(perc) {
return degToRad(percToDeg(perc));
};
degToRad = function(deg) {
return deg * Math.PI / 180;
};
// Create SVG element
svg = el.append('svg').attr('width', width + margin.left + margin.right).attr('height', height + margin.top + margin.bottom);
// Add layer for the panel
chart = svg.append('g').attr('transform', "translate(" + ((width + margin.left) / 2) + ", " + ((height + margin.top) / 2) + ")");
chart.append('path').attr('class', "arc chart-first");
chart.append('path').attr('class', "arc chart-second");
chart.append('path').attr('class', "arc chart-third");
formatValue = d3.format('1%');
arc3 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
arc2 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
arc1 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
repaintGauge = function() {
perc = 17 / 20;
var next_start = totalPercent;
arcStartRad = percToRad(next_start);
arcEndRad = arcStartRad + percToRad(perc / 2);
next_start += perc / 2;
arc1.startAngle(arcStartRad).endAngle(arcEndRad);
perc = 1 - perc;
arcStartRad = percToRad(next_start);
arcEndRad = arcStartRad + percToRad(perc / 2);
next_start += perc / 2;
arc2.startAngle(arcStartRad + padRad).endAngle(arcEndRad);
chart.select(".chart-first").attr('d', arc1);
chart.select(".chart-second").attr('d', arc2);
svg.append("text").attr("transform", "translate("+(width + margin.left-35) +","+ (radius - chartInset - barWidth/4.5) +")" + 'rotate('+'70'+')')
.attr("text-anchor", "middle").style("font-size", "12").style("font-family", "Helvetica").text('17')
}
I would use a text path for the percent text and use an arc as the template so that you don't have to worry about manually transforming the text and calculating the angle. This means reorganising your elements slightly and using arc3 (currently unused) as the path for the text.
The general format for text on a path is:
<path id="path_for_text" d="M-150,1.8369701987210297e-14A150,150 0 0,1 18.799985034645633,-148.8172051971717L13.45243373590199,-106.4869779410873A107.33333333333334,107.33333333333334 0 0,0 -107.33333333333334,1.3144542310848258e-14Z"></path>
<text>
<textPath xlink:href="#path_for_text">my text here</textPath>
</text>
so the basic alterations that we'll need to do on your code are adding the new arc for the text to go along, and adding in the text path element. So, let's create an appropriate arc generator:
// we want the text to be offset slightly from the outer edge of the arc, and the arc
// itself can have identical inner and outer radius measurements
var arc3 = d3.svg.arc()
.outerRadius(radius - chartInset + 10)
.innerRadius(radius - chartInset + 10)
// add the text element and give it a `textPath` element as a child
var arc_text = chart.append('text')
.attr('id', 'scale10')
.attr("font-size", 15)
.style("fill", "#000000")
// the textPath element will use an element with ID `text_arc` to provide its shape
arc_text.append('textPath')
.attr('startOffset','0%')
.attr('xlink:href', '#text_arc' )
// add the path with the ID `text_arc`
chart.append('path').attr('class', "arc chart-third")
.attr('id', 'text_arc')
In repaintGauge, calculate the appropriate arc:
// append the path to the chart, using the arc3 constructor to generate the arc
// these numbers will be the same as those for arc2, although I would add a little
// padding to both start and end angles to ensure that the text doesn't wrap if it
// is at 0% or 100%
arc3.startAngle(arcStartRad - 0.15).endAngle(arcEndRad + 0.15);
chart.select('id', 'text_arc')
.attr('d', arc3)
and update the text:
arc_text.select('textPath')
.text( percent + '%')
You can refactor your repaintGauge function to make it significantly simpler as some of the arc figures don't change; arc1's startAngle will always be at 1.5 Pi radians, and arc2's endAngle will always be 2.5 Pi radians. That means you only need to work out what your percent is in terms of radians, which is pretty simple: if 0% is 1.5 Pi and 100% is 2.5 Pi, and you want to represent perc percent, it will be p / 100 * Math.PI + 1.5 * Math.PI.
repaintGauge = function(perc) {
var arcOffset = Math.PI * 1.5
var current = Math.PI * perc / 100 + arcOffset
// arc1's endAngle and arc2, arc3's endAngle can be set to `current`
arc1.startAngle(arcOffset).endAngle(current)
arc2.startAngle(current + padRad).endAngle(arcOffset + Math.PI)
arc3.startAngle(current - 0.15).endAngle(arcOffset + Math.PI + 0.15)
chart.select(".chart-first").attr('d', arc1);
chart.select(".chart-second").attr('d', arc2);
chart.select(".chart-third").attr('d', arc3);
arc_text.select('textPath').text(perc + '%');
};
Here's a demo showing the text at different positions and with different values:
var name = "Value";
var value = 17;
var gaugeMaxValue = 100;
// data to calculate
var percentValue = value / gaugeMaxValue;
////////////////////////
var needleClient;
(function() {
var barWidth, chart, chartInset, degToRad, repaintGauge,
height, margin, numSections, padRad, percToDeg, percToRad,
percent, radius, sectionIndx, svg, totalPercent, width;
percent = percentValue;
numSections = 1;
sectionPerc = 1 / numSections / 2;
padRad = 0.025;
chartInset = 10;
var percStart = 0;
var arcOffset = Math.PI * 1.5
// Orientation of gauge:
totalPercent = .75;
el = d3.select('.chart-gauge');
margin = {
top: 40,
right: 20,
bottom: 30,
left: 60
};
width = el[0][0].offsetWidth - margin.left - margin.right;
height = width;
radius = Math.min(width, height) / 2;
barWidth = 40 * width / 300;
//Utility methods
percToDeg = function(perc) {
return perc * 360;
};
percToRad = function(perc) {
return degToRad(percToDeg(perc));
};
degToRad = function(deg) {
return deg * Math.PI / 180;
};
// Create SVG element
svg = el.append('svg').attr('width', width + margin.left + margin.right).attr('height', height + margin.top + margin.bottom);
// Add layer for the panel
chart = svg.append('g').attr('transform', "translate(" + ((width + margin.left) / 2) + ", " + ((height + margin.top) / 2) + ")");
formatValue = d3.format('1%');
var arc3 = d3.svg.arc().outerRadius(radius - chartInset + 10).innerRadius(radius - chartInset + 10),
arc2 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth),
arc1 = d3.svg.arc().outerRadius(radius - chartInset).innerRadius(radius - chartInset - barWidth)
// bind angle data directly to the chart elements
chart.append('path').attr('class', "arc chart-first")
.datum({ startAngle: arcOffset, endAngle: arcOffset })
.attr('d', arc1)
chart.append('path').attr('class', "arc chart-second")
.datum({ startAngle: arcOffset, endAngle: arcOffset + padRad + Math.PI })
.attr('d', arc2)
chart.append('path').attr('class', "arc chart-third")
.attr('id', 'text_arc')
.datum({ startAngle: arcOffset - 0.15, endAngle: arcOffset + Math.PI + 0.15 })
.attr('d', arc3)
var arc_text = chart.append('text')
.attr('id', 'scale10')
.attr("font-size", 15)
.style("fill", "#000000")
.attr('text-anchor', 'start')
arc_text.append('textPath')
.attr('startOffset','0%')
.attr('xlink:href', '#text_arc' )
var dataset = [{
metric: name,
value: value
}]
var texts = svg.selectAll("text")
.data(dataset)
.enter();
texts.append("text")
.text(function() {
return dataset[0].metric;
})
.attr('id', "Name")
.attr('transform', "translate(" + ((width + margin.left) / 2) + ", " + ((height + margin.top) / 1.5) + ")")
.attr("font-size", 25)
.style("fill", "#000000");
texts.append("text")
.text(function() {
return dataset[0].value + "%";
})
.attr('id', "Value")
.attr('transform', "translate(" + ((width + margin.left) / 1.4) + ", " + ((height + margin.top) / 1.5) + ")")
.attr("font-size", 25)
.style("fill", "#000000");
texts.append("text")
.text(function() {
return 0 + "%";
})
.attr('id', 'scale0')
.attr('transform', "translate(" + ((width + margin.left) / 100) + ", " + ((height + margin.top) / 2) + ")")
.attr("font-size", 15)
.style("fill", "#000000");
texts.append("text")
.text(function() {
return gaugeMaxValue + "%";
})
.attr('id', 'scale20')
.attr('transform', "translate(" + ((width + margin.left) / 1.08) + ", " + ((height + margin.top) / 2) + ")")
.attr("font-size", 15)
.style("fill", "#000000");
repaintGauge = function(perc) {
var current = Math.PI * perc / 100 + arcOffset
var t = d3.transition().duration(500)
chart.select(".chart-first")
.transition(t)
.attrTween('d', arcEndTween(current, arc1));
chart.select(".chart-second")
.transition(t)
.attrTween('d', arcStartTween(current, arc2));
chart.select(".chart-third")
.transition(t)
.attrTween('d', arcStartTween(current, arc3) );
arc_text.select('textPath')
.text( perc.toFixed(1) + '%')
}
function arcStartTween(newAngle, arc) {
return function(d) {
var interpolate = d3.interpolate(d.startAngle, newAngle);
return function(t) {
d.startAngle = interpolate(t);
return arc(d);
};
};
}
function arcEndTween(newAngle, arc) {
return function(d) {
var interpolate = d3.interpolate(d.endAngle, newAngle);
return function(t) {
d.endAngle = interpolate(t);
return arc(d);
};
};
}
/////////
var Needle = (function() {
//Helper function that returns the `d` value for moving the needle
var recalcPointerPos = function(perc) {
var centerX, centerY, leftX, leftY, rightX, rightY, thetaRad, topX, topY;
thetaRad = percToRad(perc / 2);
centerX = 0;
centerY = 0;
topX = centerX - this.len * Math.cos(thetaRad);
topY = centerY - this.len * Math.sin(thetaRad);
leftX = centerX - this.radius * Math.cos(thetaRad - Math.PI / 2);
leftY = centerY - this.radius * Math.sin(thetaRad - Math.PI / 2);
rightX = centerX - this.radius * Math.cos(thetaRad + Math.PI / 2);
rightY = centerY - this.radius * Math.sin(thetaRad + Math.PI / 2);
return "M " + leftX + " " + leftY + " L " + topX + " " + topY + " L " + rightX + " " + rightY;
};
function Needle(el) {
this.el = el;
this.len = width / 2.5;
this.radius = this.len / 8;
}
Needle.prototype.render = function() {
this.el.append('circle').attr('class', 'needle-center').attr('cx', 0).attr('cy', 0).attr('r', this.radius);
return this.el.append('path').attr('class', 'needle').attr('id', 'client-needle').attr('d', recalcPointerPos.call(this, 0));
};
Needle.prototype.moveTo = function(perc) {
var self,
oldValue = this.perc || 0;
this.perc = perc;
self = this;
// Reset pointer position
this.el.transition().delay(100).ease('quad').duration(200).select('.needle').tween('reset-progress', function() {
return function(percentOfPercent) {
var progress = (1 - percentOfPercent) * oldValue;
return d3.select(this).attr('d', recalcPointerPos.call(self, progress));
};
});
this.el.transition().delay(300).ease('bounce').duration(1500).select('.needle').tween('progress', function() {
return function(percentOfPercent) {
var progress = percentOfPercent * perc;
return d3.select(this).attr('d', recalcPointerPos.call(self, progress));
};
});
};
return Needle;
})();
setInterval(function() {
repaintGauge( Math.floor(Math.random() * 100) )
}, 1500);
needle = new Needle(chart);
needle.render();
needle.moveTo(percent);
})();
.chart-gauge
{
width: 400px;
margin: 100px auto
}
.chart-first
{
fill: #66AB8C;
}
.chart-second
{
fill: #ff533d;
}
.needle, .needle-center
{
fill: #000000;
}
.text {
color: "#112864";
font-size: 16px;
}
svg {
font: 10px sans-serif;
}
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.0.0/d3.min.js"></script>
<div class="chart-gauge"></div>
</body>
</html>

Fill not working with areaRadial() generated shape

I'm currently working on a thing which is supposed to look like a lava lamp later. Unfortunately, I'm already failing at the beginning:
I managed to create randomly generated blobs, but I just can't apply a fill. The stroke works just fine.
here is the relevant code which creates the blobs:
var ctx = this; // context
this.path = d3.areaRadial()
.angle(function(d) {return d.theta})
.radius(function(d) {return d.r})
.curve(d3.curveCatmullRomClosed.alpha(1));
// create the blob
this.create = function() {
var anchors = [];
for (var i = 0; i < ctx.options.anchors; i++) {
var currTheta = i * (360 / ctx.options.anchors) + rand(ctx.options.spread.theta);
var currRadians = Math.radians(currTheta);
var currRadius = ctx.options.radius + rand(ctx.options.spread.r);
anchors.push({theta: currRadians, r: currRadius});
}
var pathData = ctx.path(anchors);
d3.select(ctx.options.target).append('path')
.attr('d', pathData)
.attr('class', 'blob')
.style('opacity', ctx.options.opacity)
.style('transform', 'translate(' + ctx.x + 'px, ' + ctx.y + 'px)')
.attr('transform', 'translate(' + ctx.x + 'px, ' + ctx.y + 'px)');
console.log(pathData);
}
function rand(x) // creates a random number between -0.5 * x and 0.5 * x
Full code: https://codepen.io/normanwink/pen/BrMVrE
Thanks!
That's the expected behaviour, since you're using radius, which is:
Equivalent to area.y, except the accessor returns the radius: the distance from the origin ⟨0,0⟩.
And for area.y:
If y is specified, sets y0 to y and y1 to null and returns this area generator. If y is not specified, returns the current y0 accessor. (emphasis mine)
Therefore, you probably want outerRadius here.
this.path = d3.areaRadial()
.angle(function(d) {
return d.theta
})
.outerRadius(function(d) {
return d.r
})
.curve(d3.curveCatmullRomClosed.alpha(1));
This is your code with that change only:
(function() {
"use strict";
// window size
var windowX = window.innerWidth;
var windowY = window.innerHeight;
var svgX = windowX;
var svgY = windowY;
var blobCount = 1;
function init() {
// console.log(new Blob());
var svg = d3.select('#svg')
.attr('viewBox', '0 0 ' + svgX + ' ' + svgY)
.attr('preserveAspectRatio', 'xMinYMin meet');
for (var i = 0; i < blobCount; i++) {
var newBlob = new Blob(svgX * 0.5, svgY * 0.5);
}
}
function Blob(x, y) {
var ctx = this; // context
this.options = {
anchors: 8,
breathe: 30,
fill: '#ffffff',
opacity: 0.5,
radius: 150,
spread: {
theta: 10, // angle
r: 300 // radius
},
target: '#svg',
};
this.x = x || 0;
this.y = y || 0;
this.path = d3.areaRadial()
.angle(function(d) {return d.theta})
.outerRadius(function(d) {return d.r})
.curve(d3.curveCatmullRomClosed.alpha(1));
// create the blob
this.create = function() {
var anchors = [];
for (var i = 0; i < ctx.options.anchors; i++) {
var currTheta = i * (360 / ctx.options.anchors) + rand(ctx.options.spread.theta);
var currRadians = Math.radians(currTheta);
var currRadius = ctx.options.radius + rand(ctx.options.spread.r);
anchors.push({theta: currRadians, r: currRadius});
}
var pathData = ctx.path(anchors);
d3.select(ctx.options.target).append('path')
.attr('d', pathData)
.attr('class', 'blob')
.style('opacity', ctx.options.opacity)
.style('transform', 'translate(' + ctx.x + 'px, ' + ctx.y + 'px)')
.attr('transform', 'translate(' + ctx.x + 'px, ' + ctx.y + 'px)');
}
// update position and anchor movement
this.update = function() {
}
// apply changes
this.render = function() {
}
this.create();
}
function rand(i) {
return (Math.random()-0.5) * (i || 1);
}
Math.radians = function(degrees) {
return degrees * Math.PI / 180;
};
// init when ready
init();
})();
#svg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
}
.blob {
fill: blue; /* why is this not working? */
stroke: red;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
<svg id="svg"></svg>
Your problem is your pathData , because if you replace your .attr('d', pathData) with .attr('d', 'M37,17v15H14V17z M50,0H0v50h50z') which is a valid path, your fill is working properly.
I will continue searching the issue, just want to give you a hint, maybe you will find the issue faster then me. :)

d3.js zoom transition with map tiles

I'm trying to implement a zoom transition on a map that uses tiles. I want to zoom in on one location, then go back and forth between two other locations.
This is the example I'm working from: http://bl.ocks.org/mbostock/6242308
That does what I want, only I wish to use map tiles instead of a topojson or geojson file.
Right now the map is calling the final location in the jump function, but the transitions between locations aren't working. Any idea on what's happening would be greatly welcomed-- nothing showing in console and I'm a bit stuck. Many thanks.
My code is below, and it's also in this plunker.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
margin: 0;
}
#container {
position: relative;
overflow: hidden;
}
#map{
width:100%;
height:100%;
}
.layer {
position: absolute;
}
.tile {
pointer-events: none;
position: absolute;
width: 256px;
height: 256px;
}
</style>
<body>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/d3.geo.tile.v0.min.js"></script>
<div id="canvas">
<div id="container">
<g id="map">
<div class="layer"></div>
</div>
</div>
</div>
<script>
var width = Math.max(960, window.innerWidth),
height = Math.max(500, window.innerHeight),
prefix = prefixMatch(["webkit", "ms", "Moz", "O"]);
var tile = d3.geo.tile()
.size([width, height]);
var sf = [-122.417, 37.775],
belowsf = [-122.510962, 37.580284],
ny = [-74.0064, 40.7142],
brooklyn = [-73.975536, 40.691674];
var projection = d3.geo.mercator()
.scale(200000)
.translate([-width / 2, -height / 2]); // just temporary
var zoom = d3.behavior.zoom();
var canvas = d3.select("#canvas")
.style("width", width + "px")
.style("height", height + "px");
var container = d3.select("#container")
.style("width", width + "px")
.style("height", height + "px");
canvas
.call(zoomTo(ny,200300).event)
.transition()
.duration(20000)
.each(jump);
var map = d3.select("#map");
var layer = d3.select(".layer");
function zoomTo(location, newscale) {
return zoom
.scale((newscale) * 2 * Math.PI)
.translate(projection(location).map(function(x) { return -x; }));
}
function jump() {
var t = d3.select(this);
(function repeat() {
t = t.transition()
.call(zoomTo(sf, 200000).event)
.transition()
.call(zoomTo(belowsf, 200000).event)
.each("end", repeat);
})();
}
zoomed();
function zoomed() {
var tiles = tile
.scale(zoom.scale())
.translate(zoom.translate())
();
projection
.scale(zoom.scale() / 2 / Math.PI)
.translate(zoom.translate());
var image = layer
.style(prefix + "transform", matrix3d(tiles.scale, tiles.translate))
.selectAll(".tile")
.data(tiles, function(d) { return d; });
image.exit()
.remove();
image.enter().append("img")
.attr("class", "tile")
.attr("src", function(d) { return "http://" + ["a", "b", "c"][Math.random() * 3 | 0] + ".basemaps.cartocdn.com/light_all/" + d[2] + "/" + d[0] + "/" + d[1] + ".png"; })
.style("left", function(d) { return (d[0] << 8) + "px"; })
.style("top", function(d) { return (d[1] << 8) + "px"; });
}
function matrix3d(scale, translate) {
var k = scale / 256, r = scale % 1 ? Number : Math.round;
return "matrix3d(" + [k, 0, 0, 0, 0, k, 0, 0, 0, 0, k, 0, r(translate[0] * scale), r(translate[1] * scale), 0, 1 ] + ")";
}
function prefixMatch(p) {
var i = -1, n = p.length, s = document.body.style;
while (++i < n) if (p[i] + "Transform" in s) return "-" + p[i].toLowerCase() + "-";
return "";
}
function formatLocation(p, k) {
var format = d3.format("." + Math.floor(Math.log(k) / 2 - 2) + "f");
return (p[1] < 0 ? format(-p[1]) + "°S" : format(p[1]) + "°N") + " "
+ (p[0] < 0 ? format(-p[0]) + "°W" : format(p[0]) + "°E");
}
</script>
</body>
</html>
After a lot of headscratching it turned out that the main thing was that you didn't actually tell the zoom behaviour to call the zoomed function:
var zoom = d3.behavior.zoom().on("zoom", zoomed);
This required moving some variable definitions such that everything is defined when this is called. Apart from that, you don't need to adjust the projection in the zoom handler, as the zoom of the tiles is achieved by setting a transform on the container.
Complete demo here.

D3.js Gauge look like Telerik Radial Gauge

Hello I need help to add gray (Minor and Major) graduation lines inside the gauge to capture more precisely the position of the needle. Also if you have any idea how we could put text to Major graduation lines.
For my sample I would required 7 Major graduation lines divided by 10 minor separations (to look exactly like the Telerik Gauge).
Here is my implementation : http://jsfiddle.net/8svg5/
<html>
<head runat="server">
<title></title>
</head>
<body>
<div>
<a id="myTooltip" title="This is my message"></a>
<div id="svgTarget"></div>
</div>
<script>
$(function () {
var gaugeRanges = [
{
From: 1.5,
To: 2.5,
Color: "#8dcb2a"
}, {
From: 2.5,
To: 3.5,
Color: "#ffc700"
}, {
From: 3.5,
To: 4.5,
Color: "#ff7a00"
},
{
From: 4.5,
To: 6,
Color: "#c20000"
}];
$("#svgTarget").mttD3Gauge({ data: gaugeRanges });
});
</script>
</body>
</html>
(function ($) {
$.fn.mttD3Gauge = function (options) {
var settings = $.extend({
width: 300,
innerRadius: 130,
outterRadius: 145,
data: []
}, options);
this.create = function () {
this.html("<svg class='mtt-svgClock' width='" + settings.width + "' height='" + settings.width + "'></svg>");
var maxLimit = 0;
var minLimit = 9999999;
var d3DataSource = [];
var d3TickSource = [];
//Data Genration
$.each(settings.data, function (index, value) {
d3DataSource.push([value.From, value.To, value.Color]);
if (value.To > maxLimit) maxLimit = value.To;
if (value.From < minLimit) minLimit = value.From;
});
if (minLimit > 0) {
d3DataSource.push([0, minLimit, "#d7d7d7"]);
}
var pi = Math.PI;
//Control Genration
var vis = d3.select(this.selector + " .mtt-svgClock");
var translate = "translate(" + settings.width / 2 + "," + settings.width / 2 + ")";
var cScale = d3.scale.linear().domain([0, maxLimit]).range([-120 * (pi / 180), 120 * (pi / 180)]);
var arc = d3.svg.arc()
.innerRadius(settings.innerRadius)
.outerRadius(settings.outterRadius)
.startAngle(function (d) { return cScale(d[0]); })
.endAngle(function (d) { return cScale(d[1]); });
var tickArc = d3.svg.arc()
.innerRadius(settings.innerRadius - 20)
.outerRadius(settings.innerRadius - 2)
.startAngle(function (d) { return cScale(d[0]); })
.endAngle(function (d) { return cScale(d[1]); });
for (var i = 0; i < 10; i++) {
var point = (i * maxLimit) / 10.0;
d3TickSource.push([point, point +1, "#d7d7d7"]);
}
vis.selectAll("path")
.data(d3DataSource)
.enter()
.append("path")
.attr("d", arc)
.style("fill", function (d) { return d[2]; })
.attr("transform", translate);
return this;
};
return this.create();
};
}(jQuery));
enter code here
Here is the link of the page of what i'm trying to achieve with D3.js
http://demos.telerik.com/aspnet-ajax/gauge/examples/types/radialgauge/defaultcs.aspx?#qsf-demo-source
Any help will be greatly appreciated!
I finally found the way to add graduation, feel free to use this code
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
<script src="Scripts/jquery-1.9.1.js"></script>
<script src="Scripts/html5shiv.js"></script>
<script src="Scripts/d3.v3.min.js"></script>
<script src="Scripts/mtt-D3Gauge.js"></script>
</head>
<body>
<div>
<a id="myTooltip" title="This is my message"></a>
<div id="svgTarget"></div>
</div>
<script>
$(function () {
var gaugeRanges = [
{
From: 1.5,
To: 2.5,
Color: "#8dcb2a"
}, {
From: 2.5,
To: 3.5,
Color: "#ffc700"
}, {
From: 3.5,
To: 4.5,
Color: "#ff7a00"
},
{
From: 4.5,
To: 6,
Color: "#c20000"
}];
$("#svgTarget").mttD3Gauge({ data: gaugeRanges });
});
</script>
</body>
</html>
(function ($) {
$.fn.mttD3Gauge = function (options) {
var vis;
var settings = $.extend({
width: 300,
innerRadius: 130,
outterRadius: 145,
majorGraduations: 6,
minorGraduations: 10,
majorGraduationLenght: 16,
minorGraduationLenght: 10,
majorGraduationMarginTop: 7,
majorGraduationColor: "rgb(234,234,234)",
minorGraduationColor:"rgb(234,234,234)",
data: []
}, options);
this.create = function () {
this.html("<svg class='mtt-svgClock' width='" + settings.width + "' height='" + settings.width + "'></svg>");
var maxLimit = 0;
var minLimit = 9999999;
var d3DataSource = [];
//Data Genration
$.each(settings.data, function (index, value) {
d3DataSource.push([value.From, value.To, value.Color]);
if (value.To > maxLimit) maxLimit = value.To;
if (value.From < minLimit) minLimit = value.From;
});
if (minLimit > 0) {
d3DataSource.push([0, minLimit, "#d7d7d7"]);
}
//Render Gauge Color Area
vis = d3.select(this.selector + " .mtt-svgClock");
var translate = "translate(" + settings.width / 2 + "," + settings.width / 2 + ")";
var cScale = d3.scale.linear().domain([0, maxLimit]).range([-120 * (Math.PI / 180), 120 * (Math.PI / 180)]);
var arc = d3.svg.arc()
.innerRadius(settings.innerRadius)
.outerRadius(settings.outterRadius)
.startAngle(function (d) { return cScale(d[0]); })
.endAngle(function (d) { return cScale(d[1]); });
vis.selectAll("path")
.data(d3DataSource)
.enter()
.append("path")
.attr("d", arc)
.style("fill", function (d) { return d[2]; })
.attr("transform", translate);
renderMajorGraduations();
return this;
};
var renderMinorGraduations = function (majorGraduationsAngles, indexMajor) {
var graduationsAngles = [];
if (indexMajor > 0) {
var minScale = majorGraduationsAngles[indexMajor - 1];
var maxScale = majorGraduationsAngles[indexMajor];
var scaleRange = maxScale - minScale;
for (var i = 1; i < settings.minorGraduations; i++) {
var scaleValue = minScale + i * scaleRange / settings.minorGraduations;
graduationsAngles.push(scaleValue);
}
var xCenter = settings.width / 2;
var yCenter = settings.width / 2;
//Render Minor Graduations
$.each(graduationsAngles, function (indexMinor, value) {
var cos1Adj = Math.round(Math.cos((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop - settings.minorGraduationLenght));
var sin1Adj = Math.round(Math.sin((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop - settings.minorGraduationLenght));
var cos2Adj = Math.round(Math.cos((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop));
var sin2Adj = Math.round(Math.sin((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop));
var x1 = xCenter + cos1Adj;
var y1 = yCenter + sin1Adj * -1;
var x2 = xCenter + cos2Adj;
var y2 = yCenter + sin2Adj * -1;
vis.append("svg:line")
.attr("x1", x1)
.attr("y1", y1)
.attr("x2", x2)
.attr("y2", y2)
.style("stroke", settings.majorGraduationColor);
});
}
};
var renderMajorGraduations = function () {
var scaleRange = 240;
var minScale = -120;
var graduationsAngles = [];
for (var i = 0; i <= settings.majorGraduations; i++) {
var scaleValue = minScale + i * scaleRange / settings.majorGraduations;
graduationsAngles.push(scaleValue);
}
var xCenter = settings.width / 2;
var yCenter = settings.width / 2;
//Render Major Graduations
$.each(graduationsAngles, function (index, value) {
var cos1Adj = Math.round(Math.cos((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop - settings.majorGraduationLenght));
var sin1Adj = Math.round(Math.sin((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop - settings.majorGraduationLenght));
var cos2Adj = Math.round(Math.cos((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop));
var sin2Adj = Math.round(Math.sin((90 - value) * Math.PI / 180) * (settings.innerRadius - settings.majorGraduationMarginTop));
var x1 = xCenter + cos1Adj;
var y1 = yCenter + sin1Adj * -1;
var x2 = xCenter + cos2Adj;
var y2 = yCenter + sin2Adj * -1;
vis.append("svg:line")
.attr("x1", x1)
.attr("y1", y1)
.attr("x2", x2)
.attr("y2", y2)
.style("stroke", settings.majorGraduationColor);
renderMinorGraduations(graduationsAngles, index);
});
};
return this.create();
};
}(jQuery));

Resources