I have made a piechart in javascript and I'm now trying to bring it inside a react component.
However, it only draws a single slice and that one isn't even complete it's cutoff.
Here are images of the cut off slice from the react component and how it looks just rendering in html.
I am not sure why.
This is my function drawing it:
function drawDonut(inputData: DealNumbers[]) {
const radius = Math.min(width, height) / 2;
const color = d3.scaleOrdinal(d3.schemeCategory10);
const arc = d3.arc()
.outerRadius(radius - margin.top)
.innerRadius(radius - radius * 0.5);
const pie = d3
.pie()
.padAngle(0)
.value((d) => d.deals);
svg
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', `translate(${(width / 2) + margin.left}, ${(height / 2) + margin.top})`);
svg
.selectAll('.arc')
.data(pie(inputData))
.enter()
.append('g')
.attr('class', 'arc')
.append('path')
.attr('d', arc)
.style('fill', (d) => color(d.data.name));
It is pretty much the same except that I'm using a svg view box and I pass width and height as props instead of hardcoding it.
It draws perfectly fine from a javascript.
Thanks for any hints and tips you are able and take the time to give!
Best Regards,
Oliver
The issue was with the view box (moving the origin from 0,0 to -width, -height worked), a workaround for some reason I have had to copy intermediate variables
const pwidth = width
const pheight = height
and use these instead of width. Then I don't need to adjust the viewboxs origin. Which is a bit strange to say the least.
I won't accept this answer here as I have no idea why.
But adding it so someone else may find an answer if they're looking for it.
I want to make a line chart to zoom/pan in sync with multiple web pages.
These client has same Javascript and HTML source.
User zooms or pan on client A, message which is day time of domain of data is sent to the other and sender(blue line on above fig), and graph of received clients will be change simultaneously . Of course, other clients can do the same.
It is similar like a chat application.
Zoom function is:
function zoomed() {
let msg = [];
let t = d3.event.transform; //1)
msg[0] = t.rescaleX(x2).domain()[0].toString(); //2)
msg[1] = t.rescaleX(x2).domain()[1].toString(); //2)
sendMessage(msg); //3)
}
d3.event.transform catches mouse event.
convert to date time and strings.
send new scale domain to server.
Server sends received data to all clients:
function passiveZoom(rcv){
let leftend;
let rightend;
leftend = new Date(rcv[0]);
rightend = new Date(rcv[1]);
x.domain([leftend, rightend]);
svg.select(".line").attr("d", valueline);
svg.select(".axis").call(xAxis);
}
Received message from server which contain new day time.
set new domain,
update the line charts.
With this it is possible to zoom|pan all the line charts.
However, it does not work as required.
If I zoom|pan in client A, client B and client C will be changed. That is ok.
Next, I zoom|pan on client C(orange line on above figure), All graphs change to initial scale and position. Why!?
I assume that the mouse coordinates are not sent to the clients, but how should I handle it when I send the position coordinates of the mouse?
The Zoom|Pan process is forked from mbostock's block: Brush & Zoom. The sender also changes the range of the X2 domain with t.rescalex (x2).domain().
Since X2 is not used in the drawing, I changed X to x2, but I can only zoom in. I do not understand the meaning of X2.
Would you please let me know how to synchronize all of clients?
And what is x2?
This code is for clients forked from Simple line graph with v4.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
/* set the CSS */
body {
font: 12px Arial;
}
path {
stroke: steelblue;
stroke-width: 2;
fill: none;
}
.zoom {
cursor: move;
fill: none;
pointer-events: all;
}
.axis path,
.axis line {
fill: none;
stroke: grey;
stroke-width: 1;
shape-rendering: crispEdges;
}
</style>
<body>
<!-- load the d3.js library -->
<script src="http://d3js.org/d3.v4.min.js"></script>
<script src="socket.io.js"></script>
<script>
//--- Network----
let rcvT;
let socket = io.connect('http://localhost:3000');
//Recive event from server
socket.on("connect", function() {});
socket.on("disconnect", function(client) {});
socket.on("S_to_C_message", function(data) {
rcvT = data.value;
passiveZoom(rcvT);
});
socket.on("S_to_C_broadcast", function(data) {
console.log("Rcv broadcast " + data.value);
rcvT = data.value;
passiveZoom(rcvT);
});
function sendMessage(msg) {
socket.emit("C_to_S_message", { value: msg }); //send to server
}
function sendBroadcast(msg) {
socket.emit("C_to_S_broadcast", { value: msg }); // send to server
}
// --------------------
// Set the dimensions of the canvas / graph
var margin = { top: 30, right: 20, bottom: 30, left: 50 },
width = 600 - margin.left - margin.right,
height = 270 - margin.top - margin.bottom;
// Parse the date / time
var parseDate = d3.timeParse("%d-%b-%y");
// Set the ranges
var x = d3.scaleTime().range([0, width]);
var y = d3.scaleTime().range([height, 0]);
var x2 = d3.scaleTime().range([0, width]);
xAxis = d3.axisBottom(x)
.tickFormat(d3.timeFormat('%d-%b-%y'))
.ticks(5);
// var yAxis = d3.svg.axis().scale(y)
// .orient("left").ticks(5);
yAxis = d3.axisLeft(y);
// Define the line
var valueline = d3.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.close); });
// Adds the svg canvas
var svg = d3.select("body")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")");
// Get the data
d3.csv("data.csv", function(error, data) {
data.forEach(function(d) {
d.date = parseDate(d.date);
d.close = +d.close;
});
// Scale the range of the data
x.domain(d3.extent(data, function(d) { return d.date; }));
x2.domain(x.domain());
y.domain([0, d3.max(data, function(d) { return d.close; })]);
// Add the valueline path.
svg.append("path")
.data([data])
.attr("class", "line")
.attr("d", valueline);
// Add the X Axis
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
// Add the Y Axis
svg.append("g")
.attr("class", "y axis")
.call(yAxis);
});
//follow is zoom method------------------
zoom = d3.zoom()
.scaleExtent([1, 45])
.translateExtent([
[0, 0],
[width, height]
])
.extent([
[0, 0],
[width, height]
])
.on("zoom", zoomed);
svg.append("rect")
.attr("class", "zoom")
.attr("width", width)
.attr("height", height)
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(zoom);
function zoomed() {
let msg = [];
let t = d3.event.transform;
msg[0] = t.rescaleX(x2).domain()[0].toString();
msg[1] = t.rescaleX(x2).domain()[1].toString();
sendMessage(msg);
}
function passiveZoom(rcv){
let start;
let end;
start = new Date(rcv[0]);
end = new Date(rcv[1]);
x.domain([start, end]);
svg.select(".line").attr("d", valueline);
svg.select(".axis").call(xAxis);
}
</script>
</body>
If you try this code, you should exec in a few bowser windows, and run this node.js script.
var http = require("http");
var socketio = require("socket.io");
var fs = require("fs");
console.log("reflector start");
var server = http.createServer(function(req, res) {
res.writeHead(200, {"Content-Type":"text/html"});
var output = fs.readFileSync("./index.html", "utf-8");
res.end(output);
}).listen(process.env.VMC_APP_PORT || 3000);
var io = socketio.listen(server);
io.sockets.on("connection", function (socket) {
// send message to all
socket.on("C_to_S_message", function (data) {
io.sockets.emit("S_to_C_message", {value:data.value});
console.log("MSG "+data.value);
});
// boradcast send to all without sender
socket.on("C_to_S_broadcast", function (data) {
socket.broadcast.emit("S_to_C_broadcast", {value:data.value});
});
// disconnection
socket.on("disconnect", function () {
console.log("disconnect");
});
});
Assuming I understand the problem,
The (first) problem is that you are not updating (the) zoom itself.
Where d3.zoom is used, it often just keeps track of current zoom state rather than applying a transform on a container directly. In the brush and zoom example, the zoom is applied by re-scaling the data - not by applying an SVG transform to the container. Using that example, we can see that when we brush, we also call:
svg.select(".zoom").call(zoom.transform, someZoomTransform);
This:
updates the zoom state/identity as tracked by the zoom variable
emits a zoom event, which invokes the zoomed function (which in the brush and zoom example is ignored if a brush triggers it)
If we remove this line, changes in scale state made by brushing don't update the zoom. Brush to a very small domain, then zoom in and see here.
This is the case in your code, when you update the chart with the zoomed function and d3.event.transform you aren't updating the zoom state. You are updating the scales - but zoom is not updated.
Below I'll demonstrate using one zoom to update another. Note: if each zoomed function calls the others, we'll enter an infinite loop. With brush and zoom we could see if the trigger was a brush to see if the zoomed function was needed, below I use d3.event.sourceEvent.target to see if the other zoomed functions need to propagate the zoom:
var svg = d3.select("svg");
var size = 100;
var zoom1 = d3.zoom().scaleExtent([0.25,4]).on("zoom", zoomed1);
var zoom2 = d3.zoom().scaleExtent([0.25,4]).on("zoom", zoomed2);
var rect1 = svg.append("rect")
.attr("width", size)
.attr("height", size)
.attr("x", 10)
.attr("y", 10)
.call(zoom1);
var rect2 = svg.append("rect")
.attr("width", size)
.attr("height", size)
.attr("x", 300)
.attr("y", 10)
.call(zoom2);
function zoomed1() {
var t = d3.event.transform;
var k = Math.sqrt(t.k);
rect1.attr("width",size/k).attr("height",size*k);
if(d3.event.sourceEvent.target == this) {
rect2.call(zoom2.transform,t);
}
}
function zoomed2() {
var t = d3.event.transform;
var k = Math.sqrt(t.k);
rect2.attr("width",size/k).attr("height",size*k);
if(d3.event.sourceEvent.target == this) {
rect1.call(zoom2.transform,t);
}
}
rect {
cursor: pointer;
stroke: #ccc;
stroke-width: 10;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Zoom on one rectangle to update the other.
<svg width="600" height="300"></svg>
You might wonder why I hard coded size, why don't I just modify the current size, rather than the original. The answer is that the zoom transform scale is the scale relative to the original state - not the last state. For example, if scale doubles each zoom, and we zoom in 2 times, the scale goes from: k=1 → k=2 → k=4. If we multiply the current size of a shape by the new scale, we get size=1 → size=2 → size=8, this is not correct (and upon zooming out to k=2, we'll double the amount we zoom in, rather than zooming out). The transform is cumulative already, we don't want to apply it to a value that has a transform applied on it.
Applying the transform on a transformed value, rather than the original value, can lead to increasing zoom even when zooming out - this is probably why you have had trouble zooming out
So, this brings me to the second problem, x2. x2 is the reference, the original value. Yes, as Gerardo notes it is also the scale for the brush in your example, but more importantly, he states that this scale doesn't change. Because of this, x2 is well suited to be used as a reference scale, to which we can use to transform x given a zoom state:
x.domain(t.rescaleX(x2).domain());
What happens here? transform.rescaleX(x2) doesn't modify x2, it "returns a copy of the continuous scale x whose domain is transformed [given a zoom transformation].(docs)". We take the copy's domain and assign it to the x scale (range of course remains the same), and by doing so, apply the transform to the x scale. This is essentially the same as my snippet above with the square/rectangles, where I keep a reference value for the initial size of the shapes and apply the transform to this value.
Let's see this in action with a basic graph/plot with scales rather than plain shapes:
var svg = d3.select("svg");
var data = [[0,300],[1,20],[2,300]];
// Area generators:
var leftArea = d3.area().curve(d3.curveBasis)
.x(function(d) { return leftX(d[0]); })
var rightArea = d3.area().curve(d3.curveBasis)
.x(function(d) { return rightX(d[0]); })
// Scales
var leftX = d3.scaleLinear().domain([0,2]).range([0,250]);
var rightX = d3.scaleLinear().domain([0,2]).range([300,550]);
var leftX2 = leftX.copy();
var rightX2 = rightX.copy();
// Zooms
var leftZoom = d3.zoom().scaleExtent([0.25,4]).on("zoom", leftZoomed);
var rightZoom = d3.zoom().scaleExtent([0.25,4]).on("zoom", rightZoomed);
// Graphs
var leftGraph = svg.append("path")
.attr("d", leftArea(data))
.call(leftZoom);
var rightGraph = svg.append("path")
.attr("d", rightArea(data))
.call(rightZoom);
function leftZoomed() {
var t = d3.event.transform;
leftX.domain(t.rescaleX(leftX2).domain());
leftGraph.attr("d",leftArea(data));
if(d3.event.sourceEvent.target == this) {
rightGraph.call(rightZoom.transform,t);
}
}
function rightZoomed() {
var t = d3.event.transform;
rightX.domain(t.rescaleX(rightX2).domain());
rightGraph.attr("d",rightArea(data));
if(d3.event.sourceEvent.target == this) {
leftGraph.call(leftZoom.transform,t);
}
}
path {
cursor: pointer;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
Zoom on one plot to update the other (zoom on the path area itself)
<svg width="600" height="300"></svg>
Put simply, to synchronize multiple zoomable scaled graphs in one page or across clients, you should be:
updating each zoom with selection.call(zoom.transform,transform)
rescaling each scale using the current transform and a reference scale.
I haven't dug into trying this with multiple clients and sockets. But, the above should help in explaining how to approach the problem. However, with multiple clients, you might need to modify how I've stopped the infinite loop of zoom events, using or setting a property in the transform object might be the easiest. Also, as rioV8 notes, you should probably be passing the zoom parameters (or better yet, d3.event itself), not the domain, though a domain only option is possible.
With sockets, I did have some trouble in sending objects - I'm not familiar with socket.io, and didn't spend a tonne of time looking, but I got this to work with zoomed and passivezoom functions as so:
function zoomed() {
let t = d3.event.transform;
// 1. update the scale, same as in brush and zoom:
x.domain(t.rescaleX(x2).domain());
// 2. redraw the graph and axis, same as in brush and zoom:
path.attr("d", area); // where path is the graph
svg.select(".xaxis").call(xAxis);
// 3. Send the transform, if needed:
if(t.alreadySent == undefined) {
t.alreadySent = true; // custom property.
sendMessage([t.k,t.x,t.y,t.alreadySent]);
}
}
function passiveZoom(rcv){
// build a transform object (since I was unable to successfully transmit the transform)
var t = d3.zoomIdentity;
t.k = rcv[0];
t.x = rcv[1];
t.y = rcv[2];
t.alreadySent = rcv[3];
//trigger a zoom event (invoke zoomed function with new transform data).
rect.call(zoom.transform,t); // where rect is the selection that zoom is called on.
}
Rather than sending the event, I send the transform parameters (only) along with a flag to note that the zoom event that a passive zoom function triggers doesn't need to be passed onwards again. This is based in principle exactly on the above snippets.
No modification to server side script. Here's the client side that I used - it's more basic than your code, as I stripped out y scales, y axis, csv data source, etc.
I have a D3js map built with topojson.js.
var projection = d3.geo.mercator();
Everything works fine, but I am looking for an effect that I cannot manage to achieve. When, zooming the map I would like the pins over it to scale down, But if I scale it down, I need to recalculate their coordinates and I can't find the formula to do so.
Zoom handler
scale = d3.event.scale;
if (scale >= 1) {
main.attr("transform", "translate(" + d3.event.translate + ")
scale(" + d3.event.scale + ")");
}
else {
main.attr("transform", "translate(" + d3.event.translate + ")scale(1)");
}
//43x49 is the size initial pine size when scale = 1
var pins = main.select("#pins").selectAll("g").select("image")
.attr("width", function () {
return 43 - (43 * (scale - 1));
})
.attr("height", function () {
return 49 - (49 * (scale - 1));
})
.attr("x", function () {
//Calculate new image coordinates;
})
.attr("y", function () {
//Calculate new image coordinates;
});
My question is : how do I calculate x and y based on new scale?
I hope I am clear enough.
Thanks for your help
EDIT :
Calculation of initial pins coordinates :
"translate(" + (projection([d.lon, d.lat])[0] - 20) + ","
+ (projection([d.lon, d.lat])[1] - 45) + ")"
-20 and -45 to have the tip of the pin right on the target.
You need to "counter-scale" the pins, i.e. as main scales up/down you need to scale the pins down/up, in the opposite direction. The counter-scale factor is 1/scale, and you should apply it to each of the <g>s containing the pin image. This lets you remove your current width and height calculation from the <image> nodes (since the parent <g>'s scale will take care of it).
However, for this to work properly, you'll also need to remove the x and y attributes from the <image> and apply position via the counter-scaled parent <g> as well. This is necessary because if there's a local offset (which is the case when x and y are set on the <image>) that local offset gets scaled as the parent <g> is scaled, which makes the pin move to an incorrect location.
So:
var pinContainers = main.select("#pins").selectAll("g")
.attr("transform", function(d) {
var x = ... // don't know how you calculate x (since you didn't show it)
var y = ... // don't know how you calculate y
var unscale = 1/scale;
return "translate(" + x + " " + y + ") scale(" + unscale + ")";
})
pinContainers.select("image")
.attr("width", 43)
.attr("height", 49)
// you can use non-zero x and y to get the image to scale
// relative to some point other than the top-left of the
// image (such as the tip of the pin)
.attr("x", 0)
.attr("y", 0)
I am trying to build a bar graph that I can switch between the amount of data displayed based on a particular length of time. So far the code that I have is this,
var margin = {
top : 20,
right : 20,
bottom : 30,
left : 50
}, width = 960 - margin.left - margin.right, height = 500
- margin.top - margin.bottom;
var barGraph = function(json_data, type) {
if (type === 'month')
var barData = monthToArray(json_data);
else
var barData = dateToArray(json_data);
var y = d3.scale.linear().domain([ 0, Math.round(Math.max.apply(null,
Object.keys(barData).map(function(e) {
return barData[e]['Count']}))/100)*100 + 100]).range(
[ height, 0 ]);
var x = d3.scale.ordinal().rangeRoundBands([ 0, width ], .1)
.domain(d3.entries(barData).map(function(d) {
return barData[d.key].Date;
}));
var xAxis = d3.svg.axis().scale(x).orient("bottom");
var yAxis = d3.svg.axis().scale(y).orient("left");
var svg = d3.select("#chart").append("svg").attr("width",
width + margin.left + margin.right).attr("height",
height + margin.top + margin.bottom).append("g").attr(
"transform",
"translate(" + margin.left + "," + margin.top + ")");
svg.append("g").attr("class", "x axis").attr("transform",
"translate(0," + height + ")").call(xAxis);
svg.append("g").attr("class", "y axis").call(yAxis).append(
"text").attr("transform", "rotate(-90)").attr("y", 6)
.attr("dy", ".71em").style("text-anchor", "end").text(
"Total Hits");
svg.selectAll(".barComplete").data(d3.entries(barData)).enter()
.append("rect").attr("class", "barComplete").attr("x",
function(d) {
return x(barData[d.key].Date)
}).attr("width", x.rangeBand() / 2).attr("y",
function(d) {
return y(barData[d.key].Count);
}).attr("height", function(d) {
return height - y(barData[d.key].Count);
}).style("fill", "orange");
var bar = svg.selectAll(".barHits").data(d3.entries(barData))
.enter().append("rect").attr("class", "barHits").attr(
"x", function(d) {
return x(barData[d.key].Date) + x.rangeBand() / 2
}).attr("width", x.rangeBand() / 2).attr("y",
function(d) {
return y(barData[d.key].Count);
}).attr("height", function(d) {
return height - y(barData[d.key].Count);
}).style("fill", "red");
};
This does a great job displaying my original data set, I have a button set up to switch between the data sets which are all drawn at the beginning of the page. all of the arrays exist but no matter how hard I try I keep appending a new graph to the div so after the button click I have two graphs, I have tried replacing all of the code in the div, using the enter() exit() remove() functions but when using these I get an error stating that there is no exit() function I got this by following a post that someone else had posted here and another one here but to no avail. Any ideas guys?
What you are most likely doing is drawing a new graph probably with a new div each time the button is clicked, one thing you can try doing is to hold on to the charts container element somewhere, and when the button is clicked, you simply clear it's children and re-draw the graphs.
In practice, I almost never chain .data() and .enter().
// This is the syntax I prefer:
var join = svg.selectAll('rect').data(data)
var enter = join.enter()
/* For reference: */
// Selects all rects currently on the page
// #returns: selection
svg.selectAll('rect')
// Same as above but overwrites data of existing elements
// #returns: update selection
svg.selectAll('rect').data(data)
// Same as above, but now you can add rectangles not on the page
// #returns: enter selection
svg.selectAll('rect').data(data).enter()
Also important:
# selection.enter()
The enter selection merges into the update selection when you append or insert.
Okay, I figured it out So I will first direct all of your attention to here Where I found my answer. The key was in the way I was drawing the svg element, instead of replacing it with the new graph I was just continually drawing a new one and appending it to the #chart div. I found the answer that I directed you guys to and added this line in the beginning of my existing barGraph function.
d3.select("#barChart").select("svg").remove();
and it works like a charm, switches the graphs back and forth just as I imagined it would, now I have a few more tasks for the bargraph itself and I can move on to another project.
Thank you all for all of your help and maybe I can return the favor one day!
I have a bubble force chart application, and I've created various instances of it. However I can not seem to scale the bubbles in relation to the height/width of the svg.
http://jsfiddle.net/pPMqQ/138/
I've placed an inverse on the heights/widths to alter the viewport - but it doesn't feel like the right approach to me. Does anyone have any experience as to how to correct this?
var inverseHeight = 1/h * 100000;
var inverseWidth = 1/w * 100000;
var svg = d3.select(selector)
.append("svg")
.attr("class", "bubblechart")
.attr("width", parseInt(w + padding,10))
.attr("height", parseInt(h + padding,10))
.attr('viewBox', "0 0 "+parseInt(inverseWidth,10)+" "+parseInt(inverseHeight,10))
.attr('perserveAspectRatio', "xMinYMid")
.append("g")
.attr("transform", "translate(" + (w/4) + "," + (h/4) + ")");
I've tried to scale the actual bubbles
http://jsfiddle.net/pPMqQ/143/
var scale = methods.width*.005;
Its producing an ideal effect - but the chart is not always central.