Maintain ratio between two d3.js graphs - javascript

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);

Related

Centering and scaling canvas object inside another canvas object by width/height in Fabric.js

Goal: To center an object (horizontally and vertically) inside another object (rectangle or group) on canvas via Fabric.js or via Javascript which keeps the original object aspect ratio the same, but also not exceed the parent object width/height proportions?
The parent object (rectangle or group) won't be centered on the canvas element.
Here's the code I have so far: https://stackblitz.com/edit/angular-my8hky
My app.component.ts so far:
canvas: fabric.Canvas;
ngOnInit() {
this.canvas = new fabric.Canvas('canvas');
var rect = new fabric.Rect({
left: 100,
top: 50,
width: 300,
height: 200,
fill: '#eee'
});
this.canvas.add(rect);
fabric.Image.fromURL('https://angular.io/assets/images/logos/angular/logo-nav#2x.png', (img) => {
let bounds = rect.getBoundingRect();
let oImg = img.set({
left: bounds.left,
top: bounds.top,
width: rect.width
}).scale(1);
this.canvas.add(oImg).renderAll();
});
}
Not only is the new object not centered vertically, but also not centered horizontally if the rectangle object height is decreased (for example to 50px in height).
I realize I'm only declaring the inner image object width to be the same as the parent rectangle boundary width.
Current solution:
Parent rectangle width: 300 and height: 200:
Parent rectangle width: 300 and height: 50:
Desired solution:
Parent rectangle width: 300 and height: 200:
Parent rectangle width: 300 and height: 50:
Can anyone assist?
Figured it out.. Here's the StackBlitz:
https://stackblitz.com/edit/angular-pg4fis
The trick was to first add the rectangle to a Fabric group, like so:
var rect = new fabric.Rect({
left: 100,
top: 50,
width: 450,
height: 200,
fill: '#e3e3e3',
});
var rectGroup = new fabric.Group([rect], {
name: 'Rectangle',
});
this.canvas.add(rectGroup);
Then, I was able to establish the parent (group) element boundaries and use a variable scaling factor to set the image via Math functionality:
fabric.Image.fromURL('https://angular.io/assets/images/logos/angular/logo-nav#2x.png', (img) => {
let bounds = rectGroup.getBoundingRect();
const scaleFactor = Math.min(
Math.min(1, bounds.width / img.width),
Math.min(1, bounds.height / img.height)
);
img.scale(scaleFactor);
img.set({
top: bounds.top + Math.max(bounds.height - img.height * scaleFactor, 0)/2,
left: bounds.left + Math.max(bounds.width - img.width * scaleFactor, 0)/2,
});
rectGroup.addWithUpdate(img);
this.canvas.renderAll();
});

Cannot have multiple charts in one HTML page - d3

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.

Increasing d3 SVG container size

