I want to draw multiple sin with QCustomPlot in Qt. I want the sins to be bellow each other. actually, I want to show something like ECG. Can anyone help me?
Your requirements are quite short-spoken, so I will give a simple solution.
All you need is to add multiple sinus graphs to a customPlot object, and add an offset to each sinus.
customPlot->addGraph();
customPlot->graph(0)->setPen(QPen(Qt::blue)); // line color blue for first graph
customPlot->addGraph();
customPlot->graph(1)->setPen(QPen(Qt::red)); // line color red for second graph
customPlot->addGraph();
customPlot->graph(2)->setPen(QPen(Qt::green)); // line color green for third graph
customPlot->addGraph();
customPlot->graph(3)->setPen(QPen(Qt::yellow)); // line color yellow for fourth graph
// generate some points of data
QVector<double> x(250), y0(250), y1(250), y2(250), y3(250);
for (int i=0; i<250; ++i)
{
x[i] = i;
y0[i] = qCos(i/10.0);
y1[i] = qCos(i/10.0) + 3; //add offset
y2[i] = qCos(i/10.0) + 6; //add offset
y3[i] = qCos(i/10.0) + 9; //add offset
}
// configure right and top axis to show ticks but no labels:
// (see QCPAxisRect::setupFullAxesBox for a quicker method to do this)
customPlot->yAxis->setTickLabels(false);
customPlot->xAxis2->setVisible(true);
customPlot->xAxis2->setTickLabels(false);
customPlot->yAxis2->setVisible(true);
customPlot->yAxis2->setTickLabels(false);
// make left and bottom axes always transfer their ranges to right and top axes:
connect(customPlot->xAxis, SIGNAL(rangeChanged(QCPRange)), customPlot->xAxis2, SLOT(setRange(QCPRange)));
connect(customPlot->yAxis, SIGNAL(rangeChanged(QCPRange)), customPlot->yAxis2, SLOT(setRange(QCPRange)));
// pass data points to graphs:
customPlot->graph(0)->setData(x, y0);
customPlot->graph(1)->setData(x, y1);
customPlot->graph(2)->setData(x, y2);
customPlot->graph(3)->setData(x, y3);
// let the ranges scale themselves so graph 0 fits perfectly in the visible area:
customPlot->graph(0)->rescaleAxes();
// same thing for graph 1, but only enlarge ranges (in case graph 1 is smaller than graph 0):
customPlot->graph(1)->rescaleAxes(true);
customPlot->graph(2)->rescaleAxes(true);
customPlot->graph(3)->rescaleAxes(true);
// Note: we could have also just called customPlot->rescaleAxes(); instead
// Allow user to drag axis ranges with mouse, zoom with mouse wheel and select graphs by clicking:
customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
The result will be something like this:
Related
I am using QCustomPlot in a application which is focused on the graph which displays results from a external device. I have a cursor which uses the QMouseEvent. Whenever I get the mouse event it draws a horizontal line and vertical line from the mouse position to the axis.
void PlotClass::ChartMouseMove(QMouseEvent* mouse){
double x = ui->customplot->xAxis->pixelToCoord(mouse->pos().x());
double y = ui->customplot->yAxis->pixelToCoord(mouse->pos().y());
//QCPItemStraightLine *infLine = new QCPItemStraightLine(ui->customplot);
// infLine->point1->setCoords(x, 0); // location of point 1 in plot coordinate
// infLine->point2->setCoords(2, 1); // location of point 2 in plot coordinate
qDebug() << x << y;
// ui->customplot->xAxis->range().minRange();
double xLow = ui->customplot->xAxis->range().lower;
double xHigh = ui->customplot->xAxis->range().upper;
double yLow = ui->customplot->yAxis->range().lower;
double yHigh = ui->customplot->yAxis->range().upper;
infLinex->start->setCoords(x, yLow); // location of point 1 in plot coordinate
infLinex->end->setCoords(x, yHigh); // location of point 2 in plot coordinate
infLiney->start->setCoords(xLow, y); // location of point 1 in plot coordinate
infLiney->end->setCoords(xHigh, y); // location of point 2 in plot coordinate
ui->customplot->replot();
}
What I need to do is remove the cursor when the mouse is no longer over the chart. Not sure how to do this.
Also would be nice to paint the actual cursor position onto the lines in text (the values from the axis.)
Ok figured it out. I just put this function call in the timer event (possibly not the best way but it works)
void PlotClass::CheckHidecursor(void){
if(!Hidecursor && !ui->customplot->underMouse()){
Hidecursor = true;
infLinex->setVisible(false);
infLiney->setVisible(false);
yLabel->setVisible(false);
xLabel->setVisible(false);
qDebug() << "Hide";
ui->customplot->replot();
}
}
So it hides the lines and the numbers I am putting on if the mouse is no longer over the chart widget. The key function I found was QWidget::underMouse() which gives a true/false reponse.
I am drawing lines in Qt using Graphics View framework. Since i want my picture to take the same portion of space when the window is resized, I override MainWindow::resizeEvent, so that graphics view is rescaled according to the resize event:
void MainWindow::resizeEvent(QResizeEvent *event) {
int w = event->size().width(), h = event->size().height();
int prev_w = event->oldSize().width(), prev_h = event->oldSize().height();
if (prev_w != -1) {
int s1 = std::min(prev_w, prev_h), s2 = std::min(w, h);
qreal k = (qreal)s2 / s1;
std::cerr << k << std::endl;
ui->graphicsView->scale(k, k);
}
}
However, doing so, my lines (that should have thickness of 1 pixel) sometimes have different thickness after resize. As I understand, it happens because coordinates of the objects after transforming to the GraphicsView are real, so are sometimes drawn with different number of pixels. That is unacceptable! I want lines to have same 1-pixel thickness all the time.
So, my question is: what is the usual solution for this problem? For now (based on my assumption above) I can only think of deleting all objects and creating new with integer coordinates, but rescaled (manually).
You need to set your line drawing to "cosmetic" in the QPen. This makes the lines non-scalable. Otherwise, Qt scales the line widths along with the scaling of the view. Look up QPen::setCosmetic. By default, drawing lines is not cosmetic.
I have a simple Processing Sketch, drawing a continuous line of ellipses with a 20px diameter. Is there a way to modify the sketch so that it draws vector shapes instead of pixels?
void setup() {
size(900, 900);
background(110, 255, 94);
}
void draw() {
ellipse(mouseX, mouseY, 20, 20);
fill(255);
}
Thanks to everyone who can provide some helpful advice.
Expanding my comment above, there a couple of things to tackle:
drawing a continuous line of ellipses with a 20px diameter
draws vector shapes
Currently you're drawing ellipses based on mouse movement.
A side effect is that if you move the mouse fast enough you will have gaps in between ellipses.
To fill the gaps you can work out the distance between every two ellipses.
If the distance is greater than the sizes of these two ellipses you can draw some in between.
The PVector class provides a lerp() function that allows you easily interpolate between two points.
You can read more on this and run some examples here
Using the ratio between these distance of two points and the ellipse size the number of points needed in between.
Here is an example that stores mouse locations to a list of PVectors as you drag the mouse:
//create an array list to store points to draw
ArrayList<PVector> path = new ArrayList<PVector>();
//size of each ellipse
float size = 20;
//how tight will the extra ellipses be drawn together
float tightness = 1.25;
void setup() {
size(900, 900);
}
void draw() {
background(110, 255, 94);
fill(255);
//for each point in the path, starting at 1 (not 0)
for(int i = 1; i < path.size(); i++){
//get a reference to the current and previous point
PVector current = path.get(i);
PVector previous = path.get(i-1);
//calculate the distance between them
float distance = previous.dist(current);
//work out how many points will need to be added in between the current and previous points to keep the path continuous (taking the ellipse size into account)
int extraPoints = (int)(round(distance/size * tightness));
//draw the previous point
ellipse(previous.x,previous.y,size,size);
//if there are any exta points to be added, compute and draw them:
for(int j = 0; j < extraPoints; j++){
//work out a normalized (between 0.0 and 1.0) value of where each extra point should be
//think of this as a percentage along a line: 0.0 = start of line, 0.5 = 50% along the line, 1.0 = end of the line
float interpolation = map(j,0,extraPoints,0.0,1.0);
//compute the point in between using PVector's linear interpolation (lerp()) functionality
PVector inbetween = PVector.lerp(previous,current,interpolation);
//draw the point in between
ellipse(inbetween.x,inbetween.y,size,size);
}
}
//draw instructions
fill(0);
text("SPACE = clear\nLEFT = decrease tightness\nRIGHT = increase tightness\ntightness:"+tightness,10,15);
}
void mouseDragged(){
path.add(new PVector(mouseX,mouseY));
}
void keyPressed(){
if(keyCode == LEFT) tightness = constrain(tightness-0.1,0.0,3.0);
if(keyCode == RIGHT) tightness = constrain(tightness+0.1,0.0,3.0);
if(key == ' ') path.clear();
}
Note that the interpolation between points is linear.
It's the simplest, but as the name implies, it's all about lines:
it always connects two points in a straight line, not curves.
I've added the option to control how tight interpolated ellipses will be packed together. Here are a couple of screenshots with different tightness levels. You'll notice as tightness increases, the lines will become more evident:
You run the code bellow:
//create an array list to store points to draw
var path = [];
//size of each ellipse
var ellipseSize = 20;
//how tight will the extra ellipses be drawn together
var tightness = 1.25;
function setup() {
createCanvas(900, 900);
}
function draw() {
background(110, 255, 94);
fill(255);
//for each point in the path, starting at 1 (not 0)
for(var i = 1; i < path.length; i++){
//get a reference to the current and previous point
var current = path[i];
var previous = path[i-1];
//calculate the distance between them
var distance = previous.dist(current);
//work out how many points will need to be added in between the current and previous points to keep the path continuous (taking the ellipse size into account)
var extraPoints = round(distance/ellipseSize * tightness);
//draw the previous point
ellipse(previous.x,previous.y,ellipseSize,ellipseSize);
//if there are any exta points to be added, compute and draw them:
for(var j = 0; j < extraPoints; j++){
//work out a normalized (between 0.0 and 1.0) value of where each extra point should be
//think of this as a percentage along a line: 0.0 = start of line, 0.5 = 50% along the line, 1.0 = end of the line
var interpolation = map(j,0,extraPoints,0.0,1.0);
//compute the point in between using PVector's linear interpolation (lerp()) functionality
var inbetween = p5.Vector.lerp(previous,current,interpolation);
//draw the point in between
ellipse(inbetween.x,inbetween.y,ellipseSize,ellipseSize);
}
}
//draw instructions
fill(0);
text("BACKSPACE = clear\n- = decrease tightness\n+ = increase tightness\ntightness:"+tightness,10,15);
}
function mouseDragged(){
path.push(createVector(mouseX,mouseY));
}
function keyPressed(){
if(keyCode == 189) tightness = constrain(tightness-0.1,0.0,3.0);
if(keyCode == 187) tightness = constrain(tightness+0.1,0.0,3.0);
if(keyCode == BACKSPACE) path = [];
}
//https://stackoverflow.com/questions/40673192/processing-draw-vector-instead-of-pixels
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.5.4/p5.min.js"></script>
If you want smoother lines you will need to use a different interpolation such as quadratic or cubic interpolation. You can start with existing Processing functions for drawing curves such as curve() or bezier(),and you'll find some helpful resources unrelated to Processing here,here and here.
On vector shapes
You're not directly working with pixels[], you're drawing shapes.
These shapes can easily be saved to PDF using Processing's PDF library
Check out the Single Frame from an Animation (With Screen Display) example.
Here is a version that saves to PDF when pressing the 's' key:
import processing.pdf.*;
//create an array list to store points to draw
ArrayList<PVector> path = new ArrayList<PVector>();
//size of each ellipse
float size = 20;
//how tight will the extra ellipses be drawn together
float tightness = 1.25;
//PDF saving
boolean record;
void setup() {
size(900, 900);
}
void draw() {
background(110, 255, 94);
fill(255);
//if we need to save the current frame to pdf, begin recording drawing instructions
if (record) {
// Note that #### will be replaced with the frame number. Fancy!
beginRecord(PDF, "frame-####.pdf");
}
//for each point in the path, starting at 1 (not 0)
for(int i = 1; i < path.size(); i++){
//get a reference to the current and previous point
PVector current = path.get(i);
PVector previous = path.get(i-1);
//calculate the distance between them
float distance = previous.dist(current);
//work out how many points will need to be added in between the current and previous points to keep the path continuous (taking the ellipse size into account)
int extraPoints = (int)(round(distance/size * tightness));
//draw the previous point
ellipse(previous.x,previous.y,size,size);
//if there are any exta points to be added, compute and draw them:
for(int j = 0; j < extraPoints; j++){
//work out a normalized (between 0.0 and 1.0) value of where each extra point should be
//think of this as a percentage along a line: 0.0 = start of line, 0.5 = 50% along the line, 1.0 = end of the line
float interpolation = map(j,0,extraPoints,0.0,1.0);
//compute the point in between using PVector's linear interpolation (lerp()) functionality
PVector inbetween = PVector.lerp(previous,current,interpolation);
//draw the point in between
ellipse(inbetween.x,inbetween.y,size,size);
}
}
//once what we want to save has been recorded to PDF, stop recording (this will skip saving the instructions text);
if (record) {
endRecord();
record = false;
println("pdf saved");
}
//draw instructions
fill(0);
text("SPACE = clear\nLEFT = decrease tightness\nRIGHT = increase tightness\ntightness:"+tightness+"\n's' = save PDF",10,15);
}
void mouseDragged(){
path.add(new PVector(mouseX,mouseY));
}
void keyPressed(){
if(keyCode == LEFT) tightness = constrain(tightness-0.1,0.0,3.0);
if(keyCode == RIGHT) tightness = constrain(tightness+0.1,0.0,3.0);
if(key == ' ') path.clear();
if(key == 's') record = true;
}
In addition to George's great answer (which I've +1'd), I wanted to offer a more basic option:
The problem, like George said, is that when you move the mouse, you actually skip over a bunch of pixels. So if you only draw ellipses or points at mouseX, mouseY then you'll end up with gaps.
The dumb fix: the pmouseX and pmouseY variables hold the previous position of the cursor.
That might not sound very useful, but they allow you to solve exactly your problem. Instead of drawing ellipses or points at the current mouse position, draw a line from the previous position to the current position. This will eliminate any gaps in your lines.
void draw(){
line(pmouseX, pmouseY, mouseX, mouseY);
}
Shameless self-promotion: I've written a tutorial on getting user input in Processing available here.
Note: This dumb solution will only work if you aren't redrawing the background every frame. If you need to redraw everything every frame, then George's answer is the way to go.
Using processing and recursion, I'm trying to draw a similar shape to this:
But I feel like I'm losing my mind trying every possible way to draw the shape. This is closest I've gotten:
Plus my code, any help would be appreciated. Thanks:
void setup(){
size(600,600);
}
void draw(){
background(255);
draws(300, 300, 50, 5);
}
void draws(int x, int y, int x2, int num){
stroke(0);
strokeWeight(2);
line (x, y, (x+x2), y); //right
line(x, y, x, y-50); //right up
line (x-x2, y, x-(x2*2), y); // left
line(x-x2, y, x-x2, y-50); //left up
line (x, y-50, x-x2, y-50); //top
if(num>0){
draws(x-x2, y-x2, x2/2, num-1);
}
}
(Maybe this should be a comment - since it's not a complete answer - but i can't seem to make it)
That said.......
you are dividing by 2 rather than 3 at each step.
you need to draw 3 second hierarchy 'draws' each time
You have the 'y' coordinate right here - but the x coordinate is wrong.
at a guess.... the hierarchy should be something like....
`
if(num>0){
draws(x, y-x2, x2/3, num-1); // Central one
draws(x-x2, y, x2/3, num-1); // Left One
draws(x, y, x2/3/2, num-1); // right one
}
`
Thing that makes it slightly difficult is that your coordinates start from the left hand side of the right line.... probably easier to start from either the far left or far right.
The logic is similar to generating a Koch fractal. It's goes something like this:
Draw a straight horizontal line.
Divide the line into 3 segments.
Move middle segment up by the same amount of the size of one segment.
Repeat for each segment.
So, our function should basically try to draw this line:
_________
| |
| |
| |
_________| |_________
Where the process is repeated in turn for each horizontal line.
One simple way I can think of to do this is to simply start with a straight line. Then in each iteration erase the middle of the line and do the up-shift (you can draw a white line on top of the black line).
So, the pseudocode would be something like this:
// Pseudocode:
fractal (x_start,x_end, y) {
// first simply draw a straight line:
line(x_start,y,x_end,y);
// divide the line into 3 and push the middle up
length = x_end-x_start;
segment_length = length/3;
x2 = x_start+segment_length;
x3 = x_start+segment_length*2;
y2 = y-segment_length;
erase_line(x2,y,x3,y);
line(x2,y,x2,y2); // up
line(x2,y2,x3,y2); // accross
line(x3,y2,x3,y); // down
// now repeat for each segment
fractal(x_start,y,x2,y);
fractal(x2,y2,x3,y2);
fractal(x3,y,x_end,y);
}
So that's the basic working function. Notice that it doesn't stop recursing so the above function will go on infinitely (or until you run out of memory). So the first thing to do is to add a recursion limit:
// Pseudocode:
fractal (x_start,x_end, y, limit) {
//
// same content as above except the last 3 lines
//
limit --;
if (limit) {
fractal(x_start,y,x2,y,limit);
fractal(x2,y2,x3,y2,limit);
fractal(x3,y,x_end,y,limit);
}
}
That should be a good starting point.
There are other optimizations you can make. For example, you don't actually need to draw the straight line in the beginning since each iteration will basically draw over it again. You only need to draw the horizontal lines at the limit of the recursion. Which means that you don't need to erase the lines that you didn't draw. But I'll leave the implementation of that as an exercise for the reader.
I got it! Thanks for the help.
Here is the meat of it, everything else was fine:
line(x, y, x, y-x2); //right
line(x-x2, y, x-x2, y-x2); //left
if (num>0) {
draws(x-(x2/3), y-x2, x2/3, num-1); // Central one
draws(x+(x2/1.5), y, x2/3, num-1); // right one
draws(x-(x2*1.33), y, x2/3, num-1); // left one
}
if (num==0) { //If there are no more instances, draw the horzontal lines
line (x, y, (x+x2), y); //right
line (x-x2, y, x-(x2*2), y); // left
line (x, y-x2, x-x2, y-x2); //top
}
Currently, the PolarChart joins all the coordinates with lines creating a polygon. I just want it to plot each point with a dot and NOT join them together. Is this possible?
I have tried using translateValueThetaRadiusToJava2D() and Graphics2D to draw circles but it's very clunky and contrived.
Any suggestions welcome!
So the DefaultPolarItemRenderer takes in all the polar points, converts the polar points to regular Java2D coordinates, makes a Polygon with those points and then draws it. Here's how I got it to draw dots instead of a polygon:
public class MyDefaultPolarItemRenderer extends DefaultPolarItemRenderer {
#Override
public void drawSeries(java.awt.Graphics2D g2, java.awt.geom.Rectangle2D dataArea, PlotRenderingInfo info, PolarPlot plot, XYDataset dataset, int seriesIndex) {
int numPoints = dataset.getItemCount(seriesIndex);
for (int i = 0; i < numPoints; i++) {
double theta = dataset.getXValue(seriesIndex, i);
double radius = dataset.getYValue(seriesIndex, i);
Point p = plot.translateValueThetaRadiusToJava2D(theta, radius,
dataArea);
Ellipse2D el = new Ellipse2D.Double(p.x, p.y, 5, 5);
g2.fill(el);
g2.draw(el);
}
}
}
and then instantiated this class elsewhere:
MyDefaultPolarItemRenderer dpir = new MyDefaultPolarItemRenderer();
dpir.setPlot(plot);
plot.setRenderer(dpir);
This one's a little harder. Given a PolarPlot, you can obtain its AbstractRenderer and set the shape. For example,
PolarPlot plot = (PolarPlot) chart.getPlot();
AbstractRenderer ar = (AbstractRenderer) plot.getRenderer();
ar.setSeriesShape(0, ShapeUtilities.createDiamond(5), true);
The diamond will appear in the legend, but the DefaultPolarItemRenderer neither renders shapes, nor provides line control. You'd have to extend the default renderer and override drawSeries(). XYLineAndShapeRenderer is good example for study; you can see how it's used in TimeSeriesChartDemo1.
If this is terra incognita to you, I'd recommend The JFreeChart Developer Guide†.
†Disclaimer: Not affiliated with Object Refinery Limited; I'm a satisfied customer and very minor contributor.
This is an excellent discussion, in case you want the function to pick up the color assigned by user to the series
add ...
Color c =(Color)this.lookupSeriesPaint(seriesIndex);
g2.setColor(c);
before ...
g.draw(e1);
there are other functions... use code completion to see what else functions are available against series rendereing with name starting from lookupSeries........(int seriesindex)
I found a rather strange way to get the points without any lines connecting them.
I set the Stroke of the renderer to be a thin line, with a dash phase of 0, and length of 1e10:
Stroke dashedStroke = new BasicStroke(
0.0f, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND,
0.0f, new float[] {0.0f, 1e10f}, 1.0f );
renderer.setSeriesStroke(0, dashedStroke);