width attribute not working with d3 style - css

I'm trying to display bar graphs with width of each bar representing age. but using style method in d3, i am unable to apply width attribute to the bars (as checked by doing inspect element in browser). other attributes applied using style are working fine.
function showData(clients){
let max = d3.max(clients, (d) => {
return (parseInt(d.age));
})
//console.log(max);
let scale = d3.scaleLinear().domain([0, parseInt(max)]).range([0, 100]);
//console.log(scale);
let join = container.selectAll('div').data(clients);
join.enter().append('div')
.text((d) => {
return d.name + ' : ' + scale(parseInt(d.age));
})
.style('background-color', 'blue')
.style('margin', '5px')
.style('color', 'white')
.style('width', (d) => {
return parseInt(d.age)
});
}

css width is not just a number:
width: 150px;
width: 20em;
width: 75%;
width: auto;
I believe you want:
.style('width', (d) => {
return parseInt(d.age) + 'px';
});

If you're applying width or height to svg or svg elements, use .attr() instead of .style(). In this case, you can use numbers, no px needed. If you don't specify any unit, it will be assumed to be px. If you're using width and height in style attribute, you must specify a unit, no matter what element.
const width = 400,
height = 200;
const svg = d3.select("svg")
.attr("width", width)
.attr("height", height)
.style("background", "steelblue")
.append("rect")
.attr("width", 100)
.attr("height", 200)
.attr("fill", "tomato");
<script src="https://unpkg.com/d3#6.3.1/dist/d3.min.js"></script>
<svg></svg>

Related

Using Matter.js, how to position SVG paths in a compound body?