I am trying to increase the size of my SVG container dynamically so that it fits all the data. There is a fiddle which explains the dynamic increase of the SVG: http://jsfiddle.net/CKW5q/
However, the same concept doesn't work for a bi-directional sankey chart(d3). Following is the function called to expand the parentnode:
function expand(node) {
node.state = "expanded";
node.children.forEach(function (child) {
child.state = "collapsed";
child._parent = this;
child.parent = null;
containChildren(child);
}, node);
HEIGHT = HEIGHT + node.children.length * 10; //update height by 10px per child
WIDTH = WIDTH + node.children.length * 10;//update width
}
svg.attr("height", HEIGHT); // update svg height
This gives very odd results. It definitely increases the container, but unfortunately keeps the original SVG dimensions intact:
I suppose that the SVG HEIGHT and WIDTH being declared at the start of the script needs to be updated, which for some reasons am not able to. Any help will be appreciated.
You will need to do something like this:
var Chart = (function(window, d3) {
(init code that doesn't change on resize here)
render();
function render() {
updateDimensions();
(code that needs to be re-rendered on resize here)
}
function updateDimensions() {
margin = {top: 100, right: 40, bottom: 80, left: 80}; // example
width = window.innerWidth - margin.left - margin.right;
height = window.innerHeight - margin.top - margin.bottom;
}
return {
render: render
}
})(window, d3);
window.addEventListener('resize', Chart.render);

viewbox SVG attribute causes graphs to become spaced very far apart

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.

Javascript (jQuery) Scale an Image to Its Container's Center Point

This seems like it should be quite simple, but for some reason I can't quite wrap my brain around it. I have an image inside a "viewport" div, of which the overflow property is set to hidden.
I've implemented a simple zooming and panning with jQuery UI, however I am having trouble getting the zoom to appear to originate from the center of the viewport. I did a little screencast from Photoshop the effect I'm trying to reproduce: http://dl.dropbox.com/u/107346/share/reference-point-zoom.mov
In PS you can adjust the scaling reference point an the object will scale from that point. Obviously this is not possible with HTML/CSS/JS, so I'm trying to find the appropriate left and top CSS values to mimic the effect.
Here is the code in question, with a few unnecessary bits removed:
html
<div id="viewport">
<img id="map" src="http://dl.dropbox.com/u/107346/share/fake-map.png" alt="" />
</div>
<div id="zoom-control"></div>
javascript
$('#zoom-control').slider({
min: 300,
max: 1020,
value: 300,
step: 24,
slide: function(event, ui) {
var old_width = $('#map').width();
var new_width = ui.value;
var width_change = new_width - old_width;
$('#map').css({
width: new_width,
// this is where I'm stuck...
// dividing by 2 makes the map zoom
// from the center, but if I've panned
// the map to a different location I'd
// like that reference point to change.
// So instead of zooming relative to
// the map image center point, it would
// appear to zoom relative to the center
// of the viewport.
left: "-=" + (width_change / 2),
top: "-=" + (width_change / 2)
});
}
});
Here is the project on JSFiddle: http://jsfiddle.net/christiannaths/W4seR/
Here's the working solution. I will explain the logic at the next edit.
Function Logic:
Summary: Remember the center position of the image, relatively.
The calculations for width and height are similar, I will only explain the height calculationThe detailled explanation is just an example of function logic. The real code, with different variable names can be found at the bottom of the answer.
Calculate the center (x,y) of the #map, relative to #viewport. This can be done by using the offset(), height() and width() methods.
// Absolute difference between the top border of #map and #viewport
var differenceY = viewport.offset().top - map.offset().top;
// We want to get the center position, so add it.
var centerPosition = differenceY + viewport.height() * 0.5;
// Don't forget about the border (3px per CSS)
centerPosition += 3;
// Calculate the relative center position of #map
var relativeCenterY = centerPosition / map.height();
// RESULT: A relative offset. When initialized, the center of #map is at
// the center of #viewport, so 50% (= 0.5)
// Same method for relativeCenterX
Calculate the new top and left offsets:
// Calculate the effect of zooming (example: zoom 1->2 = 2)
var relativeChange = new_width / old_width;
// Calculate the new height
var new_height = relativeChange * old_height;
// Calculate the `top` and `left` CSS properties.
// These must be negative if the upperleft corner is outside he viewport
// Add 50% of the #viewport's height to correctly position #map
// (otherwise, the center will be at the upperleft corner)
var newTopCss = -relativeCenterY * new_height + 0.5 * viewport.height();
Change the CSS property
map.css("top", newTopCss);
Demo: http://jsfiddle.net/W4seR/12/
var map = $('#map');
var viewport = $('#viewport');
// Cache the size of the viewport (300x300)
var viewport_size = {
x: viewport.width(),
y: viewport.height()
};
map.draggable();
$('#zoom-control').slider({
min: 300,
max: 1020,
value: 300,
step: 24,
create: function() {
map.css({
'width': 300,
'left': 0,
'top': 0
});
},
slide: function(event, ui) {
var old_width = map.width();
var old_height = map.height();
var viewport_offset = viewport.offset();
var offset = map.offset();
offset = {
top: viewport_offset.top - offset.top + .5*viewport_size.y +3,
left: viewport_offset.left - offset.left + .5*viewport_size.x +3
};
// Relative offsets, relative to the center!
offset.top = offset.top / old_height;
offset.left = offset.left / old_width;
var new_width = ui.value;
var relative = new_width / old_width;
var new_height = relative * old_height;
offset = {
top: -offset.top * new_height + .5*viewport_size.y,
left: -offset.left * new_width + .5*viewport_size.x
};
var css_properties = {
width: new_width,
left: offset.left,
top: offset.top
};
map.css(css_properties);
trace((map.position().left));
}
});
I have always relied on the kindness of strangers. Pertinent changes:
// Calculate the offset as a percentage, accounting for the height of the window
var x_offset = ((map.position().left-150))/(old_width/2);
var y_offset = ((map.position().top-150))/(old_width/2);
var css_properties = {
width: new_width,
// Set the offset based on the existing percentage rather than 1/2
// then readjust for the height of the window
left: (new_width * x_offset /2 ) + 150 + "px",
top: (new_width * y_offset /2 ) + 150 + "px"
};
Replace the hardcoded 150 with a variable set on viewport instantiation if necessary.
Here is a quick working version:
http://jsfiddle.net/flabbyrabbit/chLkZ/
Probably not the neatest solution but seems to work nicely, hope it helps.
Update: sorry this only works if zoom is 0 when the map is moved.

Categories

Resources