I am looking for a way to calculate the SVG path for a regular polygon given the number of sides, the width and height of the wrapper-container.
Any help will be really appreciated!
Thank you,
Best.
Update: N could be both even and odd.
Step 1. How to create SVG
const $svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
$svg.setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:xlink", "http://www.w3.org/1999/xlink")
$svg.setAttribute('width', 500)
$svg.setAttribute('height', 500)
$svg.setAttribute('viewBox', '0 0 500 500')
document.body.appendChild($svg)
Step 2. How to create Line
const $el= document.createElementNS("http://www.w3.org/2000/svg", "line")
$el.setAttribute('x1', 0)
$el.setAttribute('y1', 0)
$el.setAttribute('x2', 100) // not real
$el.setAttribute('y2', 100) // not real
$svg.appendChild($el)
Step 3. How to create real Line
function (sides, width) {
const angle = 360 / sides
const $el1= document.createElementNS("http://www.w3.org/2000/svg", "line")
$el1.setAttribute('x1', 0)
$el1.setAttribute('y1', 0)
$el1.setAttribute('x2', width)
$el1.setAttribute('y2', 0)
$svg.appendChild($el1)
// we already created first line, so start from 1
for (var i = 1; i < sides.length; i++) {
const $el= document.createElementNS("http://www.w3.org/2000/svg", "line")
const offsetX_angle = 180 - angle
const hypotenuse = width / Math.sin(90)
const offsetX = Math.sin(90 - offsetX_angle) * hypotenuse
const offsetY = Math.sin(offsetX_angle) * hypotenuse
// now we know the offsets
// and we can easily populate other lines using same offset
const x1 = width
const y1 = 0
const x2 = width + offsetX
const y2 = 0 + offsetY
// just need to switch angles based in i
}
}
This is not full solution, it's steps for making solution
Related
I need to make a Sunflowerplot and after rotating the lines the position of them has to be translated back. But i don't know how to get the new x,y pos after rotating.
I want to rotate only the line but its position does ofcourse change too.
var xOld = (save[i][0])/(xS.value/100/3.4);
var yOld = (save[i][1])/(yS.value/100/3.5*-1);
//Above code is to get and transform the position where to draw
//and works very well without rotate
var line = d3.select("svg")
.append("line")
.attr("stroke-width","1")
//Backwardline
.attr("x1",xOld-lineLength)
.attr("y1",yOld)
//I think that i need to translate the new position here
.attr("transform", "translate(50, " + 360 +") rotate(" + 45 * -1+ ")")
//Forwardline
.attr("x2",(xOld)+lineLength)
.attr("y2",(yOld))
.style("stroke","blue");
I added a snippet where you can determine the number of petals yourself, and play with the styling and rotation a little if you want
const svg = d3.select('body').append('svg');
// The distance in pixels between the edge and the center of each petal
const petalRadius = 20;
// sin, cos, and tan work in radians
const fullCircle = 2 * Math.PI;
// Zero rads make the shape point to the right with the right angle
// Use - 0.5 * pi rads to make the first petal point upwards instead
// You can play with this offset to see what it does
const offset = - Math.PI / 2;
function drawSunflower(container, petals) {
const radsPerPetal = fullCircle / petals;
const path = container.append('path');
// We're going to need this a lot. M moves to the given coordinates, in this case
// That is the center of the sunflower
const goToCenter = ` M ${petalRadius},${petalRadius}`;
// Construct the `d` attribute. Start in the center and work form there.
let d = goToCenter;
let counter = 0;
while (counter < petals) {
const rads = counter * radsPerPetal + offset;
const dx = Math.cos(rads) * petalRadius;
const dy = Math.sin(rads) * petalRadius;
// Draw a relative line to dx, dy, then go to center
d += `l ${dx},${dy}` + goToCenter;
counter += 1;
}
path.attr('d', d);
}
const transform = 2 * petalRadius + 5;
for (let i = 0; i < 5; i++) {
for (let j = 0; j < 3; j++) {
let container = svg.append('g').attr('transform', `translate(${i * transform}, ${j * transform})`);
drawSunflower(container, i * 5 + j + 1);
}
}
g > path {
stroke: black;
stroke-width: 1px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
I have a bubble chart in D3 and I'm using it to show how many bubbles there are per group. This version has about 500 bubbles to start with and my full version has about 3,000.
I am struggling along two dimension. I'm trying to get the bubbles to stay put when they're not transitioning between states and I'm also trying to get the bubbles to create a rectangular shape.
This is a demo of the bubble chart. I'll add the code and then go through what I've tried.
This is the code for my bubbles.
// Initial time and quarter
let time_so_far = 0;
let quarter = 0;
const tick_time = 100
// Forces
const radius = 1.5
const padding1 = 10;
const padding2 = 2;
const strength = 50
const veloc_decay = .99
const alpha = .05
const alpha_decay = 0
const alpha_min = 0.001
const alpha_Collision = .08;
const charge_strength = -.5
const charge_theta = .9
// Load data
Promise.all([
d3.tsv("stages.tsv", d3.autoType),
d3.tsv("customers.tsv", d3.autoType),
])
// Once data is loaded...
.then(function(files){
// Prepare the data...
const stage_data = files[0]
const customer_data = files[1]
// Consolidate stages by id.
stage_data.forEach(d => {
if (d3.keys(stakeholders).includes(d.id+"")) {
stakeholders[d.id+""].push(d);
} else {
stakeholders[d.id+""] = [d];
}
});
// Consolidate customers by week.
customer_data.forEach(d => {
if (d3.keys(customers).includes(d.week+"")) {
customers[d.week+""].push(d);
} else {
customers[d.week+""] = [d];
}
});
// Create node data.
var nodes = d3.keys(stakeholders).map(function(d) {
// Initialize count for each group.
groups[stakeholders[d][0].stage].cnt += 1;
return {
id: "node"+d,
x: groups[stakeholders[d][0].stage].x + Math.random(),
y: groups[stakeholders[d][0].stage].y + Math.random(),
r: radius,
color: groups[stakeholders[d][0].stage].color,
group: stakeholders[d][0].stage,
timeleft: stakeholders[d][0].weeks,
istage: 0,
stages: stakeholders[d]
}
});
// Circle for each node.
const circle = svg.append("g")
.selectAll("circle")
.data(nodes)
.join("circle")
.attr("cx", d => d.x)
.attr("cy", d => d.y)
.attr("fill", d => d.color)
.attr("r", d => d.r);
// Forces
const simulation = d3.forceSimulation(nodes)
// .force("bounds", boxingForce)
.force("x", d => d3.forceX(d.x))
.force("y", d => d3.forceY(d.y))
.force("cluster", forceCluster())
.force("collide", forceCollide())
.force("charge", d3.forceManyBody().strength(charge_strength).theta(charge_theta))
// .force('center', d3.forceCenter(center_x, center_y))
.alpha(alpha)
.alphaDecay(alpha_decay)
.alphaMin(alpha_min)
.velocityDecay(veloc_decay)
// Adjust position of circles.
simulation.on("tick", () => {
circle
.attr("cx", d => Math.max(r, Math.min(500 - r, d.x)))
.attr("cy", d => Math.max(r, Math.min(500 - r, d.y)))
.attr("fill", d => groups[d.group].color);
});
// Force to increment nodes to groups.
function forceCluster() {
let nodes;
function force(alpha) {
const l = alpha * strength;
for (const d of nodes) {
d.vx -= (d.x - groups[d.group].x) * l;
d.vy -= (d.y - groups[d.group].y) * l;
}
}
force.initialize = _ => nodes = _;
return force;
}
// Force for collision detection.
function forceCollide() {
let nodes;
let maxRadius;
function force() {
const quadtree = d3.quadtree(nodes, d => d.x, d => d.y);
for (const d of nodes) {
const r = d.r + maxRadius;
const nx1 = d.x - r, ny1 = d.y - r;
const nx2 = d.x + r, ny2 = d.y + r;
quadtree.visit((q, x1, y1, x2, y2) => {
if (!q.length) do {
if (q.data !== d) {
const r = d.r + q.data.r + (d.group === q.data.group ? padding1 : padding2);
let x = d.x - q.data.x, y = d.y - q.data.y, l = Math.hypot(x, y);
if (l < r) {
l = (l - r) / l * alpha_Collision;
d.x -= x *= l, d.y -= y *= l;
q.data.x += x, q.data.y += y;
}
}
} while (q = q.next);
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
}
}
force.initialize = _ => maxRadius = d3.max(nodes = _, d => d.r) + Math.max(padding1, padding2);
return force;
}
// Make time pass. Adjust node stage as necessary.
function timer() {
// Ticker...
nodes.forEach(function(o,i) {
o.timeleft -= 1;
if (o.timeleft == 0 && o.istage < o.stages.length-1) {
// Decrease counter for previous group.
groups[o.group].cnt -= 1;
// Update current node to new group.
o.istage += 1;
o.group = o.stages[o.istage].stage;
o.timeleft = o.stages[o.istage].weeks;
// Increment counter for new group.
groups[o.group].cnt += 1;
}
});
// Previous quarter
quarter = Math.floor(time_so_far / 12)
// Increment time.
time_so_far += 1;
// goes by week, timer updates every quarter
var current_quarter = Math.floor(time_so_far / 13) + 1
// stop on the last quarter
if(time_so_far == d3.keys(customers).length) { return }
d3.select("#timecount .cnt").text(quarters[current_quarter]);
// update counter
d3.selectAll(".counter")
.text(d => d.cnt)
// Define length of a tick
d3.timeout(timer, tick_time);
} // #end timer()
timer()
}); // end TSV
Right now, my bubbles are constantly moving. Even if I make the space for the bubbles really large and the padding really small, they keep moving.
I've tried to set .alphaDecay() to a value greater than 0 and it gets the bubbles to stop moving and they look quite good, but then they don't have energy to transition between states.
I'd like to set it so that the bubbles find their spot when the page loads and then they don't move, except to change from no interactions to portfolio to partner similar to the bubble chart here.
The other problem is that the bubbles congregate as circles. I'd like to get them to fill in the whole rectangular backdrop for each of the states.
Per Mike Bostock's comments, I added boundaries in the simulation.on function. It works to set boundaries on the whole space, but it doesn't apply the boundaries to each state individually, so they still end up clustering as circles.
I've also tried John Guerra's d3.forceBoundary but I run into the same problem.
How can I force the bubbles to stay in one position and only move when a transition in states occurs and how can I get the bubbles to congregate in rectangles on each state?
Edit: I tried to set alphaDecay > 0 so the bubbles would initialize and stop moving and then I added a new alpha value in the .on("tick", function, but that just let them keep energy.
The core of the problem is that I don't know how to apply force that lets them move across the viz from one state to another state, but doesn't cause them to jumble around.
My next try is going to be to create a different force for changing states than for getting created.
Edit2: I've got a solution going for the energy problem. It's a bit hacky.
I added o.statechange = 3 within the if loop inside nodes.forEach(function(o,i) { and I added o.statechange -= 1 right above the if loop. And then, in forceCluster I added
for (var i = 0, n = nodes.length, node, k = alpha * strength; i < n; ++i) {
node = nodes[i];
if(node.statechange <= 0) { continue }
node.vx -= (node.x - groups[node.group].x) * k;
node.vy -= (node.y - groups[node.group].y) * k;
}
This gives circles energy for three ticks if they need to make a move. Otherwise, they don't get any. (Last edit, this work-around worked for a small number of nodes but it fails as the number of nodes gets larger)
I use this as reference: https://bl.ocks.org/iamkevinv/0a24e9126cd2fa6b283c6f2d774b69a2
Adjusted some syntax to fit for version 5
Scale works, Translate looks like it works too because if I change the value, it zooms on different place..
But the problem is it doesn't zoom on the correct place I clicked.
I think this doesn't get to the place correctly because I use d3.geoMercator().fitSize([width, height], geoJSONFeatures) instead:
var bounds = path.bounds(d),
dx = bounds[1][0] - bounds[0][0],
dy = bounds[1][1] - bounds[0][1],
x = (bounds[0][0] + bounds[1][0]) / 2,
y = (bounds[0][1] + bounds[1][1]) / 2,
scale = Math.max(1, Math.min(8, 0.9 / Math.max(dx / width, dy / height))),
translate = [width / 2 - scale * x, height / 2 - scale * y];
Already tried to change the values to fit mine but failed, I can't get it.
Here is my projection:
var width = 500;
var height = 600;
d3.json("/regions50mtopo.json")
.then((geoJSON) => {
var geoJSONFeatures = topojson.feature(geoJSON, geoJSON.objects["Regions.50m"]);
// My Projection
var projection = d3.geoMercator().fitSize([width, height], geoJSONFeatures);
...
Any help, guide or reference?
Note: I'm mapping different country and fitSize(...) solves the
problem easily to fit on my svg that's why I can't use the same as in
the reference link I provided.
Found an answer: https://bl.ocks.org/veltman/77679636739ea2fc6f0be1b4473cf03a
centered = centered !== d && d;
var paths = svg.selectAll("path")
.classed("active", d => d === centered);
// Starting translate/scale
var t0 = projection.translate(),
s0 = projection.scale();
// Re-fit to destination
projection.fitSize([960, 500], centered || states);
// Create interpolators
var interpolateTranslate = d3.interpolate(t0, projection.translate()),
interpolateScale = d3.interpolate(s0, projection.scale());
var interpolator = function(t) {
projection.scale(interpolateScale(t))
.translate(interpolateTranslate(t));
paths.attr("d", path);
};
d3.transition()
.duration(750)
.tween("projection", function() {
return interpolator;
});
Exactly what I'm looking for. It works now as expected.
But maybe somebody also have suggestions on how to optimise it, because as the author said too, it feels slow and "laggy" when zooming in/out.
Question
I have positive and negative values, and I'd like to plot them on a "logarithmic" scale.
Imagine a scale with evenly spaced ticks for the following values:
-1000, -100, -10, -1, 0, 1, 10, 100, 1000
I want 0 in there, which is defined to be -Inf by logarithms, complicating this further.
However, I don't think this request is unreasonable. This seems like a sensible scale any data scientist might want to plot strongly divergent values against.
How do you create such a scale and axis in d3?
Thoughts
It might be possible to do this cleverly with 2 d3.scaleLog()s or maybe 3 scales if you use a technique like this one.
I was hoping there might be an easy way to fit this in a d3.scalePow() with .exponent(0.1) but unless I've got my log rules mixed up, you can't get a .scaleLog() out of a .scalePow() (though you can probably approximate it okay for some ranges).
We can't have a true log scale like this, or even a combination of two log scales like this as. We need to set a cut off for zeroish values, and this is where error might be introduced depending on your data. Otherwise, to make a scale function like this is fairly straightforward, just call a different scale for negative and positive while setting zero-ish values to zero.
This combination of scales might look like:
var positive = d3.scaleLog()
.domain([1e-6,1000])
.range([height/2,0])
var negative = d3.scaleLog()
.domain([-1000,-1e-6])
.range([height,height/2])
var scale = function(x) {
if (x > 1e-6) return positive(x);
else if (x < -1e-6) return negative(x);
else return height/2; // zero value.
}
And an example:
var width = 500;
var height = 300;
var positive = d3.scaleLog()
.domain([1e-1,1000])
.range([height/2,0])
var negative = d3.scaleLog()
.domain([-1000,-1e-1])
.range([height,height/2])
var scale = function(x) {
if (x > 1e-6) return positive(x);
else if (x < -1e-6) return negative(x);
else return height/2; // zero value.
}
var line = d3.line()
.y(function(d) { return scale(d) })
.x(function(d,i) { return (i); })
var svg = d3.select("body")
.append("svg")
.attr("width",width)
.attr("height",height)
var data = d3.range(width).map(function(d) {
return (d - 250) * 4;
})
svg.append("path")
.attr("d", line(data) );
path {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Creating a single scale
The above is a proof of concept maybe.
Now the trickier part is making an axis. We could make axes for both the scales above, leaving zero with some sort of manual correction. But it will be easier to create a scale with our own interpolotor using the above as an exmaple. This gives us one scale which we can create an axis for. Our interpolator might look like:
// Interpolate an output value:
var interpolator = function(a,b) {
var y0 = a;
var y1 = b;
var yd = b-a;
var k = 0.0001;
var positive = d3.scaleLog()
.domain([k,1])
.range([(y0 + y1)/2 ,y1])
var negative = d3.scaleLog()
.domain([-1,-k])
.range([y0, (y1 + y0)/2])
return function(t) {
t = (t - 0.5) * 2; // for an easy range of -1 to 1.
if (t > k) return positive(t);
if (t < -1 + k) return y0;
if (t < -k) return negative(t);
else return (y0 + y1) /2;
}
}
And then we can apply that to a regular old d3 linear scale:
d3.scaleLinear().interpolate(interpolator)...
This will interpolate numbers in the domain to the range as we've specified. It largely takes the above and adopts it for use as a d3 interpolator: a,b are the domain limits, t is a normalized domain between 0 and 1, and k defines the zeroish values. More on k below.
To get the ticks, assuming an nice round domain that only has nice round base ten numbers we could use:
// Set the ticks:
var ticks = [0];
scale.domain().forEach(function(d) {
while (Math.abs(d) >= 1) {
ticks.push(d); d /= 10;
}
})
Applying this we get:
var margin = {left: 40, top: 10, bottom: 10}
var width = 500;
var height = 300;
var svg = d3.select("body")
.append("svg")
.attr("width",width+margin.left)
.attr("height",height+margin.top+margin.bottom)
.append("g").attr("transform","translate("+[margin.left,margin.top]+")");
var data = d3.range(width).map(function(d) {
return (d - 250) * 4;
})
// Interpolate an output value:
var interpolator = function(a,b) {
var y0 = a;
var y1 = b;
var yd = b-a;
var k = 0.0001;
var positive = d3.scaleLog()
.domain([k,1])
.range([(y0 + y1)/2 ,y1])
var negative = d3.scaleLog()
.domain([-1,-k])
.range([y0, (y1 + y0)/2])
return function(t) {
t = (t - 0.5) * 2; // for an easy range of -1 to 1.
if (t > k) return positive(t);
if (t < -1 + k) return y0;
if (t < -k) return negative(t);
else return (y0 + y1) /2;
}
}
// Create a scale using it:
var scale = d3.scaleLinear()
.range([height,0])
.domain([-1000,1000])
.interpolate(interpolator);
// Set the ticks:
var ticks = [0];
scale.domain().forEach(function(d) {
while (Math.abs(d) >= 1) {
ticks.push(d); d /= 10;
}
})
// Apply the scale:
var line = d3.line()
.y(function(d) { return scale(d) })
.x(function(d,i) { return (i); })
// Draw a line:
svg.append("path")
.attr("d", line(data) )
.attr("class","line");
// Add an axis:
var axis = d3.axisLeft()
.scale(scale)
.tickValues(ticks)
svg.append("g").call(axis);
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Modifying the k value
Ok, what's up about k. It is needed to set the zeroish values. k also changes the shape of the graph. If showing regularly spaced ticks, increasing k ten fold increases the magnitude of the minimum magnitude ticks (other than zero) ten fold. In my exmaples above multiplying k by ten pushes the ticks with magnitude one overtop of the zero tick. Dividing it by ten would create room for a 0.1 tick (of course that requires modifying the tick generator to show that tick). k is hard to explain so I hope I did ok there.
I'll demonstrate to try and communicate it a bit better. Let's set the minimum magnitude ticks to be 0.1 using the above, we'll want to modify the tick function and k:
var margin = {left: 40, top: 10, bottom: 10}
var width = 500;
var height = 300;
var svg = d3.select("body")
.append("svg")
.attr("width",width+margin.left)
.attr("height",height+margin.top+margin.bottom)
.append("g").attr("transform","translate("+[margin.left,margin.top]+")");
var data = d3.range(width).map(function(d) {
return (d - 250) * 4;
})
// Interpolate an output value:
var interpolator = function(a,b) {
var y0 = a;
var y1 = b;
var yd = b-a;
var k = 0.00001;
var positive = d3.scaleLog()
.domain([k,1])
.range([(y0 + y1)/2 ,y1])
var negative = d3.scaleLog()
.domain([-1,-k])
.range([y0, (y1 + y0)/2])
return function(t) {
t = (t - 0.5) * 2; // for an easy range of -1 to 1.
if (t > k) {return positive(t)};
if (t < -1 + k) return y0;
if (t < -k) return negative(t);
else return (y0 + y1) /2 //yd;
}
}
// Create a scale using it:
var scale = d3.scaleLinear()
.range([height,0])
.domain([-1000,1000])
.interpolate(interpolator);
// Set the ticks:
var ticks = [0];
scale.domain().forEach(function(d) {
while (Math.abs(d) >= 0.1) {
ticks.push(d); d /= 10;
}
})
// Apply the scale:
var line = d3.line()
.y(function(d) { return scale(d) })
.x(function(d,i) { return (i); })
// Draw a line:
svg.append("path")
.attr("d", line(data) )
.attr("class","line");
// Add an axis:
var axis = d3.axisLeft()
.scale(scale)
.tickValues(ticks)
.ticks(10,".1f")
svg.append("g").call(axis);
.line {
fill: none;
stroke: steelblue;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
If you have a domain of +/- 1000, and you want the minimum magnitude tick to be 1 (not including zero) you want a k of 0.0001, or 0.1/1000.
If positive and negative limits of the domain are different, then we would need two k values, one for the negative cut off and one for the positive.
Lastly,
k sets values that are zeroish, in my example, t values that are between -k and +k are set to be the same - 0. Ideally this won't be many values in the dataset, but if it is, you might get a line such as:
Each input value is different, but the there are many zero output values, producing a visual artifact due to the bounds of what I haved considered to be zeroish. If there is only one value in the zeroish bounds, like in my examples above (but not the above picture), we get a much nicer:
I'm interested in tweaking the radius of the circles on the circle pack layout. For that I need to know how the original radius is calculated.
By reading the d3.js source code for pack layout it seems the default radius function is simply Math.sqrt of value for each node. But that is not really the case because I modified the D3.js original circle pack example adding a .radius(function(d){return Math.sqrt(d);}) and as you can see at bl.ocks.org/ecerulm/f0a36710e3 the radius of the circles are not the same.
The d3.layout.pack() uses Math.sqrt as radius function. But pack.nodes will apply a scale transform d3_layout_packTransform(node, x, y, k) to make the whole circle pack chart to fit if radius wasn't explicitly set. That is why if you apply you own function (even if its radius(Math.sqrt)) you will need to apply your own scaling after if you want to get the same result as with implicit radius.
In the example below I explicitly set Math.sqrt as the radius function and then scale afterward to fit [diameter,diameter] with my own function pack_transform since d3_layout_packTranform is not accesible:
var pack = d3.layout.pack()
.value(function(d) { return d.size; })
.radius(Math.sqrt)
.size([diameter - 4, diameter - 4]);
var packnodes = pack.nodes(root);
var packroot = packnodes[0];
var w = diameter, h = diameter;
function pack_transform(node, k) {
function inner_transform(node,cx,cy,k) {
var children = node.children;
node.x = cx + k * (node.x-cx);
node.y = cy + k * ( node.y-cy);
node.r *= k;
if (children) {
var i = -1, n = children.length;
while (++i < n) inner_transform(children[i],cx,cy, k);
}
}
return inner_transform(node,node.x,node.y,k);
}
pack_transform(packroot, 1 / Math.max(2 * packroot.r / w, 2 * packroot.r / h));