I'm trying to create a capital letter "A" as an SVG shape for use with Matter.js but the letter shape displays incorrectly.
CodePen here and duplicated here:
function percentX(percent) {
return Math.round((percent / 100) * window.innerWidth);
}
function percentY(percent) {
return Math.round((percent / 100) * window.innerHeight);
}
const Engine = Matter.Engine,
Bodies = Matter.Bodies,
Body = Matter.Body,
Svg = Matter.Svg,
Vertices = Matter.Vertices,
Composite = Matter.Composite,
Render = Matter.Render,
Runner = Matter.Runner;
// create an engine
const engine = Engine.create(),
world = engine.world;
// create a renderer
const render = Render.create({
element: document.body,
engine: engine,
options: {
wireframes: false,
showInternalEdges: false,
width: percentX(100),
height: percentY(100),
background: "transparent"
}
});
let bodies = [],
bgColor = "#0A0618";
// SVGs
let vertexSets = [],
svgLetter,
svgLetterLegOne,
svgLetterLegTwo,
svgLetterCounter;
let letterX = percentX(60);
let letterY = percentY(20);
let letterXLegOne = percentX(60) - 40;
let letterYLegOne = percentY(20) + 40;
let letterXLegTwo = percentX(60) + 40;
let letterYLegTwo = percentY(20) + 40;
let letterSize = (window.innerWidth / 1000);
// A
// silhouette test (incorrectly displaying Batman ears)
$('#svg-test').find('path').each(function(i, path) {
svgTest = Bodies.fromVertices(
percentX(30),
letterY,
Vertices.scale(Svg.pathToVertices(path, 10),
letterSize,
letterSize), {
render: {
fillStyle: "white",
strokeStyle: "white",
lineWidth: 2
}
}, true);
vertexSets.push(svgTest);
});
// letter base shape
$('#svg-3').find('path').each(function(i, path) {
svgLetter = Bodies.fromVertices(
letterX,
letterY,
Vertices.scale(Svg.pathToVertices(path, 10),
letterSize,
letterSize), {
render: {
fillStyle: "yellow",
strokeStyle: "yellow",
lineWidth: 2
}
}, true);
vertexSets.push(svgLetter);
});
// left leg
$('#svg-3-leg-1').find('path').each(function(i, path) {
svgLetterLegOne = Bodies.fromVertices(
letterXLegOne,
letterYLegOne,
Vertices.scale(Svg.pathToVertices(path, 10),
letterSize,
letterSize), {
render: {
fillStyle: "green",
strokeStyle: "green",
lineWidth: 2,
isStatic: true
}
}, true);
vertexSets.push(svgLetterLegOne);
});
// right leg
$('#svg-3-leg-2').find('path').each(function(i, path) {
svgLetterLegTwo = Bodies.fromVertices(
letterXLegTwo,
letterYLegTwo,
Vertices.scale(Svg.pathToVertices(path, 10),
letterSize,
letterSize), {
render: {
fillStyle: "blue",
strokeStyle: "blue",
lineWidth: 2,
isStatic: true
}
}, true);
vertexSets.push(svgLetterLegTwo);
});
// counter (hole in the center), no need for offset repositioning
$('#svg-3-counter').find('path').each(function(i, path) {
svgLetterCounter = Bodies.fromVertices(
letterX,
letterY,
Vertices.scale(Svg.pathToVertices(path, 10),
letterSize,
letterSize), {
render: {
fillStyle: bgColor,
strokeStyle: bgColor,
lineWidth: 2
}
}, true);
vertexSets.push(svgLetterCounter);
});
// create compound body for letter "A"
var compoundBodyA = Body.create({
parts: [svgLetter, svgLetterLegOne, svgLetterLegTwo, svgLetterCounter]
});
// add A and O compound bodies to the world
Composite.add(world, [
compoundBodyA
]);
// add all SVGs to the world
Composite.add(world, vertexSets);
// run the renderer
Render.run(render);
// create runner
const runner = Runner.create();
// run the engine
Runner.run(runner, engine);
// hold in place for testing
world.gravity.y = 0;
world.gravity.x = 0;
html {
box-sizing: border-box;
}
*, *::before, *::after {
box-sizing: inherit;
}
*:focus, *::before:focus, *::after:focus {
outline: none;
}
* {
font-family: monaco, courier;
}
body {
margin: 0;
padding: 0;
background: #0A0618;
}
svg {
display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pathseg#1.2.1/pathseg.js"></script>
<script src="https://cdn.jsdelivr.net/npm/poly-decomp#0.3.0/build/decomp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.18.0/matter.min.js"></script>
<svg class="svg-letter" id="svg-test">
<path class="st0" d="M59.3,0h46.4l59,141h-50.8l-7.4-18.8h-49L50.3,141h-50L59.3,0z"/>
</svg>
<svg class="svg-letter" id="svg-3">
<path d="M57.2,122.2H7.9L59,0h46.4l51.1,122.2h-50.3H57.2z"/>
</svg>
<svg class="svg-letter" id="svg-3-leg-1">
<path d="M0,141l7.9-18.8h49.3L50,141H0z"/>
</svg>
<svg class="svg-letter" id="svg-3-leg-2">
<path d="M106.2,122.2h50.3l7.9,18.8h-50.8L106.2,122.2z"/>
</svg>
<svg class="svg-letter" id="svg-3-counter">
<path d="M94.6,89L81.8,55L69,89H94.6z"/>
</svg>
I know Matter.js can't always handle SVGs with compound paths (an internal path creating a knockout) so my plan was to have two separate paths, the silhouette of the shape and the knockout, and group them as a compound body. Only the silhouette isn't even displaying correctly (the white version on the left). For some reason, the shape always has those Batman ears which I can't get rid of.
So my new plan is to break the silhouette shape into three parts, the main body (in yellow) and two legs (green and blue). That allows all three parts to have only four sides which seems to prevent the bug.
My problem is positioning those two legs so that they are always precisely butting up against the main body shape. I can adjust the position offset to accomplish this but since I've set the width and height of the render object to be proportional to the width and height of the browser, the letter breaks apart if the page is loaded in any other sized window.
Using variables (lines 45–54), I've tried setting the width and height of the legs to reference the main body shape with offsets:
svgThree.position.x - 40,
svgThree.position.y + 40,
And I've tried keeping all units and offsets proportional:
percentX(60) - percentX(2),
percentY(20) + percentX(2),
But nothing works. Without setting the letter to exact pixel dimensions, is there any way to keep these three paths touching and in precise relation to each other across different browser sizes?
Alternatively, if there's any way to build that SVG shape to avoid that bug, I'd greatly appreciate such a solution.
(Using Chrome Version 102.0.5005.115)
It's not a perfect solution but the one I eventually went with was this:
Setting the letter size to a static number (0.8), instead of a derivation of the window width, allowed me to set the width and height of the two legs of the letter to a static offset distance from the main body shape, so the change in the original CodePen above would be to lines 48–54:
let letterXLegOne = letterX - 43;
let letterYLegOne = letterY + 49;
let letterXLegTwo = letterX + 43;
let letterYLegTwo = letterY + 49;
let letterSize = 0.8;
The fix is incorporated into this CodePen as a final product.

Use CSS to move charts close to each other

I have two charts. One is bigger than the other. The bigger one consist of two labels called a and b. The small one consist of label c and d. Code for both charts are similar as the following.
var chart = c3.generate({
data: {
columns: [
['Rra', 3880],
['b', 50],
],
type : 'donut'
},
});
var chart = c3.generat
size: {
height: 200,
width: 450
},
data: {
columns: [
['c', 50],
['d', 50],
],
},
});
I was wondering if it is possible to move the small chart between the empty space in the big chart. So they end up looking like two layered charts. I did inspect element on the small chart and tried to use css to tweak the transform="translate(225,83)" to different values. However i noticed when the small chart got close to the bigger chart parts of it disappeared.
I am not expert in css or charting libraries and would love inputs on how to achieve this. Here is my FIDDLE
Although I do agree with YourConscious's answer (using the d3 plugin to create what you want instead of using hacky CSS), here is the CSS method I have come up with:
.chart_container{
position:relative;
}
#chart1{
position:absolute !important;
top:50%;
left:50%;
transform:translate(-50%, -50%);
}
You can see it in action in this Fiddle
The magic is wrapping both charts in a container div, setting it to position:relative; so you can position your smaller chart off of it. Then give your smaller chart the above styles to always position it in the middle of the larger chart.
The !important is there because it seems the plugin is putting an inline style of position:relative
You could probably do it.. with position:absolute; and nesting. But I wouldn't really recommend it and will likely not be that stable (z-index issues, mobile, updating it later on, etc).
I think that library has direct documentation and capabilities to create multiple layers / rings. Why not just do the right way?
Exaggerated example (5 layers / rings)
var dataset = {
apples: [53245, 28479, 19697, 24037, 40245],
oranges: [53245, 28479, 19697, 24037, 40245],
lemons: [53245, 28479, 19697, 24037, 40245],
pears: [53245, 28479, 19697, 24037, 40245],
pineapples: [53245, 28479, 19697, 24037, 40245],
};
var width = 460,
height = 300,
cwidth = 25;
var color = d3.scale.category20();
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var gs = svg.selectAll("g").data(d3.values(dataset)).enter().append("g");
var path = gs.selectAll("path")
.data(function(d) { return pie(d); })
.enter().append("path")
.attr("fill", function(d, i) { return color(i); })
.attr("d", function(d, i, j) { return arc.innerRadius(10+cwidth*j).outerRadius(cwidth*(j+1))(d); });
J Fiddler.
Also, this might be a good resource:

