Unwanted behavior when using insidetextorientation = 'radial' in plotly sunburst - r
When using insidetextorientation = 'radial', even in a very simple plotly sunburst chart, some of the labels are grossly displaced.
library(plotly)
my_labels <- c("A","B","C","AA","AB","AC","BA","BB","CA","CB","CC")
my_parents <- c("","","","A","A","A","B","B","C","C","C")
my_values <- c(15,23,7,3,5,7,6,17,1,2,4)
plot_ly(labels = my_labels,parents = my_parents,values = my_values,type = 'sunburst',
branchvalues = 'total',insidetextorientation = 'radial')
Notice the "B" and "BA" on the left are misplaced. Similar issues occur when zooming in on any of the parents and do not persist with insidetextorientation = 'auto'. Is there a way to fix this?
On the plotly community forum the same question was asked 2 years ago, but not answered.
This isn't a great answer. It works, but when you click out of a parent so that everything is showing again, the plot has to stop moving before the labels will change back to the radial angles.
I've tried a lot of different ways to improve it. I think that letting Plotly package authors know is important; short of changing the code, I don't know of a better way to make this happen. The main problem is that the SVG paths that change when you enter and leave a parent are deleted and re-calculated. Any event that's attached is deleted at that point. (I tried both Plotly events and straight JS events.)
So instead of an event, it's on an interval. Every 100 milliseconds it checks to see if the labels need to be fixed.
plot_ly(labels = my_labels, parents = my_parents,
values = my_values, type = 'sunburst',
branchvalues = 'total', insidetextorientation = 'horizontal') %>%
htmlwidgets::onRender("function() {
pc = ['B', 'A', 'C', 'BB', 'BA', 'AC', 'AB', 'AA', 'CC', 'CB', 'CA'];
rpl = [0, -64, 28, -68, 28, -32, -80, 68, 40, 16, 4];
rx = /.*rotate\\((.*)\\)/g;
function fixer(){
tx = document.querySelectorAll('g > text');
if(tx.length === 11) { /*not when clicking subgroups*/
for(i=0;i<tx.length;i++){
wh = tx[i].getAttribute('data-unformatted');
tr = tx[i].getAttribute('transform');
rot = /.*rotate\\((.*)\\)/g.exec(tr);
if(rot !== null){ /*if a text rotation is designated*/
if(rpl[pc.indexOf(wh)] !== Number(rot)) {
rot = rot[1];
if(Number(rot) !== rpl[i] && wh === pc[i]){ /*if angle does not match & label does*/
beg = /(.*)rotate/.exec(tr)[1]; /*capture translate string*/
xy = beg + 'rotate(' + rpl[i] + ')'; /*build new transform string*/
tx[i].setAttribute('transform', xy); /*replace transform string with new*/
}
}
}
if(rot === null && wh === pc[i]) { /*if no rotation is present and label matches*/
str = tr + 'rotate(' + rpl[i] + ')'; /* build new transform string */
tx[i].setAttribute('transform', str); /*replace transform string with new*/
}
}
}
}
setInterval(fixer, 100);}") # check regularly! (every 100 ms)
I basically started with onRender set to just spit out the transform attribute for every label, so that I could get the order in which the labels appear (see pc in the JS) and the angles that the labels are placed at (see rpl in the JS). This was done while insidetextorientation = 'radial'. (I actually looked at all the text options: tangential, horizontal, auto, and radial.)
From there I tried MANY things to trigger the JS function fixer. In the end the only thing that was consistent enough for me to even share my work was using setInterval.
Update; Firefox friendly!
I really did try to figure this out for my initial answer. Here is the code without static arrays for the labels or angles. Let me know if you run into issues.
plot_ly(labels = my_labels, parents = my_parents,
values = my_values, type = 'sunburst',
branchvalues = 'total', insidetextorientation = 'horizontal') %>%
htmlwidgets::onRender("function(el, x) {
/* calculates the angles for each slice */
function angler (arVal, deg){ /* array of values in order; degrees each value unit*/
results = []; /* for storing the results */
theta = 0; /* for cumulative angle */
for(k = 0; k < arVal.length; k++){
here = arVal[k] * deg; /* units times degrees */
mrot = theta + (.5 * here); /* initial text angle */
if(mrot >= 90 && mrot < 270) {
mrot = mrot - 180; /* don't put text upside down */
}
if(mrot >= 270 && mrot < 360){
mrot = mrot - 360; /* don't put text upside down */
}
results[k] = mrot;
theta = theta + here; /* for the next loop */
}
return(results);
}
par = x.data[0].parents; /* collect the necessary data*/
val = x.data[0].values;
lab = x.data[0].labels;
parI = par.reduce((a, e, i) => { /*which rows have parents?*/
if (e == '') { a.push(i); };
return a;
}, []); /*in array a, if element e at pos i equates to ''*/
parC = par.reduce((a, e, i) => { /*which rows do NOT have parents?*/
if (e != '') { a.push(i); };
return a;
}, []); /*in array a, if element e at pos i equates to ''*/
alp = lab.map((i, j) => {return [i, val[j], par[j]]}); /* combine data */
palv = alp.filter((e, i) => parI.some(j => i === j)); /*parent array*/
calv = alp.filter((e, i) => parC.some(j => i === j)); /*children array*/
pvalS = palv.sort(function(a, b) {
return((a[1] > b[1]) ? -1 : ((a[1] == b[1]) ? 0 : 1));
}) /*parents sorted by values*/
parS = pvalS.map(function(j) {return j[0];}); /*extract ordered parents*/
csort = []; /* for ordered kids array*/
for(i = 0; i < parS.length; i++){ /* sort children by parent and size*/
arr = calv.filter(j => parS[i].includes(j[2]));
arr1 = arr.map(function(w) {return w[1]}); /*get just values*/
arr2 = arr1.sort(function(a, b){ return b - a });
bld = [];
for(k = 0; k < arr2.length; k++) {
arr3 = arr.filter(j => arr2[k] == j[1]);
csort = csort.concat(arr3);
}
}
/* get the order--- in reverse*/
cvo = csort.map(function(j){return j[1]});
cvov = Object.values(cvo).reverse();
pvo = pvalS.map(function(j) {return j[1];}); /*extract ordered parents' values*/
pvov = Object.values(pvo).reverse();
ctots = cvov.reduce((a, b) => a + b, 0); /* 45 */
rotV = 360/ctots; /* 8 in this example */
kids = angler(cvov, rotV); /* collect kids angles */
parents = angler(pvov, rotV); /* collect parent angles */
kidS = csort.map(function(j) {return j[0];}); /* extract kid labels in order */
/*pc = ['B', 'A', 'C', 'BB', 'BA', 'AC', 'AB', 'AA', 'CC', 'CB', 'CA'];*/
pc = Object.values(parS).concat(Object.values(kidS));
/*rpl = [0, -64, 28, -68, 28, -32, -80, 68, 40, 16, 4];*/
rpl = parents.reverse().concat(kids.reverse());
rx = /.*rotate\\((.*)\\)/g;
function fixer(){
tx = document.querySelectorAll('g > text');
if(tx.length === 11) { /*not when clicking subgroups*/
for(i=0;i<tx.length;i++){
wh = tx[i].getAttribute('data-unformatted');
tr = tx[i].getAttribute('transform');
rot = /.*rotate\\((.*)\\)/g.exec(tr);
if(rot !== null){ /*if a text rotation is designated*/
if(rpl[pc.indexOf(wh)] !== Number(rot)) {
rot = rot[1];
if(Number(rot) !== rpl[i] && wh === pc[i]){ /*if angle does not match & label does*/
beg = /(.*)rotate/.exec(tr)[1]; /*capture translate string*/
xy = beg + 'rotate(' + rpl[i] + ')'; /*build new transform string*/
tx[i].setAttribute('transform', xy); /*replace transform string with new*/
}
}
}
if(rot === null && wh === pc[i]) { /*if no rotation is present and label matches*/
str = tr + 'rotate(' + rpl[i] + ')'; /* build new transform string */
tx[i].setAttribute('transform', str); /*replace transform string with new*/
}
}
}
}
setInterval(fixer, 100);}") # check regularly! (every 100 ms)
Related
R Plotly ScatterPolar chart - how to rotate labels individually?
I'm trying to make a simple scatterpolar chart in R Plotly that has 60+ categorical labels on the radial axis. Because there's so many labels, the default method of placing the labels results in them overlapping each other at the top and bottom of the chart. I've tried setting layout.polar.angularaxis.tickangle to 90, -90, etc, but that rotates ALL labels by that amount. Is there a way to either A) rotate each individual label by a specified value such that I can set some labels to -90 degrees, others to -45, others to 0, others to +45, etc... or B) leave the label rotations at 0 (as they are now), but dodge them a bit so they don't overlap at the top and bottom of the chart? I think B would be easier and preferred, but not sure if either A or B are possible. Here's a minimum reproducible example: labs1 <- c("Red ", "Green ", "Blue ", "Yellow ", "Orange ", "Purple ", "Pink ", "Black ", "White ", "Gray ") labs2 <- c("Bookshelf", "Television", "Refridgerator", "Toolbox", "Xylophone" , "Chromosome") labs <- sample(paste0(rep(labs1, each = 6), rep(labs2, times = 10)), 60, replace = F) df <- data.frame(Label = labs, Radius = sample(0:100, 60, replace = F)) plot_ly(df) %>% add_trace(type = 'scatterpolar', mode = 'markers', r = ~Radius, theta = ~Label, fill = 'toself') And here's the result:
It's been a while since you asked, but I just ran across your question. I've got a workaround. If you'd like to duplicate the data I've used, I used set.seed(35) before calling the creation of df. This uses the library htmlwidgets in addition to plotly. I've added margin to the layout to account for label rotation. This may need to be modified. (For example, if you had really long labels, the margin would need to be larger.) In the onRender call, I have several things happening. The function detector: checks for labels that have collided with each other The function angler: extracts the tick angle the label is associated with The function quad: identifies which quadrant of the circle it's working with, then rotates the text accordingly lbls is a node list of every label in the plot. The for loop rotates through lbls, sending every two labels to the function detector. If a collision is detected, the indicators are sent to the angler function (which calls the quad function). It's not perfect, but it works. library(plotly) library(htmlwidgets) plot_ly(df, type = 'scatterpolar', mode = 'markers', r = ~Radius, theta = ~Label, fill = 'toself') %>% layout(margin = list(t = 75, r = 75, b = 75, l = 75)) %>% # make room for rotated labels onRender( "function(el, x) { function detector(r1, r2) { var r1 = r1.getBoundingClientRect(); /* catch associated real estate */ var r2 = r2.getBoundingClientRect(); return !(r2.left > r1.right || /* check for collisions */ r2.right < r1.left || r2.top > r1.bottom || r2.bottom < r1.top); } function angler(ind) { /* get tick angle to rotate label */ var par = lbls[ind].parentNode.parentNode.children[ind]; var parAt = par.getAttribute('transform'); /* send extracted angle to quad function*/ quad(/(?<=rotate\\()[^)]*/.exec(parAt)[0], ind); /* using regex, after 'rotate(' -- except ')' */ } function quad(ang, ind) { var ang = Math.abs(Number(ang)); /* make the angle a positive number */ var gimmeXform = lbls[ind].getAttribute('transform'); /* extract values to modify */ var xer = lbls[ind].getAttribute('x'); /* 3rd & 4th quad, ctrl rotation pt */ var yer = lbls[ind].getAttribute('y'); /* 3rd & 4th quad, ctrl rotation pt */ if(ang <= 90) { tang = ang * -.3; /* rotate ang * -x, text-anchor: start */ tanch = 'start'; } else if (ang > 90 && ang <= 180) { tang = (180 - ang) * 0.3; /* rotate ang * +x, text-anchor: end */ tanch = 'end'; } else if (ang > 180 && ang < 270) { tang = (ang - 180) * -.3; /* rotate ang * -x, text-anchor: end */ tang = '' + tang + ',' + xer + ',' + yer; /* add rotation controls*/ tanch = 'end'; } else { tang = (360 - ang) * 0.3; /* rotate ang * +x, text-anchor: start */ tang = '' + tang + ',' + xer + ',' + yer; /* add rotation controls*/ tanch = 'start'; } /* rotate text */ lbls[ind].setAttribute('transform', gimmeXform + 'rotate(' + tang + ')'); lbls[ind].setAttribute('text-anchor', tanch); } lbls = document.querySelectorAll('g.angularaxistick > text'); inds = []; for(i = 0; i < lbls.length; i+=2) { if(detector(lbls[i], lbls[i + 1])) { /* if collision detected */ angler(i); angler(i + 1); } } }" ) Whether you've plotted it with a ton of real estate: Or very little real estate, it alters the labels: FYI: if all of the labels overlap, it won't work.
Calculate other angles based on known rotation angle
I am making a mechanical system animation on OpenGL and having a little trouble calculating the rotations angle of the connecting rods based on a known rotation angle A and the position of the point D. I need to calculate the angle CDE and CBG as well as the position of point E based on angle A and the position of D. But my high school is failing me right now. I have tried several ways but they all lead to nothing. The length of segment DA is also known. Do you have any ideas on how to do that? What should I do?
I have had to make a few assumptions, and while I was finding a solution I forgot to check the labels so below is an image to clarify point and line name, including in red the geometry used to solve. Assumptions. Points A and B are fixed. Lines BC, FC, and DC are all the same length L Point D is constrained to the line EG Angle not labeled is the angle you refer to in the question. The point F is on the circle centered at A. I forgot to label the radius and angle. Point A is at the origin {x: 0, y: 0} I also assume that you know the basics of vector math and that the problem is not finding the angle between lines or vectors, but rather solving to find the points C and D that is giving you troubles (hope so as this is going to be a long answer for me). Solving Depending on the value of L and the position of the constraining line EG there may not be a solution for all positions of F. The method below will result in either some values being NaN or the position of D will be incorrect. Find C Easy start. As A is at the origin then F is at F.x = cos(angle) * radius, F.y = sin(angle) * radius Now find the mid m point on the line FB and the length of the line Bm as b This forms the right triangle mBC and we know the length of BC === L and just calculated length of line Bm === b thus the length of the line mC is (L * L - b * b) ** 0.5 Create a unit vector (normalized) from F to B, rotate it clockwise 90 deg and scale it by the calculated length of mC. Add that vector to the point m and you have C // vector nx = B.x - F.x; ny = B.y - F.y; // Normalize, scale, rotate and add to m to get C. shorthand // mC len of line mC s = mC / (nx * nx + ny * ny) ** 0.5; C.x = m.x - ny * s; C.y = m.y + nx * s; // OR in steps // normalize len = (nx * nx + ny * ny) ** 0.5; nx /= len; ny /= len; // scale to length of mC nx *= mC; ny *= mC; // rotated 90CW and add to m to get C C.x = m.x - ny; C.y = m.y + nx; Find D Now that we have the point C we know that the point D is on the constraining line EG. Thus we know that the point D is at the point where a circle at C or radius L intercepts the line EG However there are two solutions to the intercept of circle and line, the point B is at one of these points if B is on the line EG. If B is not on the line EG then you will have to pick which of the two solutions you want. Likely the point D is the furthest from B There are several methods to find the intercepts of a line and a circle. The following is a little more complex but will help when picking which point to use // line EG as vec vxA = G.x - E.x; vyA = G.y - E.y; // square of length line EG lenA = vxA * vxA + vyA * vyA; // vector from E to C vxB = C.x - E.x; vyB = C.y - E.y; // square of length line EC lenB = vxB * vxB + vyB * vyB; // dot product A.B * - 2 b = -2 * (vxB * vxA + vyB * vyA); // Stuff I forget what its called d = (b * b - 4 * lenA * (lenB - L * L)) ** 0.5; // L is length of CD // is there a solution if not we are done if (isNaN(d)) { return } // there are two solution (even if the same point) // Solutions as unit distances along line EG u1 = (b - d) / (2 * lenA); u2 = (b + d) / (2 * lenA); // this is the one we want The second unit distance is the one that will fit your layout example. So now we just find the point at u2 on the line EG and we have the final point D D.x = E.x + u2 * (G.x - E.x); D.y = E.y + u2 * (G.y - E.y); The angles In your question it is a little ambiguous to me which angles you want. So I will just give you a method to find the angle between to lines. Eg CB and CD Convert both lines to vectors. The cross product of these vectors divided by the square root of the product of the squared lengths gives us the sin of the angle. However we still need the quadrant. We workout which quadrant by checking the sign of the dot product of the two vectors. Note this method will find the the smallest angle between the two lines and is invariant to the order of the lines Note the angle is in radians // vector CB xA = B.x - C.x; yA = B.y - C.y; // vector CD xB = D.x - C.x; yB = D.y - C.y; // square root of the product of the squared lengths l = ((xa * xa + ya * ya) * (xb * xb + yb * yb)) ** 0.5; // if this is 0 then angle between lines is 0 if (l === 0) { return 0 } // return angle angle = Math.asin((xa * yb - ya * xb) / l); // get angle quadrant undefined // if dot of the vectors is < 0 then angle is in quadrants 2 or 3. get angle and return if (xa * xb + ya * yb < 0) { return (angle< 0 ? -Math.PI: Math.PI) - angle; } // else the angle is in quads 1 or 4 so just return the angle return angle; DONE To make sure it all worked I have created an interactive diagram. The code of interest is at the top. Variables names are as in my diagram at top of answer. Most of the code is just cut and paste vector libs and UI stuff unrelated to the answer. To use The diagram will scale to fit the page so click full page if needed. Use the mouse to drag points with white circles around. For example to rotate F around A click and drag it. The white line segment El sets the length of the lines CF, CB, CD. The radius of circle at A is set by moving the white circle point to the right of it. Move mouse out of form to animate. Mouse only interface. Overkill but its done. setTimeout(() => { // points and lines as in diagram of answer const A = new Vec2(-100,100); const B = new Vec2(-240, - 100); const C = new Vec2(); const D = new Vec2(); const E = new Vec2(-300, -100); const F = new Vec2(); const G = new Vec2(200, -100); const AF = new Line2(A, F), FA = new Line2(F, A); const BC = new Line2(B, C), CB = new Line2(C, B); const CD = new Line2(C, D), DC = new Line2(D, C); const EG = new Line2(E, G), GE = new Line2(G, E); const FB = new Line2(F, B), BF = new Line2(B, F); const FC = new Line2(F, C), CF = new Line2(C, F); // Math to find points C and D function findCandD() { F.initPolar(angle, radius).add(A) // Get position of F FB.unitDistOn(0.5, m); // Find point midway between F, B, store as m // Using right triangle m, B, C the hypot BC length is L var c = (FB.length * 0.5) ** 2; // Half the length of FB squared const clLen = (L * L - c) ** 0.5 // Length of line mC FB.asVec(v1).rotate90CW().length = clLen; // Create vector v1 at 90 from FB and length clLen C.init(m).add(v1); // Add v1 to m to get point C const I = EG.unitInterceptsCircle(C, L, cI); // Point D is L dist from if (EG.unitInterceptsCircle(C, L, cI)) { // Point D is L dist from C. thus us the intercept of corcle radius L and constraining line EG EG.unitDistanceOn(cI.y, D) // Use second intercept as first could be at point B } else { C.x = NaN } // C is too far from constraining line EG for a solution // At this point, the line CD may be the wrong length. Check the length CD is correct blk = Math.isSmall(CD.length - L) ? black : red; // Mark all in red if no solution } // Here on down UI, and all the support code requestAnimationFrame(update); const refRes = 512; var scale = 1; const mousePos = new Vec2(); var w = 0, h = 0, cw = 0, ch = 0; var frame = 0; const m = new Vec2(); // holds mid point on line BF const m1 = new Vec2(); const v1 = new Vec2(); // temp vector const v2 = new Vec2(); // temp vector const cI = new Vec2(); // circle intercepts var radius = 100; var L = 200 var angle = 1; const aa = new Vec2(A.x + radius, A.y); const al = new Vec2(E.x + L, E.y); const rad = new Line2(A, aa); const cl = new Line2(m, C) const armLen = new Line2(E, al); var blk = "#000" const wht = "#FFF" const red = "#F00" const black = "#000" const ui = Vecs2([A, B, aa, E, G, al, F]) function update(timer){ frame ++; ctx.setTransform(1,0,0,1,0,0); // reset transform if (w !== innerWidth || h !== innerHeight){ cw = (w = canvas.width = innerWidth) / 2; ch = (h = canvas.height = innerHeight) / 2; scale = Math.min(w / refRes, h / refRes); } else { ctx.clearRect(0, 0, w, h); } ctx.clearRect(0, 0, canvas.width, canvas.height); mousePos.init(mouse); mousePos.x = (mousePos.x - canvas.width / 2) / scale; mousePos.y = (mousePos.y -canvas.height / 2) / scale; mousePos.button = mouse.button; ctx.font = "24px Arial black" ctx.textAlign = "center"; ctx.setTransform(scale,0,0,scale,canvas.width / 2, canvas.height / 2); const nearest = ui.dragable(mousePos, 20); if (nearest === A) { aa.y = A.y aa.x = A.x + radius; } else if(nearest === F){ angle = A.directionTo(F); } else if(nearest === aa){ aa.y = A.y radius = rad.length; } else if (nearest === E) { EG.distanceAlong(L, al) } else if (nearest === G || nearest === al) { EG.nearestOnLine(al, al) L = armLen.length; } if (nearest) { canvas.style.cursor = ui.dragging ? "none" : "move"; nearest.draw(ctx, "#F00", 2, 4); if (nearest.isLine2) { nearest.nearestOnLine(mousePos, onLine).draw(ctx, "#FFF", 2, 2) } } else { canvas.style.cursor = "default"; } angle += SPEED; findCandD(); ui.mark(ctx, wht, 1, 4); ui.mark(ctx, wht, 1, 14); armLen.draw(ctx,wht,2) EG.draw(ctx, wht, 1) ctx.fillStyle = wht; ctx.fillText("E", E.x, E.y - 16) ctx.fillText("G", G.x, G.y - 16) ctx.fillText("l", armLen.p2.x, armLen.p2.y - 16) FC.draw(ctx, blk, 4) BC.draw(ctx, blk, 4) CD.draw(ctx, blk, 4) A.draw(ctx, blk, 2, radius); C.draw(ctx, blk, 4, 4) F.draw(ctx, blk, 4, 4) B.draw(ctx, blk, 4, 4); D.draw(ctx, blk, 4, 4) ctx.fillStyle = blk; ctx.fillText("B", B.x, B.y - 16) ctx.fillText("A", A.x, A.y - 16) ctx.fillText("F", F.x, F.y + 26) ctx.fillText("D", D.x, D.y - 16) ctx.fillText("C", C.x, C.y - 16) ctx.font = "16px Arial"; drawAngle(C, CD, CB, 40, B.add(Vec2.Vec(60, -50), Vec2.Vec()), ctx, blk, 2); drawAngle(C, CF, CB, 50, A.add(Vec2.Vec(-160, 0), Vec2.Vec()), ctx, blk, 2); drawAngle(C, CD, CF, 60, A.add(Vec2.Vec(300, 20), Vec2.Vec()), ctx, blk, 2); blk = Math.isSmall(CD.length - L) ? black : red; requestAnimationFrame(update); } }, 0); const ctx = canvas.getContext("2d"); const mouse = {x: 0, y: 0, ox: 0, oy: 0, button: false, callback: undefined} function mouseEvents(e) { const bounds = canvas.getBoundingClientRect(); mouse.x = e.pageX - bounds.left - scrollX; mouse.y = e.pageY - bounds.top - scrollY; mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button; } ["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents)); var SPEED = 0.05; canvas.addEventListener("mouseover",() => SPEED = 0); canvas.addEventListener("mouseout",() => SPEED = 0.05); Math.EPSILON = 1e-6; Math.isSmall = val => Math.abs(val) < Math.EPSILON; Math.isUnit = u => !(u < 0 || u > 1); Math.uClamp = u => u <= 0 ? 0 : u >= 1 ? 1 : u; // almost 2* faster than Math.min, Math.Max method Math.TAU = Math.PI * 2; Math.rand = (m, M) => Math.random() * (M - m) + m; Math.randI = (m, M) => Math.random() * (M - m) + m | 0; Math.rad2Deg = r => r * 180 / Math.PI; Math.symbols = {}; Math.symbols.degrees = "°"; /* export {Vec2, Line2} */ // this should be a module var temp; function Vec2(x = 0, y = (temp = x, x === 0 ? (x = 0 , 0) : (x = x.x, temp.y))) { this.x = x; this.y = y } Vec2.Vec = (x, y) => ({x, y}); // Vec2 like Vec2.prototype = { isVec2: true, init(x, y = (temp = x, x = x.x, temp.y)) { this.x = x; this.y = y; return this }, // assumes x is a Vec2 if y is undefined initPolar(dir, length = (temp = dir, dir = dir.x, temp.y)) { this.x = Math.cos(dir) * length; this.y = Math.sin(dir) * length; return this }, toPolar(res = this) { const dir = this.direction, len = this.length; res.x = dir; res.y = length; return res; }, zero() { this.x = this.y = 0; return this }, initUnit(dir) { this.x = Math.cos(dir); this.y = Math.sin(dir); return this }, copy() { return new Vec2(this) }, equal(v) { return (this.x - v.x) === 0 && (this.y - v.y) === 0 }, isUnits() { return Math.isUnit(this.x) && Math.isUnit(this.y) }, add(v, res = this) { res.x = this.x + v.x; res.y = this.y + v.y; return res }, addScaled(v, scale, res = this) { res.x = this.x + v.x * scale; res.y = this.y + v.y * scale; return res }, sub(v, res = this) { res.x = this.x - v.x; res.y = this.y - v.y; return res }, scale(val, res = this) { res.x = this.x * val; res.y = this.y * val; return res }, invScale(val, res = this) { res.x = this.x / val; res.y = this.y / val; return res }, dot(v) { return this.x * v.x + this.y * v.y }, uDot(v, div) { return (this.x * v.x + this.y * v.y) / div }, cross(v) { return this.x * v.y - this.y * v.x }, uCross(v, div) { return (this.x * v.y - this.y * v.x) / div }, get direction() { return Math.atan2(this.y, this.x) }, set direction(dir) { this.initPolar(dir, this.length) }, get length() { return this.lengthSqr ** 0.5 }, set length(l) { this.scale(l / this.length) }, get lengthSqr() { return this.x * this.x + this.y * this.y }, set lengthSqr(lSqr) { this.scale(lSqr ** 0.5 / this.length) }, distanceFrom(vec) { return ((this.x - vec.x) ** 2 + (this.y - vec.y) ** 2) ** 0.5 }, distanceSqrFrom(vec) { return ((this.x - vec.x) ** 2 + (this.y - vec.y) ** 2) }, directionTo(vec) { return Math.atan2(vec.y - this.y, vec.x - this.x) }, normalize(res = this) { return this.invScale(this.length, res) }, rotate90CW(res = this) { const y = this.x; res.x = -this.y; res.y = y; return res; }, angleTo(vec) { const xa = this.x, ya = this.y; const xb = vec.x, yb = vec.y; const l = ((xa * xa + ya * ya) * (xb * xb + yb * yb)) ** 0.5; var ang = 0; if (l !== 0) { ang = Math.asin((xa * yb - ya * xb) / l); if (xa * xb + ya * yb < 0) { return (ang < 0 ? -Math.PI: Math.PI) - ang } } return ang; }, drawFrom(v, ctx, col = ctx.strokeStyle, lw = ctx.lineWidth, scale = 1) { ctx.strokeStyle = col; ctx.lineWidth = lw; ctx.beginPath(); ctx.lineTo(v.x, v.y); ctx.lineTo(v.x + this.x * scale, v.y + this.y * scale); ctx.stroke(); }, draw(ctx, col = ctx.strokeStyle, lw = ctx.lineWidth, size = 4) { ctx.strokeStyle = col; ctx.lineWidth = lw; ctx.beginPath(); ctx.arc(this.x, this.y, size, 0, Math.TAU); ctx.stroke(); }, path(ctx, size) { ctx.moveTo(this.x + size, this.y); ctx.arc(this.x, this.y, size, 0, Math.TAU); }, toString(digits = 3) { return "{x: " + this.x.toFixed(digits) + ", y: " + this.y.toFixed(digits) + "}" }, }; function Vecs2(vecsOrLength) { const vecs2 = Object.assign([], Vecs2.prototype); if (Array.isArray(vecsOrLength)) { vecs2.push(...vecsOrLength) } else if (vecsOrLength && vecsOrLength >= 1) { while (vecsOrLength-- > 0) { vecs2.push(new Vec2()) } } return vecs2; } Vecs2.prototype = { isVecs2: true, nearest(vec, maxDist = Infinity, tolerance = 1) { // max for argument semantic, used as semantic min in function var found; for (const v of this) { const dist = v.distanceFrom(vec); if (dist < maxDist) { if (dist <= tolerance) { return v } maxDist = dist; found = v; } } return found; }, copy() { var idx = 0; const copy = Vecs2(this.length); for(const p of this) { copy[idx++].init(p) } return copy; }, uniformTransform(rMat, pMat, res = this) { var idx = 0; for(const p of this) { p.uniformTransform(rMat, pMat, res[idx++]) } }, mark(ctx, col = ctx.strokeStyle, lw = ctx.lineWidth, size = 4) { ctx.strokeStyle = col; ctx.lineWidth = lw; ctx.beginPath(); for (const p of this) { p.path(ctx, size) } ctx.stroke(); }, draw(ctx, close = false, col = ctx.strokeStyle, lw = ctx.lineWidth) { ctx.strokeStyle = col; ctx.lineWidth = lw; ctx.beginPath(); for (const p of this) { ctx.lineTo(p.x, p.y) } close && ctx.closePath(); ctx.stroke(); }, path(ctx, first = true) { for (const p of this) { if (first) { first = false; ctx.moveTo(p.x, p.y); } else { ctx.lineTo(p.x, p.y) } } }, dragable(mouse, maxDist = Infinity, tolerance = 1) { var near; if (this.length) { if (!this.dragging) { if (!this.offset) { this.offset = new Vec2() } near = this.nearest(this.offset.init(mouse), maxDist, tolerance); // mouse may not be a Vec2 if (near && mouse.button) { this.dragging = near; this.offset.init(near).sub(mouse); } } if (this.dragging) { near = this.dragging; if (mouse.button) { this.dragging.init(mouse).add(this.offset) } else { this.dragging = undefined } } } return near; } } function Line2(p1 = new Vec2(), p2 = (temp = p1, p1 = p1.p1 ? p1.p1 : p1, temp.p2 ? temp.p2 : new Vec2())) { this.p1 = p1; this.p2 = p2; } Line2.prototype = { isLine2: true, init(p1, p2 = (temp = p1, p1 = p1.p1, temp.p2)) { this.p1.init(p1); this.p2.init(p2) }, copy() { return new Line2(this) }, asVec(res = new Vec2()) { return this.p2.sub(this.p1, res) }, unitDistOn(u, res = new Vec2()) { return this.p2.sub(this.p1, res).scale(u).add(this.p1) }, unitDistanceOn(u, res = new Vec2()) { return this.p2.sub(this.p1, res).scale(u).add(this.p1) }, distAlong(dist, res = new Vec2()) { return this.p2.sub(this.p1, res).uDot(res, res.length).add(this.p1) }, distanceAlong(dist, res = new Vec2()) { return this.p2.sub(this.p1, res).scale(dist / res.length).add(this.p1) }, get length() { return this.lengthSqr ** 0.5 }, get lengthSqr() { return (this.p1.x - this.p2.x) ** 2 + (this.p1.y - this.p2.y) ** 2 }, get direction() { return this.asVec(wV2).direction }, translate(vec, res = this) { this.p1.add(vec, res.p1); this.p2.add(vec, res.p2); return res; }, reflect(line, u, res = line) { this.asVec(wV2).normalize(); line.asVec(wV1); line.unitDistOn(u, res.p1); const d = wV1.uDot(wV2, 0.5); wV3.init(wV2.x * d - wV1.x, wV2.y * d - wV1.y); res.p1.add(wV3.scale(1 - u), res.p2); return res; }, reflectAsUnitVec(line, u, res = new Vec2()) { this.asVec(res).normalize(); line.asVec(wV1); return res.scale(wV1.uDot(res, 0.5)).sub(wV1).normalize() }, angleTo(line) { return this.asVec(wV1).angleTo(line.asVec(wV2)) }, translateNormal(amount, res = this) { this.asVec(wV1).rot90CW().length = -amount; this.translate(wV1, res); return res; }, distanceNearestVec(vec) { // WARNING!! distanceLineFromVec is (and others are) dependent on vars used in this function return this.asVec(wV1).uDot(vec.sub(this.p1, wV2), wV1.length); }, unitNearestVec(vec) { // WARNING!! distanceLineFromVec is (and others are) dependent on vars used in this function return this.asVec(wV1).uDot(vec.sub(this.p1, wV2), wV1.lengthSqr); }, nearestOnLine(vec, res = new Vec2()) { return this.p1.addScaled(wV1, this.unitNearestVec(vec), res) }, nearestOnSegment(vec, res = new Vec2()) { return this.p1.addScaled(wV1, Math.uClamp(this.unitNearestVec(vec)), res) }, distanceLineFromVec(vec) { return this.nearestOnLine(vec, wV1).sub(vec).length }, distanceSegmentFromVec(vec) { return this.nearestOnSegment(vec, wV1).sub(vec).length }, unitInterceptsLine(line, res = new Vec2()) { // segments this.asVec(wV1); line.asVec(wV2); const c = wV1.cross(wV2); if (Math.isSmall(c)) { return } wV3.init(this.p1).sub(line.p1); res.init(wV1.uCross(wV3, c), wV2.uCross(wV3, c)); return res; }, unitInterceptsCircle(point, radius, res = new Vec2()) { this.asVec(wV1); var b = -2 * this.p1.sub(point, wV2).dot(wV1); const c = 2 * wV1.lengthSqr; const d = (b * b - 2 * c * (wV2.lengthSqr - radius * radius)) ** 0.5 if (isNaN(d)) { return } return res.init((b - d) / c, (b + d) / c); }, draw(ctx, col = ctx.strokeStyle, lw = ctx.lineWidth) { ctx.strokeStyle = col; ctx.lineWidth = lw; ctx.beginPath(); ctx.lineTo(this.p1.x, this.p1.y); ctx.lineTo(this.p2.x, this.p2.y); ctx.stroke(); }, path(ctx) { ctx.moveTo(this.p1.x, this.p1.y); ctx.lineTo(this.p2.x, this.p2.y); }, toString(digits = 3) { return "{ p1: " + this.p1.toString(digits) + ", p2: " + this.p2.toString(digits) + "}" }, }; const wV1 = new Vec2(), wV2 = new Vec2(), wV3 = new Vec2(); // pre allocated work vectors used by Line2 functions const wVA1 = new Vec2(), wVA2 = new Vec2(), wVA3 = new Vec2(); // pre allocated work vectors const wVL1 = new Vec2(), wVL2 = new Vec2(), wVL3 = new Vec2(); // pre allocated work vectors used by Line2Array functions const wL1 = new Line2(), wL2 = new Line2(), wL3 = new Line2(); // pre allocated work lines function drawLable(text, from, to, ctx, col = ctx.strokeStyle, lw = ctx.lineWidth) { ctx.fillStyle = ctx.strokeStyle = col; ctx.lineWidth = lw; ctx.beginPath(); ctx.lineTo(from.x, from.y); ctx.lineTo(to.x, to.y); ctx.stroke(); const w = ctx.measureText(text).width; var offset = 8; if (from.x < to.x) { ctx.fillText(text, to.x + offset + w / 2, to.y) } else { ctx.fillText(text, to.x - offset - w / 2, to.y) } } function drawAngle(pos, lineA, lineB, radius, lablePos, ctx, col = ctx.strokeStyle, lw = ctx.lineWidth) { ctx.strokeStyle = col; ctx.lineWidth = lw; const from = lineA.direction; const angle = lineA.angleTo(lineB); ctx.beginPath(); ctx.arc(pos.x, pos.y, radius, from, from + angle, angle < 0); ctx.stroke(); drawLable( Math.rad2Deg(angle).toFixed(2) + Math.symbols.degrees, Vec2.Vec( pos.x + Math.cos(from + angle / 2) * radius, pos.y + Math.sin(from + angle / 2) * radius ), lablePos, ctx, col, lw / 2, ); } canvas { position : absolute; top : 0px; left : 0px; background: #4D8; } <canvas id="canvas"></canvas>
how to dragging Threejs point
I'm trying to huge graph visualization with threejs r86(latest master version), for showing 600,000 nodes I found a way to draw them faster than using mesh with THREE.points but know I need to make them draggable, after many searches I found raycast to found closest object to mouse point but I have a problem becouse all of taht points are just an object and can not be changed seperately. function Graph3(Nodes, Edges) { this.renderer = new THREE.WebGLRenderer({ alpha: true}); var width = window.innerWidth , height = window.innerHeight; this.renderer.setSize(width, height, false); document.body.appendChild(this.renderer.domElement); this.scene = new THREE.Scene(), this.camera = new THREE.PerspectiveCamera(100, width / height, 0.1, 3000), this.controls = new THREE.OrbitControls(this.camera); this.controls.enableKeys = true; this.controls.enableRotate = false; var material, geometry; self = this; material = new THREE.LineBasicMaterial({color: '#ccc'}); geometry = new THREE.Geometry(); geometry.vertices = Nodes.map(function(item){return new THREE.Vector3(item.pos.x,item.pos.y,item.pos.z);}); // this.vertices = geometry.vertices; this.line = new THREE.LineSegments(geometry, material); this.scene.add(this.line); var Node = new THREE.Group(); material = new THREE.PointsMaterial( { color:0x000060 ,size:1 } ); this.particles = new THREE.Mesh(geometry,material) this.particles = new THREE.Points( geometry, material); this.scene.add( this.particles ); dragControls = new THREE.DragControls([this.particles], this.camera/*,this.scene*/, this.renderer.domElement); this.camera.position.z = 200; var raycaster = new THREE.Raycaster(); var mouse = new THREE.Vector2(); document.addEventListener( 'click', function ( event ) { // calculate mouse position in normalized device coordinates // (-1 to +1) for both components mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1; mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1; console.log(mouse); }, false ); stats = new Stats(); document.body.appendChild(stats.dom); this.animate = function() { raycaster.setFromCamera( mouse, self.camera ); var intersections = raycaster.intersectObject( self.particles ); intersection = ( intersections.length ) > 0 ? intersections[ 0 ] : null; if ( intersection !== null) { console.log(intersection); } requestAnimationFrame( self.animate ); stats.update(); self.renderer.render(self.scene, self.camera); } this.animate();} I had able to change all the points with dragControls but can't move them seperatly I had found EventsControls.js file which help us to handle events but I couldn't use it
Here you can check how to target individual parts of a buffer geometry with a raycaster: https://github.com/mrdoob/three.js/blob/master/examples/webgl_interactive_buffergeometry.html As for moving them, refer to this question and answer: How to quickly update a large BufferGeometry?
Thanks for helping me in previous question. I am making my points in 2d plane (z = 0) and I could making them with bufferGeometry and RawShaderMaterial but now I have another problem in dragging them, how raycaster do? it need vec3 positions but I have changed it for performance purpose. var Geo = new THREE.BufferGeometry(); var position = new Float32Array( NodeCount * 2 ); var colors = new Float32Array( NodeCount * 3 ); var sizes = new Float32Array( NodeCount ); for ( var i = 0; i < NodeCount; i++ ) { position[ 2*i ] = (Math.random() - 0.5) * 10; position[ 2*i + 1 ] = (Math.random() - 0.5) * 10; colors[ 3*i ] = Math.random(); colors[3*i+1] = Math.random(); colors[3*i+2] = Math.random(); // sizes sizes[i] = Math.random() * 5 ; } Geo.addAttribute( 'position', new THREE.BufferAttribute( position, 2 ) ); Geo.addAttribute( 'color', new THREE.BufferAttribute( colors, 3 ) ); Geo.addAttribute( 'size', new THREE.BufferAttribute( sizes, 1 ) ); points = new THREE.Points( Geo, new THREE.RawShaderMaterial({ vertexShader:` precision highp float; uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; uniform vec3 cameraPosition; attribute vec2 position; /// reason of problem varying vec3 vColor; attribute vec3 color; attribute float size; void main() { vColor = color; gl_PointSize = size; gl_Position = projectionMatrix * modelViewMatrix * vec4( position , 0, 1 ); }`, fragmentShader:` precision highp float; varying vec3 vColor; void main() { gl_FragColor = vec4( vColor, 1.0 ) ; }` }) ); scene.add( points ); and my using of raycaster: function mouseDown(e) { e.preventDefault(); var mouse = new THREE.Vector2(); mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1; mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1; // mouse.z = 0; raycaster.setFromCamera(mouse, camera); raycaster.far = camera.position.z + 3; const intersect = raycaster.intersectObject(points); console.log(intersect); if (intersect.length > 0) { controls.enabled = false; console.log(intersect); selection = intersect[0].index; } } function mouseUp(e) { controls.enabled = true; var vector = new THREE.Vector3(); vector.x = (( event.clientX / window.innerWidth ) * 2 - 1); vector.y = (- ( event.clientY / window.innerHeight ) * 2 + 1); vector.z = 1.0; console.log(camera.position.z); vector.unproject( camera ); var dir = vector.sub( camera.position ).normalize(); var distance = - camera.position.z / dir.z; var temp = camera.position.clone().add( dir.multiplyScalar( distance ) ); var pos = points.geometry.attributes.position; pos.setXY(selection, temp.x, temp.y); pos.updateRange.offset = selection; // where to start updating pos.updateRange.count = 1; // how many vertices to update pos.needsUpdate = true; selection = undefined; }
Highcharts dataLabels reposition on overlap
I have a Highcharts component where the user can add comments to a graph, and the comment shows up as a dataLabel in a scatter series. However, I noticed that by default the allowOverlap just removes the dataLabels that collides, and my question to this is: would it be possible to make the colliding dataLabels stack on top of each other? I'm thinking that since the allowOverlap: true can detect which ones that are colliding, there might be a way to take advantage of this? This is how my dataLabels look now: This is my goal: Hope that someone can help me with a clever solution, I sure know I am out of ideas! By the way, right now the dataLabels gets their xAxis position by dividing the xAxis :{ max: value } by 1,5. This is just to position it equally on all my graphs, which all have different min and max values. Might be worth mentioning.
It is possible to override the funciton that should be hiding the overlapping labels and adjust position of the overlapping labels like this: $(function() { (function(H) { var each = H.each, UNDEFINED; H.Chart.prototype.hideOverlappingLabels = function(labels, rerun) { if (rerun === UNDEFINED || rerun < 10) //infinity loop limit { var doTheRerun = false, len = labels.length, label, i, j, label1, label2, isIntersecting, pos1, pos2, parent1, parent2, padding, intersectRect = function(x1, y1, w1, h1, x2, y2, w2, h2) { return !( x2 > x1 + w1 || x2 + w2 < x1 || y2 > y1 + h1 || y2 + h2 < y1 ); }; for (i = 0; i < len; i++) { label = labels[i]; if (label) { label.oldOpacity = label.opacity; label.newOpacity = 1; } } labels.sort(function(a, b) { return (b.labelrank || 0) - (a.labelrank || 0); }); for (i = 0; i < len; i++) { label1 = labels[i]; for (j = i + 1; j < len; ++j) { label2 = labels[j]; if (label1 && label2 && label1.placed && label2.placed && label1.newOpacity !== 0 && label2.newOpacity !== 0) { pos1 = label1.alignAttr; pos2 = label2.alignAttr; parent1 = label1.parentGroup; // Different panes have different positions parent2 = label2.parentGroup; padding = 2 * (label1.box ? 0 : label1.padding); // Substract the padding if no background or border (#4333) isIntersecting = intersectRect( pos1.x + parent1.translateX, pos1.y + parent1.translateY, label1.width - padding, label1.height - padding, pos2.x + parent2.translateX, pos2.y + parent2.translateY, label2.width - padding, label2.height - padding ); if (isIntersecting) { (label1.labelrank < label2.labelrank ? label1 : label2).addHeightOffset = true; } } } } each(labels, function(label) { var complete, newOpacity; if (label) { if (label.addHeightOffset && label.placed) { label.alignAttr.y -= label.height; label.addHeightOffset = false; doTheRerun = true; } label['attr'](label.alignAttr); } }); if (doTheRerun) { rerun = (rerun || 0) + 1; H.Chart.prototype.hideOverlappingLabels(labels, rerun); } } }; }(Highcharts)) $('#container').highcharts({ series: [{ dataLabels: { enabled: true, format: 'The value that is important for this point is: {y}' }, data: [1, 2, 1, 1, 2, 2, 2] }] }); }); <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <script src="http://code.highcharts.com/highcharts.js"></script> <div id="container" style="height: 400px"></div> Demo in JSFiddle: http://jsfiddle.net/ozLtvwke/1/
ZedGraph labels
In ZedGraph, how do I show text labels for each point and in the XAxis all together? If I do myPane.XAxis.Type = AxisType.Text; myPane.XAxis.Scale.TextLabels = array_of_string; I get labels on the XAxis like this And if I do for (int i = 0; i < myCurve.Points.Count; i++) { PointPair pt = myCurve.Points[i]; // Create a text label from the Y data value TextObj text = new TextObj( pt.Y.ToString("f0"), pt.X, pt.Y + 0.1, CoordType.AxisXYScale, AlignH.Left, AlignV.Center); text.ZOrder = ZOrder.A_InFront; text.FontSpec.Angle = 0; myPane.GraphObjList.Add(text); } I get labels on the curve, like this But if I do both at the same time, labels on the curve disappear. Is there a way to combine both kind of labels?
I've changed my answer after you clarified the question. You just have to remember to position the labels correctly: <% System.Collections.Generic.List<ZedGraphWebPointPair> points = new System.Collections.Generic.List<ZedGraphWebPointPair>(); for (int i = 0; i < 15; i++) { // Let's have some fun with maths points.Add(new ZedGraphWebPointPair { X = i, Y = Math.Pow(i - 10, 2) * -1 + 120 }); } System.Collections.Generic.List<string> XAxisLabels = new System.Collections.Generic.List<string>(); TestGraph.CurveList.Add(new ZedGraphWebLineItem { Color = System.Drawing.Color.Red }); TestGraph.XAxis.Scale.FontSpec.Size = 9; int j = 1; foreach (ZedGraphWebPointPair point in points) { // Add the points we calculated TestGraph.CurveList[0].Points.Add(point); // Add the labels for the points TestGraph.GraphObjList.Add(new ZedGraphWebTextObj { Location = { CoordinateFrame = ZedGraph.CoordType.XChartFractionYScale, // Make sure we position them according to the CoordinateFrame X = Convert.ToSingle(j) / points.Count - 0.05f, Y = Convert.ToSingle(point.Y) + 3f, AlignV = ZedGraph.AlignV.Top }, Text = point.Y.ToString(), FontSpec = { Angle = 90, Size = 9, Border = { IsVisible = false } } }); // Add the labels for the XAxis XAxisLabels.Add(String.Format("P{0}", j)); j++; } TestGraph.RenderGraph += delegate(ZedGraphWeb zgw, System.Drawing.Graphics g, ZedGraph.MasterPane mp) { ZedGraph.GraphPane gp = mp[0]; gp.XAxis.Type = ZedGraph.AxisType.Text; gp.XAxis.Scale.TextLabels = XAxisLabels.ToArray(); }; %> That code will produce this graph:
If the axis type is text, the code below is easier to get x-coordinates of the points ;) for (int tPoint = 0; tPoint < curve.Points.Count; tPoint++) { TextObj text = new TextObj(curve.Points[tPoint].Y.ToString(), curve.Points[tPoint].X, curve.Points[tPoint].Y + 10); }