I'm using the jQuery Flot Charts plugin in one of my projects. I have several charts standing in the same column and what I'm trying to do is: Show the crosshair on all of the charts if you hover on any of them. I'm doing this successfully using the following code.
//graphs - this is an object which contains the references to all of my graphs.
$.each(graphs, function(key, value) {
$(key).bind("plothover", function (event, pos, item) {
$.each(graphs, function(innerKey, innerValue) {
if(key != innerKey) {
innerValue.setCrosshair({x: pos.x});
}
});
if(item) {
var x = item.datapoint[0].toFixed(2),
y = item.datapoint[1].toFixed(2);
console.log("x:" + x + ", " + "y:" + y);
}
});
});
I'm iterating over the graphs, adding the crosshair and bind it to each other. So, now, when you hover over one of the graphs you'll see the crosshair in the same position on all of the others.
No problems with this. However I'm having problems with the second part of my code:
if(item) {
var x = item.datapoint[0].toFixed(2),
y = item.datapoint[1].toFixed(2);
console.log("x:" + x + ", " + "y:" + y);
}
And the problem is that I'm getting the console.log to print values only when I hover the actual point with my mouse while I want to get that value whenever the crosshair crosses that point, not necessarily the mouse pointer. Any clues what I'm doing wrong or maybe there's a setting in the graph options for that to work?
And another thing is that I can get the value for one graph only - the one my mouse is on, I don't seem to be able to get the values for the rest of the graphs where the crosshair is also moving.
The highlighting with
if(item) {
var x = item.datapoint[0].toFixed(2),
y = item.datapoint[1].toFixed(2);
console.log("x:" + x + ", " + "y:" + y);
}
only works when the cursor is near a point (otherwise item is null).
To get the nearest point to the crosshair, you have to do the highlighting manually by searching the nearest point and interpolate (for every graph). The code for that could look like this:
var axes = value.getAxes();
if (pos.x < axes.xaxis.min || pos.x > axes.xaxis.max ||
pos.y < axes.yaxis.min || pos.y > axes.yaxis.max) {
return;
}
$('#output').html("x: " + pos.x.toPrecision(2));
$.each(graphs, function(innerKey, innerValue) {
var i, series = innerValue.getData()[0];
// Find the nearest points, x-wise
for (i = 0; i < series.data.length; ++i) {
if (series.data[i][0] > pos.x) {
break;
}
}
// Now Interpolate
var y,
p1 = series.data[i - 1],
p2 = series.data[i];
if (p1 == null) {
y = p2[1];
} else if (p2 == null) {
y = p1[1];
} else {
y = p1[1] + (p2[1] - p1[1]) * (pos.x - p1[0]) / (p2[0] - p1[0]);
}
$('#output').html($('#output').html() + "<br />" + "y (" + innerValue.getData()[0].label + "): " + y.toPrecision(2));
See this fiddle for a full working example. Some Remarks to the new code and fiddle:
has sine and cosine values as example data, so uses floating point numbers, change accordingly for int numbers and/or date values
uses a <p> element for output instead of the console
point-finding and interpolation code can be optimized further if needed (basic version here taken from the example on the Flot page)
works only if there is only one data series per graph
Related
The prompt of the assignment is "Write a program that shows the X and Y coordinates of the mouse in a label on the top left of the screen. You should update the values of the coordinates whenever the mouse moves." The basic code I have laid out is:
var pos;
function start(){
mouseMoveMethod(mousePos);
}
function mousePos(e){
pos = new Text("((" + e.getX() + "," + e.getY() + ")");
pos.setPosition(75, 75)
add(pos);
}
It "works" but it just keeps adding text over top of the new text instead of updating the existing text. I know I'm missing something, but I just cannot figure out what to implement to make it work the way it needs to. I've been stuck for days :(
I got it!!! I just needed to think smarter LOL
var pos;
function start(){
pos = new Text(" ");
pos.setPosition(75, 75)
add(pos);
mouseMoveMethod(mousePos);
}
function mousePos(e){
pos.setText("(" + e.getX() + "," + e.getY() + ")");
}
Hi,
I'm developing a visual like a scatter plot using D3.js it has around 20k points without labels. I want to show the labels for the filtered data. I modified a function to avoid labels overlapping. It works but if I have a large number of points after applying the filter it cause crush the browser !!
Any ideas to improve the algorithm ? or to use the force function in D3v3 to do the job ?
function arrangeLabels(svg) {
var move = 1;
while(move > 0) {
move = 0;
svg.selectAll(".dotB")
.each(function() {
var that = this,
a = this.getBoundingClientRect();
svg.selectAll("text.dotB")
.each(function() {
if (this != that) {
var b = this.getBoundingClientRect();
if ((Math.abs(a.left - b.left) * 2 < (a.width + b.width)) &&
(Math.abs(a.top - b.top) * 2 < (a.height + b.height))) {
var dy = (a.bottom + b.height)+2,
move += Math.abs(dy);
d3.select(this).attr("y", dy);
a = this.getBoundingClientRect();
}
}
});
})
}
I found this method to solve the overlapping issue using D3v4 https://walkingtree.tech/d3-quadrant-chart-collision-in-angular2-application/ any idea how to do the same in D3v3 ?!
So the problem is with the if logic:
if (
(Math.abs(a.left - b.left) * 2 < (a.width + b.width)) &&
(Math.abs(a.top - b.top) < a.height + b.height)
) {
var dy = (a.bottom + b.height)+2,
move += Math.abs(dy);
d3.select(this).attr("y", dy);
a = this.getBoundingClientRect();
}
I am trying to create a special kind of donut chart in D3 which will contain different rings for positive and negative values. The values can be greater than 100% or less than -100% so there will be an arc representing the remaining value. Below is the sample image of the chart:
The first positive category (Category_1 - Gray) value is 80, so it is 80% filling the the circle with gray, leaving the 20% for next positive category. The next positive category value (Category_2 - Orange) is 160. So it is first using the 20% left by Category_1 (140 value left now). Then it is filling the next circle (upward) 100% (40 value left now), and for the remaining value (40), it is creating partial-circle upward.
Now, we have Category_3 (dark-red) as negative (-120%), so it if creating an inward circle and filling it 100% (20 value left now), and then it is creating an inward arc for remaining value (20). We have another negative category (Category_4 - red), so it will start from where the previous negative category (Category_3) ended and fill 20% area from there.
Edit 3: I've created a very basic arc-based donut chart and when total value is exceeding 100, I am able to create outer rings for the remaining values. Below is the JSFiddle link:
http://jsfiddle.net/rishabh1990/zmuqze80/
data = [20, 240];
var startAngle = 0;
var previousData = 0;
var exceedingData;
var cumulativeData = 0;
var remainder = 100;
var innerRadius = 60;
var outerRadius = 40;
var filledFlag;
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
for (var i = 0; i < data.length; i++) {
filledFlag = 0;
exceedingData = 0;
console.log("---------- Iteration: " + (i + 1) + "---------");
if (data[i] > remainder) {
filledFlag = 1;
exceedingData = data[i] - remainder;
console.log("Exceeding: " + exceedingData);
data[i] = data[i] - exceedingData;
data.splice(i + 1, 0, exceedingData);
}
if( filledFlag === 1) {
cumulativeData = 0;
} else {
cumulativeData += data[i];
}
console.log("Previous: " + previousData);
console.log("Data: " + data, "Current Data: " + data[i]);
var endAngle = (previousData + (data[i] / 50)) * Math.PI;
console.log("Start " + startAngle, "End " + endAngle);
previousData = previousData + data[i] / 50;
//if(i===1) endAngle = 1.4 * Math.PI;
//if(i===2) endAngle = 2 * Math.PI;
var vis = d3.select("#svg_donut");
arc.startAngle(startAngle).endAngle(endAngle);
vis.append("path")
.attr("d", arc)
.attr("transform", "translate(200,200)")
.style("fill", function(d) {
if (i === 0) return "red";
//if (i === 1) return "green";
//if (i === 2) return "blue"
//if (i === 3) return "orange"
//if (i === 4) return "yellow";
});
if (exceedingData > 0) {
console.log("Increasing Radius From " + outerRadius + " To " + (outerRadius + 40));
outerRadius = outerRadius + 22;
innerRadius = innerRadius + 22;
arc.innerRadius(innerRadius).outerRadius(outerRadius);
console.log("Outer: ", outerRadius);
}
if (remainder === 100) {
remainder = 100 - data[i];
} else {
remainder = 100 - cumulativeData;
};
if (filledFlag === 1) {
remainder = 100;
}
console.log("Remainder: " + remainder);
startAngle = endAngle;
}
Please share some ideas for implementation.
Ok, this took some time, but it seems to be working. First, let's identify that what you describe as a donut chart can also be rendered as a series of bars — using the exact same data. So I started from there and eventually worked it into a donut chart, but left the bar implementation in there as well. The other thing is that a generic solution should be able to wrap the segments at any value, not just 100, so I included a slider that lets you vary that wrapping value. Finally — and this is easier to explain in a bars rather than donut implementation — rather than always having the bars wrap left-to-right, like text, it may be desirable to zigzag, i.e. alternate wrapping left-to-right then right-to-left and so on. The effect this has is that when an amount is broken up into two segments on two separate lines, the zigzag approach will keep those two segments next to each other. I added a checkbox to turn on/off this zigzag behavior.
Here's a working jsFiddle and another iteration of it.
Here are the important bits:
There's a function wrap(data, wrapLength) which takes an array of data values and a wrapLength at which to wrap these values. That function figures out which data values have to be split up into sub-segments and returns a new array of them, with each segment's object having x1, x2 and y values. x1 and x2 are the start and end of each bar, and y is the row of the bar. In a donut chart those values are equivalently start angle (x1), end angle (x2) and radius (y) of each arc.
The function wrap() doesn't know how to account for negative vs positive values, so wrap() has to be called twice — once with all the negatives and then all the positives. From there, some processing is applied selectively to just the negatives and then more processing is applied to the combination of the two sets. The entire set of transformations described in the last 2 paragraphs is captured by following snippet. I'm not including the implementation of wrap() here, just the code that calls it; also not including the rendering code, which is pretty straightforward once segments is generated.
// Turn N data points into N + x segments, as dictated by wrapLength. Do this separately
// for positive and negative values. They'll be merged further down, after we apply
// a specific transformation to just the negatives
var positiveSegments = wrap(data.filter(function(d) { return d.value > 0; }), wrapLength);
var negativeSegments = wrap(data.filter(function(d) { return d.value < 0; }), wrapLength);
// Flip and offset-by-one the y-value of every negative segment. I.e. 0 becomes -1, 1 becomes -2
negativeSegments.forEach(function(segment) { segment.y = -(segment.y + 1); });
// Flip the order of the negative segments, so that their sorted from negative-most y-value and up
negativeSegments.reverse()
// Combine negative and positive segments
segments = negativeSegments.concat(positiveSegments);
if(zigzag) {
segments.forEach(function(segment) {
if(Math.abs(segment.y) % 2 == (segment.y < 0 ? 0 : 1)) { flipSegment(segment, wrapLength); }
});
}
// Offset the y of every segment (negative or positive) so that the minimum y is 0
// and goes up from there
var maxNegativeY = negativeSegments[0].y * -1;
segments.forEach(function(segment) { segment.y += maxNegativeY; });
I have a radar that I can zoom into, this all works if I am scrolling in with the mouse or pinch zooming. However, when i double click on an arc within the radar it needs to zoom in to it.
i have removed the default double click with
.on('dblclick.zoom',null);
and I have a double click event on each arc that can be double clicked (so it is not going through the zoom listener). This double click handler calls the zoom listeners scale and translate functions and then the event. I have tried calling the scale then event and then the translate and then the event but I get the same result, nothing zooms
RadarDraw.ZoomListener.scale(6);
RadarDraw.ZoomListener.translate([0, 0]);
RadarDraw.ZoomListener.event(svg.transition().duration(500));
from what I have read this should work. Is there something I am missing here?
Thanks
Mark
my zoom handler function looks like this
function zoom(){
if (d3.event.scale <= 1 || d3.event.scale > 5) {
//on scale 1 (zoomed out) center the radar to its origianl position
//if (RadarDraw.ZoomListener.scale() <= 1 && previousZoom)
//{
// resetZoomOut();
// previousZoom = false;
//}
var scalesize;
if (d3.event.scale <= 1)
{
scalesize = 1;
}
if (d3.event.scale > 5)
{
scalesize = 0.2;
}
$(".isadot").each(function (i, v) {
var tmp = v;
var PrevTransform = tmp.attributes["transform"].nodeValue;
//copy translate only remove other scale
var currentTrans = PrevTransform.split(" ");
tmp.attributes["transform"].nodeValue = currentTrans[0] + " scale(" + scalesize + ")";
});
previousZoomLevel = d3.event.scale;
return;
} else {
// RadarDraw.ZoomListener.translate(d3.event.translate);
//allow panning not zoom between min and max
svg.attr("transform", "translate("
+ (d3.event.translate[0] + (_config.Width / 2)) + "," + (d3.event.translate[1] + (_config.Height / 2))
+ ")scale(" + d3.event.scale + ")");
if (d3.event.scale > 1)
previousZoom = true;
//if we are zoomed in and out previous zoom level is 5 or above then exit zoom (we are either scrolling out or pinching out of zoom)
if (tools.IsArcFocused && previousZoomLevel >= 5) {
tools.ExitZoom(false);
}
//scale the dots by the zom level
var scalesize = 0.6;
//switch (RadarDraw.ZoomListener.scale())
//{
// case ():
// break;
// default:
// scalesize = 0.6;
// break;
//}
if (d3.event.scale <= 1) {
scalesize = 1;
}
if (d3.event.scale >= 5) {
scalesize = 0.4;
}
$(".isadot").each(function (i, v) {
var tmp = v;
var PrevTransform = tmp.attributes["transform"].nodeValue;
//copy translate only remove other scale
var currentTrans = PrevTransform.split(" ");
tmp.attributes["transform"].nodeValue = currentTrans[0] + " scale(" + scalesize + ")";
});
}
}
EDIT
Rob was correct it ended up being .on('dblclick.zoom',null); that was causing the issue in ie 9, after commenting it out it acted as expected. Thanks for the back and forth Rob, got there in the end haha.
I have a requirement to Plot a graph with zoom feature using x axis selection, i am using Flot for plotting graph. I have to show the number of points present after zooming.
I am alwayz getting the total length not the Zoomed length.
Kindly Help
In your "plotzoom" callback, you can get min & max x / y co-ordinates back like so (from the examples on the site):
placeholder.bind('plotzoom', function (event, plot) {
var axes = plot.getAxes();
$(".message").html("Zooming to x: " + axes.xaxis.min.toFixed(2)
+ " – " + axes.xaxis.max.toFixed(2)
+ " and y: " + axes.yaxis.min.toFixed(2)
+ " – " + axes.yaxis.max.toFixed(2));
});
You can the use these to count how many points are within this rectangle.
May i suggest using underscore.js filter function to simplify this?
Eg.
var xmin = axes.xaxis.min.toFixed(2);
var xmax = axes.xaxis.max.toFixed(2);
var ymin = axes.yaxis.min.toFixed(2);
var ymax = axes.yaxis.max.toFixed(2);
var points = [{x: 1, y:20} ..some points.. {x: 30, y: -23}];
var shownpoints = _.filter(points, function(point){
return point.x > xmin &&
point.x < xmax &&
point.y > ymin &&
point.y < ymax;
}
);
This will give an array with points in-between x & y maxims. You can then use length to get the count!