How to position an element absolute to the window regardless of its DOM position?

I am creating a pop-up overlay modal and am having problems getting the positioning/scrolling working correctly.
I can set my modal to be position:fixed but then if the modal's height is too much, then the modal overflows off of the window and you cannot see the bottom of it.
If I set the modal to be position:absolute then the element becomes positioned relative to the closest ancestor with position:relative, correct? (or at least thats what it appears to do) Instead I want the modal to ALWAYS be relative to the window so that I can center it easily.
Is there a way to make the below .modal positioned relative to the window ( or element) even if the element is nested deep inside the DOM like this:
<body ng-app="myapp" ng-controller="mycontroller">
<div>
<div>
<div ui-view>
<div class=".modal"></div>
</div>
</div>
</div>
</body>
If you insist on having it in that same markup and nested in the same manner, your best bet is in JavaScript.
Here's some JS code that gives a good method of accomplishing what you asked for:
function ShowDivInCenter()
{
try
{
divWidth = 100;
divHeight = 100;
divId = 'divLogin'; // id of the div that you want to show in center
// Get the x and y coordinates of the center in output browser's window
var centerX, centerY;
if (self.innerHeight)
{
centerX = self.innerWidth;
centerY = self.innerHeight;
}
else if (document.documentElement && document.documentElement.clientHeight)
{
centerX = document.documentElement.clientWidth;
centerY = document.documentElement.clientHeight;
}
else if (document.body)
{
centerX = document.body.clientWidth;
centerY = document.body.clientHeight;
}
var offsetLeft = (centerX - divWidth) / 2;
var offsetTop = (centerY - divHeight) / 2;
// The initial width and height of the div can be set in the
// style sheet with display:none; divid is passed as an argument to // the function
var ojbDiv = document.getElementById(divId);
ojbDiv.style.position = 'absolute';
ojbDiv.style.top = offsetTop + 'px';
ojbDiv.style.left = offsetLeft + 'px';
ojbDiv.style.display = "block";
}
catch (e) {}
}
You can then call the function through any event, for example:
<body onload='ShowDivInCenter();' onresize='ShowDivInCenter();'>
if you want it to be dynamic.

