I need to have two same group bar charts, one below another.
I just copied the code and defined two divs and two SVGs
I want to have this chart twice.
But I have this as a result.
Can someone tell me what is my problem?
HTML:
<div class="svg-div">
<svg id="svg1" width="1000" height="500"></svg>
<svg id="svg2" width="1000" height="500"></svg>
<script src = "barchart.js"> </script>
</div>
Javascript:
//SVG for the second chart
var svg2 = d3.select('#chart1').append("svg2").attr('width', 800).attr('height', 550),
margin = {top: 20, right: 20, bottom: 20, left: 40},
width = +svg2.attr("width") - margin.left - margin.right,
height = +svg2.attr("height") - margin.top - margin.bottom,
g = svg2.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
You have tons of duplicated variables here! That simply won't work.
The correct way to fix this is renaming all the variables (if the charts are substantially different) or creating a function that you call multiple times (if the charts are the same and only the data changes, for instance).
Meanwhile, here is a quick and lazy solution: I wrapped your two codes inside IIFEs:
(function chart1(){
//code here for chart1
}())
(function chart2(){
//code here for chart2
}())
Here is the plunker: https://plnkr.co/edit/IoTUkqrGWmunrSW9x9ls?p=preview
Again, this is not the correct way to fix this, I'm doing this only for you to see that variables are scoped. I suggest that you change the variables or, even better, if the code is the same for both charts, simply put all of it inside a function that you call twice (with the same arguments or with different arguments, that's up to you). Like this:
function draw(selector){
var svg = d3.select(selector).append("svg")
//etc...
}
draw("#chart1");
draw("#chart2");
Here is the corresponding plunker: https://plnkr.co/edit/7bZhmmMV1CjUgx7v59wY?p=preview
var svg2 = d3.select('#chart1').append("svg").attr('width', 800).attr('height', 550),
margin = {top: 20, right: 20, bottom: 20, left: 40},
width = +svg2.attr("width") - margin.left - margin.right,
height = +svg2.attr("height") - margin.top - margin.bottom,
g = svg2.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
I made a small change that should make it work. .append() needs a valid element so just append an svg.
Related
I have a reports page containing three D3 JS v3 plots and some tabular data. The page looks great while the window is maximized but when the user resizes the browser windows it starts to behave oddly i.e. each plot overflows the containing div etc.
After a bit of research I found that I just need to set the viewBox attribute of the root svg for each plot i.e.
var margin = {top: 40, right: 20, bottom: 20, left: 20},
width = 450 - margin.left - margin.right,
height = 370 - margin.top - margin.bottom;
var chartSvg = d3.select("#myPlotDivId")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.attr("viewBox", "0 0 " + (width + margin.left + margin.right) + " " + (height + margin.top + margin.bottom))
.attr("preserveAspectRatio", "xMidYMid meet")
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
This I can do within the specific AngularJS controller and is no problem. However, I also need to update all three plot svgs each time the window is resized. There is a nice JSFiddle demonstrating this:
var aspect = chartSvg.width() / chartSvg.height(),
container = chartSvg.parent();
$(window).on("resize", function() {
var targetWidth = container.width();
chartSvg.attr("width", targetWidth);
chartSvg.attr("height", Math.round(targetWidth / aspect));
}).trigger("resize");
However, I'd prefer to do this within the controller and the function from where I create the plots ... How can I do this? I would not like to have a Window event hook out in the page calling my controllers or creating a global directive just for this. Any ideas?
First of all, I hope you have some angular component/directive wrapping d3 code, if not - this is something you should do.
Then this component should accept width and height:
app.component('myd3Plot', ...
width: '<',
height: '<'
And in parent controller/component you have listener for window resize and adjust child plots sizes:
window.on('resize', function() {
vm.child1.height = ...
vm.child2.height = ...
})
<myd3-plot height="$ctrl.child1.height"...
<myd3-plot height="$ctrl.child2.height"...
Finally I did it as follows i.e. injecting the window component into my controller and defining an onresize event handler:
class ReportCtrl {
// 1) inject the window into my controller
constructor(private $window: IWindowService, ...) {
// etc ...
// 2) reusable function to resize a d3 js plot
var resizePlot = function (divId: string) {
var container = d3.select(divId);
var svg = container.select("svg");
var aspect = svg.attr("width") / svg.attr("height");
var targetWidth = container.node().getBoundingClientRect().width;
svg.attr("width", targetWidth);
svg.attr("height", Math.round(targetWidth / aspect));
};
// 3) hook on the window onresize event
$window.onresize = function (ev: UIEvent) {
resizePlot("#hist");
resizePlot("#scatter1");
resizePlot("#scatter2");
};
}
}
I'm new to D3.js and CodeMirror (and to StackOverflow as well, please pardon my mistakes) and I want to try some cool stuff for my newest project, so these two are my best bet but I had some problem. Please help me noticing what I'm doing wrong, and what the best solution to make this program work out. I'm glad we have good forum here.
So, long story short, I'm trying to build a visualization tools by using D3.js and CodeMirror as my editor. For example, this is what I try to visualize, but instead of reading the morley.csv data (hard-coded), I want to input the data from my CodeMirror editor. So this is what I write for the editor:
<textarea id="values"></textarea>
<script>
var editor = CodeMirror.fromTextArea(values, {
lineNumbers: true,
matchBrackets: true,
theme : '3024-day'
});
</script>
<a id="gen" class="btn btn-primary btn-block" role="button">Generate Visualization</a>
And this for the visualization builder:
<div class="row" id="chart">
<script>
d3.select("#gen").on("click", function(){
d3.event.preventDefault();
d3.select("#chart svg").remove();
var dataset = editor.getValue();
var margin = {top: 10, right: 50, bottom: 20, left: 50},
width = 120 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var min = Infinity,
max = -Infinity;
var chart = d3.box()
.whiskers(iqr(1.5))
.width(width)
.height(height);
d3.csv(dataset, function(error, csv) {
if (error) throw error;
var data = [];
csv.forEach(function(x) {
var e = Math.floor(x.Expt - 1),
r = Math.floor(x.Run - 1),
s = Math.floor(x.Speed),
d = data[e];
if (!d) d = data[e] = [s];
else d.push(s);
if (s > max) max = s;
if (s < min) min = s;
});
chart.domain([min, max]);
var svg = d3.select("#chart").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("class", "box")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.bottom + margin.top)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")")
.call(chart);
setInterval(function() {
svg.datum(randomize).call(chart.duration(1000));
}, 2000);
});
function randomize(d) {
if (!d.randomizer) d.randomizer = randomizer(d);
return d.map(d.randomizer);
}
function randomizer(d) {
var k = d3.max(d) * .02;
return function(d) {
return Math.max(min, Math.min(max, d + k * (Math.random() - .5)));
};
}
function iqr(k) {
return function(d, i) {
var q1 = d.quartiles[0],
q3 = d.quartiles[2],
iqr = (q3 - q1) * k,
i = -1,
j = d.length;
while (d[++i] < q1 - iqr);
while (d[--j] > q3 + iqr);
return [i, j];
};
}
});
</script>
</div>
Let's just assumed I used the same morley.csv for my input data.
Somehow it didn't work as I planned. I don't understand what I'm missing here, but my best guess it must have something to do with how d3.csv can't parse my input from textarea values. The output div chart is always show nothing..
Thanks for helping out, although I'm new here. Any help is more than appreciated.
1.
Change d3.csv() to d3.csv.parse() or d3.csvParse() in the newer versions. The former only accepts a file name, the latter accepts a string of CSV data.
Debug and make sure your dataset variable is truly a comma separated string.
2.
Also, if all that you need is a flat array like [1,2,3], I think you need to take all of that code out of the callback function and just do this:
data = d3.csv.parse(dataset);
Then just continue with your existing code after that.
The reason to have a callback function in the csv.parse call is only to structure the returned array; e.g. if your array elements are objects with multiple values inside of them, you'd set that up in the callback function.
I created a graph (based on this block https://bl.ocks.org/mbostock/1667367) on a resizable container. The problem is that when I resize the container, if the container is at a certain height, both graphs overlap. The ideia is to maintain the ratio and distance between the two graphs, whatever the size of the container is. Any ideas?
Code: http://pastebin.com/DJqYbe6G
Nailed it. It seems the combination between AngularJS and D3 give a hard time to access the svg elements. By accessing the parent graph height we can maintain a ratio between him and the child graph:
var parentHeigtht = angular.element($elem[0])[0].parentNode.clientHeight;
var margin = {top: 20, right: 10, bottom: 220, left: 40},
margin2 = {top: parentHeigtht-150, right: 10, bottom: 60, left: 40},
width = ($elem[0].parentNode.clientWidth) - margin.left - margin.right,
height = ($elem[0].parentNode.clientHeight) - (margin.top) - (margin.bottom),
height2 = ($elem[0].parentNode.clientHeight) - (margin2.top) - (margin2.bottom);
I modified a force diagram to change node circles into images, but would like some consistency with the way links are connected, like how a flowchart is. Similar to what is seen on this fiddle.
There may be something that needs to be modified in this code:
var forceLayout = d3.layout.force()
.nodes(nodes)
.links([])
.gravity(gravity)
.size([width, height])
.charge(function(d){
var charge = -500;
if (d.index === 0) charge = 10 * charge;
return charge;
});
The way that fiddle has it, the chargeand the linkDistance makes it look consistent, but changing the values to what's here doesn't help.
var force = d3.layout.force()
.charge(-200)
.linkDistance(50)
.size([width + margin.left + margin.right, height + margin.top + margin.bottom]);
Here's a link to my fiddle.
In your fiddle code, I set in the CSS part row 7 to width: 600px; instead of width: 80%;.
Also I added in the JavaScript, in row 118 a global variable
var width = 600
height = 800
and I made the .linkDistance(30)
Hope this helps you..
I have multiple D3 graphs on a cordova/phonegap page and want to have them scale to fit the horizontal / vertical screens. Adding attributes of "Viewbox" & "presereAspectRatio" did that great as long as I commented out the earlier width & hight attributes.
The graphs are just defined on the page as:
<div id="graph1"></div>
<div id="graph2"></div>
<div id="graph3"></div>
etc....
and work fine with the static attr's of "width" & "height"
But when I add the attribute "viewbox" they scale wonderfully, but are now spaced about ~15cm apart from each other causing you to have to scroll down the phone a long time to see them.
If I comment out the "viewbox" & "preserveAspectRatio" & uncomment the original static "width" & "height" attributes the graphs appear on the screen one after another as before. But they are static. I'm not sure what aspect within or outside each could be affected by this.
Below is the code I'm using for the select aspect, etc.
var margin = {
top: 40,
right: 20,
bottom: 35,
left: 40
},
width = 475 - margin.left - margin.right,
height = 205 - margin.top - margin.bottom;
var svg = d3.select("#graph1")
.append("svg")
.attr("viewBox","0 0 475 205")
.attr("preserveAspectRatio", "xMinYMin")
// .attr("width", width + margin.left + margin.right)
// .attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
Any thoughts on this would be greatly appreciated.
Thanks ;)
When you leave off the width and height, they get set to 100%, which means that the height will be 100% of your viewport. That means extra space gets added to the bottom of your svg. You can test this by creating a <rect> that is the size of your viewBox, and placing a border on the svg element.
HERE is an example. As you can see, there is extra space that is outside of the viewBox (not covered by the rect) that is part of the svg element.
Unfortunately, you're probably going to need to use a script to resize the svg's. You can create a function to set the height attribute based on the width of the container (perhaps the body element in your case) and the ratio of the height and width in your viewBox. Here's one way to do that:
function resizeAll() {
d3.selectAll('svg').call(scaleSvg);
}
function scaleSvg(sel) {
sel.each(function() {
// split the viewbox into its component parts
var vbArray = d3.select(this).attr('viewBox').split(' ');
// find the ratio of height to width
var heightWidthRatio = +vbArray[3] / +vbArray[2];
// get the width of the body (or you could use some other container)
var w = document.body.offsetWidth;
// set the width and height of the element
d3.select(this)
.attr('width', w)
.attr('height', w * heightWidthRatio);
});
}
Then you would simply call resizeAll() when the page loads, and when the window is resized.
HERE is an example.