How to scale text size compared to container

How would you scale text size based on container size. Other questions state it can't be done with css, if not how would you go about it?
Here is a way to set the text to fit the width of the container. It works by incrementally increasing the font size of a hidden div until it is smaller than the width of the container for which you want to text to fit inside. It then sets the font size of the container to that of the hidden div.
This is a little inefficient and could be optimized by caching the old width of the container, then incrementing from the old font size to either larger or smaller values based on how the width of the container changed.
jsFiddle: http://jsfiddle.net/sarathijh/XhXQk/
<div class="scale-text styles">
This is a test of scaling text to fit the container. Lorem Ipsum Dolor Sit Amet.
</div>
<div id="font-metrics"></div>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
<script>
var fontMetrics = document.getElementById('font-metrics');
var scaleTexts = $('.scale-text');
$(window).on('resize', updateFontSize);
updateFontSize();
function updateFontSize()
{
scaleTexts.each(function()
{
var $scaleText = $$(this);
fontMetrics.innerHTML = this.innerHTML;
fontMetrics.style.fontFamily = $scaleText.css('font-family');
fontMetrics.style.fontWeight = $scaleText.css('font-weight');
fontMetrics.style.fontStyle = $scaleText.css('font-style');
fontMetrics.style.textTransform = $scaleText.css('text-transform');
var fontSize = 50; // max font-size to test
fontMetrics.style.fontSize = fontSize + 'px';
while ($$(fontMetrics).width() > $scaleText.width() && fontSize >= 0)
{
fontSize -= 1;
fontMetrics.style.fontSize = fontSize + 'px';
}
this.style.fontSize = fontSize + 'px';
});
}
/**
* A simple caching function for jQuery objects.
*/
function $$(object)
{
if (!object.jquery)
object.jquery = $(object);
return object.jquery;
}
</script>
You should be able to do this using jQuery -- something like..
$('#cooldiv').css('font-size', '100%')
and
$("#cooldiv").animate({
'width' : (xWidth * (35 / 100)) + 'px',
'minWidth' : (xWidth * (35 / 100)) + 'px',
'maxWidth' : (xWidth * (35 / 100)) + 'px',
'fontSize' : (xWidth * (35 / 100)) + '%'
}, 1000);
I couldn't work out how to do it with CSS, so I used pure JS (don't need jquery).
var els = document.getElementsByClassName('abc')
for (var i = 0; i < els.length; i++){
els[i].style.fontSize = els[i].clientHeight + 'px'
}
Example: http://jsfiddle.net/nickg1/H64mQ/
Here's how I accomplished it- I wrapped the text in a span and scaled that down with a CSS transform:
<div id="fit-text-in-here">
<span>Scale this text to fit inside the parent div.</span>
</div>
jQuery:
$(function(){
var textWidth = $('#fit-text-in-here span').width(),
fitWidth = $('#fit-text-in-here').width();
if (textWidth > fitWidth) {
var scaleTo = fitWidth / textWidth,
offset = (fitWidth - textWidth)/2;
$('#fit-text-in-here span').css({
'-moz-transform': 'scale('+scaleTo+')',
'-webkit-transform': 'scale('+scaleTo+')',
'-o-transform': 'scale('+scaleTo+')',
'transform': 'scale('+scaleTo+')',
'margin-left': offset,
'display': 'inline-block'
});
}
});

d3.js Map (<svg>) Auto Fit into Parent Container and Resize with Window

UPDATE: I have posted and accepted a fully working solution in the answers section. Any code in this section is to be used as reference for comparison to your own NON-WORKING code, but is not to be used as the solution.
I'm building a dashboard and using d3.js to add a world map that will plot tweets in real time based on geo location.
The world.json file referenced in the d3.json() line is downloadable HERE (it's called world-countries.json).
The map is on the page as an SVG container and is rendered using d3.
Below are the relevant code slices.
<div id="mapContainer">
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="500"></svg>
</div>
#mapContainer svg {
display:block;
margin:0 auto;
}
#mapContainer path {
fill:#DDD;
stroke:#FFF;
}
// generate US plot
function draw() {
var map = d3.select("svg");
var width = $("svg").parent().width();
var height = $("svg").parent().height();
var projection = d3.geo.equirectangular().scale(185).translate([width/2, height/2]);
var path = d3.geo.path().projection(projection);
d3.json('plugins/maps/world.json', function(collection) {
map.selectAll('path').data(collection.features).enter()
.append('path')
.attr('d', path)
.attr("width", width)
.attr("height", height);
});
}
draw();
latestLoop();
$(window).resize(function() {
draw();
});
UPDATE: I have scaled the map to an acceptable size (for my particular browser size), but it still will not scale and center when I change the size of the window. IF, however, I resize the window, then hit refresh, then the map will be centered once the page is reloaded. However, since the scale is static, it is not scaled properly.
COMPLETE SOLUTION:
Here's the solution which will resize the map AFTER the user has released the edge of the window to resize it, and center it in the parent container.
<div id="mapContainer"></div>
function draw(ht) {
$("#mapContainer").html("<svg id='map' xmlns='http://www.w3.org/2000/svg' width='100%' height='" + ht + "'></svg>");
map = d3.select("svg");
var width = $("svg").parent().width();
var height = ht;
// I discovered that the unscaled equirectangular map is 640x360. Thus, we
// should scale our map accordingly. Depending on the width ratio of the new
// container, the scale will be this ratio * 100. You could also use the height
// instead. The aspect ratio of an equirectangular map is 2:1, so that's why
// our height is half of our width.
projection = d3.geo.equirectangular().scale((width/640)*100).translate([width/2, height/2]);
var path = d3.geo.path().projection(projection);
d3.json('plugins/maps/world.json', function(collection) {
map.selectAll('path').data(collection.features).enter()
.append('path')
.attr('d', path)
.attr("width", width)
.attr("height", width/2);
});
}
draw($("#mapContainer").width()/2);
$(window).resize(function() {
if(this.resizeTO) clearTimeout(this.resizeTO);
this.resizeTO = setTimeout(function() {
$(this).trigger('resizeEnd');
}, 500);
});
$(window).bind('resizeEnd', function() {
var height = $("#mapContainer").width()/2;
$("#mapContainer svg").css("height", height);
draw(height);
});
The selection object is an multidimensional array, although in most cases it will probably have only one object in it. That object has a "clientWidth" field that tells you how wide its parent is.
So you can do this:
var selection = d3.select("#chart");
width = selection[0][0].clientWidth;
This should work:
<svg
xmlns="http://www.w3.org/2000/svg"
width="860"
height="500"
viewBox="0 0 860 500"
preserveAspectRatio="xMinYMin meet">
The best choice is to have a combined use of aspect ratio on normal definition of d3 graph's width and height. This has helped me in lot of my graph works.
Step 1 : Dynamically get the height of the div to which the graph has to be appended.
Step 2 : Declare width as aspect ratio with respect to the dynamically caught height.
var graph_div = document.getElementById(graph.divId);
graph.height = graph_div.clientHeight;
graph.width = (960/1200)*graph.height;
In d3 v4, we could do this
const projection = d3.geo.equirectangular().fitSize([width, height], geojson);
const path = d3.geo.path().projection(projection);
fitSize is equivalent to
fitExtent([[0, 0], [width, height]], geojson)
fill free to add padding

Resources