Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

two y axis with different coordinates, one on left and one on right #20

Closed
asmwarrior opened this issue Mar 27, 2024 · 67 comments
Closed

Comments

@asmwarrior
Copy link

Hi, I have a feature request which I would like to ask here.

In my application, I need to plot two plots in a same graph/chart. But each plot has its own unit.

This means that I need two Y axis, one on left margin and one on right margin.

Is this possible? I'm not sure you can understand my need, maybe, this is something like in the below link:

Plots with different scales — Matplotlib 3.8.3 documentation

Thanks.

@GitHubLionel
Copy link
Owner

Hi,

I think this is not possible. You can create a second Y axis but you can not attach it to a specific plot.
And I don't know if it is possible to do that without major change in the code.
It would be necessary to be able to connect a graph to an axis.
Interesting problem but no time to investigate that.
Regards,
Lionel

@asmwarrior
Copy link
Author

Hi, Lionel, thanks for your response.

I know it is complex to add such feature, so I think leave this issue open. Maybe some day we can find a way to implement it.

Thanks.

@asmwarrior
Copy link
Author

Some good news about two Y axies in Wisteria-Dataviz (also a wxWidgets based project), see here:

Any feature about two Y axies in the chart? · Issue #43 · Blake-Madden/Wisteria-Dataviz

@asmwarrior
Copy link
Author

I have one question, for the code here:

void mpScaleY::DoPlot(wxDC &dc, mpWindow &w)
{
int orgx = GetOrigin(w);
// Draw nothing if we are outside margins
if (!m_drawOutsideMargins && ((orgx > (w.GetScreenX() - w.GetMarginRight())) || (orgx + 1 < w.GetMarginLeft())))
return;
// Draw Y axis
dc.DrawLine(orgx + 1, m_plotBondaries.startPy, orgx + 1, m_plotBondaries.endPy);
const double scaleY = w.GetScaleY();
const double step = GetStep(scaleY);
const double end = w.GetPosY() + (double)w.GetScreenY() / scaleY;
wxString fmt;
if (m_labelFormat.IsEmpty())
{
double maxScaleAbs = fabs(w.GetDesiredYmax());
double minScaleAbs = fabs(w.GetDesiredYmin());
double endscale = (maxScaleAbs > minScaleAbs) ? maxScaleAbs : minScaleAbs;
if ((endscale < 1e4) && (endscale > 1e-3))
fmt = _T("%.2f");
else
fmt = _T("%.2e");
}
else
{
fmt = m_labelFormat;
}
double n = floor((w.GetPosY() - (double)(w.GetScreenY()) / scaleY) / step) * step;
wxCoord tmp = 65536;
wxCoord labelW = 0;
// Before staring cycle, calculate label height
wxCoord labelHeigth = 0;
wxString s;
wxCoord tx = 0, ty = 0;
s.Printf(fmt, n);
dc.GetTextExtent(s, &tx, &labelHeigth);
labelHeigth /= 2;
// Draw grid, ticks and label
for (; n < end; n += step)
{
// To have a real zero
if (fabs(n) < 1e-10)
n = 0;
const int p = (int)((w.GetPosY() - n) * scaleY);
if ((p > m_plotBondaries.startPy + labelHeigth) && (p < m_plotBondaries.endPy - labelHeigth))
{
// Draw axis grids
if (m_grids && (n != 0))
{
dc.SetPen(m_gridpen);
dc.DrawLine(m_plotBondaries.startPx + 1, p, m_plotBondaries.endPx - 1, p);
}
// Draw axis ticks
if (m_ticks)
{
dc.SetPen(m_pen);
if (m_flags == mpALIGN_BORDER_LEFT)
{
dc.DrawLine(orgx, p, orgx + 4, p);
}
else
{
dc.DrawLine(orgx - 4, p, orgx, p);
}
}
if (IsLogAxis())
{
s = FormatLogValue(n);
if (s.IsEmpty())
continue;
}
else
s.Printf(fmt, n);
// Print ticks labels
dc.GetTextExtent(s, &tx, &ty);
#ifdef MATHPLOT_DO_LOGGING
if (ty != labelHeigth)
wxLogMessage(_T("mpScaleY::Plot: ty(%d) and labelHeigth(%d) differ!"), ty, labelHeigth);
#endif
labelW = (labelW <= tx) ? tx : labelW;
if ((tmp - p + labelHeigth) > MIN_Y_AXIS_LABEL_SEPARATION)
{
if ((m_flags == mpALIGN_BORDER_LEFT) || (m_flags == mpALIGN_RIGHT))
dc.DrawText(s, orgx + 4, p - ty / 2);
else
dc.DrawText(s, orgx - tx - 4, p - ty / 2);
tmp = p - labelHeigth;
}
}
}
// Draw axis name
DrawScaleName(dc, w, orgx, labelW);
}

If I have a second Y(in the right margin) axis, and if I set the maxY value and minY value for this Y axis, and can I calculate each ticks and each labels for this second Y axis?

For example, I have the first Y axis on the window, which is in the Y coordinate range [0, 100], and for the second Y axis, it ranges at [0, 200]. So, for each ticks label in the right axis is twice as the each ticks in the left axis.

I mean, is it possible to customize the tick labels in the right Y axis?

I think the above is the first step. If I can correctly draw the second Y axis, than, I will find a way to draw the lines according to the "scale and shift" of the second Y axis, because I can easily apply the transforms on the lines (x,y) coordinates.

@GitHubLionel
Copy link
Owner

Hi,

For your example, on can simply introduce a new property factor in the mpScaleY class. Then in line 1838 you will have :
s.Printf(fmt, n * factor);
So with factor = 2 for the second Y axis, when you read 100 in the first axis, you will read 200 in the second axis.

But well, I don't think that's what you want.
In fact, I don't know how to set maxY and minY for an axis. The logic is that axis are drawn first according the full screen available (m_plotBondaries). Then after, curves are drawn but there are no link between axis and curves, only the "plotBondaries" is pertinent.

asmwarrior added a commit to asmwarrior/wxMathPlot that referenced this issue May 2, 2024
see here for discussion:
two y axis with different coordinates, one on left and one on right · Issue GitHubLionel#20 · GitHubLionel/wxMathPlot — GitHubLionel#20
@asmwarrior
Copy link
Author

I have commit my first changes to my fork (* try to implement a second Y axis which has different tick labels)

here is the result:

image

It looks good.

In fact, I don't know how to set maxY and minY for an axis. The logic is that axis are drawn first according the full screen available (m_plotBondaries). Then after, curves are drawn but there are no link between axis and curves, only the "plotBondaries" is pertinent.

This is one issue, yes, I see the a curve is not based on an axis, but I think I may need to extend a new curve class, which can depends on the "mpScaleY2".

My design strategy is that I can have a function newY = oldY * scale + shift for the Y2 axis, so for the mpScaleY2 class, I only need to remember the scale and shift member variable. Now, the remaining steps are:

1, I have already know how to draw the Y axis and Y axis's tick lines(horizontal lines)
2, When initialize the maxY and minY for the Y2 axis, I need to calculate the scale and shift member variable for the Y2 axis
3, after that, I think the Y2's scale and shift is totally fixed (depends on the Y axis's maxY and minY)
4, if the left Y axis's max Y and minY get changed, I need to adjust/recalculate the Y2's scale and shift

The hardest part is the step 2.

Here comes another issue, does the "tick lines" draw twice if I have both Y axis one the left side, and Y2 axis one the right side? How can I determine both the left Y and right Y are drawing in the same "steps" (in pixel coordinates).

@GitHubLionel
Copy link
Owner

Hi,

The crux of the problem is the definition of m_scaleY. All other parameters are defined with it, like m_posY.
So we need another m_scaleY for the second axis.
I made a try. I introduced m_scaleY2, m_posY2 and a factor in mpWindow class. In mpScaleY, I just add a boolean to say that it is another Y axis. And in mpFX, mpFY and mpFXY, another boolean to say that we use another axis.
With these little changes, I have almost solve the problem.
You just have to :

  • define the factor for example to reduce by 2 the axis : m_plot->Y2Factor = 2.0;
  • define another axis with Y2Axis to true
  • say UseY2Axis to true for the series you want to fit to Y2 axis.

You have sources in the sandbox repository : https://github.com/GitHubLionel/wxMathPlot/tree/master/Sandbox

There are some problems when you resize the windows or when you select an area, but for the moment, it is just a test.

@GitHubLionel
Copy link
Owner

We have strange behaviour when we select an area to zoom. Since the coordinates are in the first axis referential, if we select an object in the second axis referential, the result is correct but not what we expected. I don't know how to correct this effect.

@asmwarrior
Copy link
Author

Hi, thanks for your effort, I would suggest that you could use a new "git branch" to handle such feature implementation, because if I try to use the new file inside the "SandBox" folder, it is hard to check the modifications.

I haven't tried your code in the "SandBox" yet, I will test them soon.

About the issue you mentioned:

We have strange behaviour when we select an area to zoom. Since the coordinates are in the first axis referential, if we select an object in the second axis referential, the result is correct but not what we expected. I don't know how to correct this effect.

I think it is OK to keep a scale and shift value from the coordinates transform based on Y1 axis to Y2 axis. That scale and shift value should be fixed. So, once you zoom the Y1 axis, you should also zoom the Y2 axis in the same level. I mentioned this way in my previous comments.

Thanks.

@GitHubLionel
Copy link
Owner

Yes, if you zoom on Y1, you also zoom in Y2. It is logic.
But if we have this configuration with blue/yellow sinus on Y2 (range 1-2) and others curves on Y1 :
Z1
And then try to select an area around it, you will obtain this :
Z2
because in fact, you have selected an area in range 2-4 (Y1 axis).
Result is correct (we see green curve) but not what you expected (blue/yellow sinus is not visible).

@asmwarrior
Copy link
Author

Sorry for the late reply.

I just copy the wxMathPlot.h/cpp file from the SandBox folder to the "mathplot", and can you show me what is the client code to use the second Y axis?

Thanks.

@GitHubLionel
Copy link
Owner

Has I write previously,
You just have to :

  • define the factor for example to reduce by 2 the axis : m_plot->Y2Factor = 2.0;
  • define another axis with Y2Axis to true
  • say UseY2Axis to true for the series you want to fit to Y2 axis.
    So :
    m_plot->Y2Factor = 2.0;
    mpScaleY *rightAxis = new mpScaleY(wxT("Y2"), mpALIGN_RIGHT, true, true);
    For a mpFX, mpFy, ... serie : serie->UseY2Axis = true;

@asmwarrior
Copy link
Author

Thanks. I see you mentioned issue.

This issue happens you use the mouse to drag a rectangle, and you want to zoom to this rectangle.
This issue does not happen when you use mouse scroll to zoom in or zoom out the scene.

So, my guess is that the issue happens from the function:

void mpWindow::ZoomRect(wxPoint p0, wxPoint p1)
{
  // Compute the 2 corners in graph coordinates:
  double p0x = p2x(p0.x);
  double p0y = p2y(p0.y);
  double p1x = p2x(p1.x);
  double p1y = p2y(p1.y);

  // Order them:
  mpFloatRect zoom;
  zoom.Xmin = p0x < p1x ? p0x : p1x;
  zoom.Xmax = p0x > p1x ? p0x : p1x;
  zoom.Ymin = p0y < p1y ? p0y : p1y;
  zoom.Ymax = p0y > p1y ? p0y : p1y;

#ifdef MATHPLOT_DO_LOGGING
  wxLogMessage(_T("Zoom: (%f,%f)-(%f,%f)"), zoom.Xmin, zoom.Ymin, zoom.Xmax, zoom.Ymax);
#endif

  Fit(zoom);
}

Here, the zoom variable is defined for the view XY coordinates of Y1.

So, later, inside the function Fit(zoom), you need to special handling of the zoom. So that the zoom can correctly apply to the lines depends on Y2 axis.

// JL
void mpWindow::Fit(const mpFloatRect &rect, wxCoord *printSizeX, wxCoord *printSizeY)
{
  if (m_magnetize)
  {
    // Avoid paint cross if mouse move
    m_repainting = true;
  }

  // Save desired borders:
  m_desired = rect;

  if (printSizeX != NULL && printSizeY != NULL)
  {
    // Printer:
    SetScreen(*printSizeX, *printSizeY);
  }
  else
  {
    // Normal case (screen):
    int h, w;
    GetClientSize(&w, &h);
    SetScreen(w, h);
  }

  double Ax, Ay;

  Ax = rect.Xmax - rect.Xmin;
  Ay = rect.Ymax - rect.Ymin;

  m_scaleX = ISNOTNULL(Ax) ? m_plotWidth / Ax : 1;
  m_scaleY = ISNOTNULL(Ay) ? m_plotHeight / Ay : 1;
  m_scaleY2 = m_scaleY * Y2Factor;

  if (m_lockaspect)
  {
#ifdef MATHPLOT_DO_LOGGING
    wxLogMessage(_T("mpWindow::Fit()(lock) m_scaleX=%f,m_scaleY=%f"), m_scaleX, m_scaleY);
#endif
    // Keep the lowest "scale" to fit the whole range required by that axis (to actually "fit"!):
    double s = m_scaleX < m_scaleY ? m_scaleX : m_scaleY;
    m_scaleX = s;
    m_scaleY = s;
    m_scaleY2 = s;
  }

  // Adjusts corner coordinates: This should be simply:
  //   m_posX = m_minX;
  //   m_posY = m_maxY;
  // But account for centering if we have lock aspect:
  m_posX = (rect.Xmin + rect.Xmax) / 2 - (m_plotWidth / 2 + m_margin.left) / m_scaleX;
  m_posY = (rect.Ymin + rect.Ymax) / 2 + (m_plotHeight / 2 + m_margin.top) / m_scaleY;
  m_posY2 = (rect.Ymin + rect.Ymax) / 2 + (m_plotHeight / 2 + m_margin.top) / m_scaleY2;

#ifdef MATHPLOT_DO_LOGGING
  wxLogMessage(_T("mpWindow::Fit() m_desired.Xmin=%f m_desired.Xmax=%f  m_desired.Ymin=%f m_desired.Ymax=%f"),
      m_desired.Xmin, m_desired.Xmax, m_desired.Ymin, m_desired.Ymax);
  wxLogMessage(_T("mpWindow::Fit() m_scaleX = %f , m_scrX = %d,m_scrY=%d, Ax=%f, Ay=%f, m_posX=%f, m_posY=%f"),
      m_scaleX, m_scrX, m_scrY, Ax, Ay, m_posX, m_posY);
#endif

  // It is VERY IMPORTANT to DO NOT call Refresh if we are drawing to the printer!!
  // Otherwise, the DC dimensions will be those of the window instead of the printer device
  if (printSizeX == NULL || printSizeY == NULL)
    UpdateAll();
}

I'm debugging the above function, and try to see if I can solve the issue.

asmwarrior added a commit to asmwarrior/wxMathPlot that referenced this issue May 10, 2024
see here for discussion:
two y axis with different coordinates, one on left and one on right · Issue GitHubLionel#20 · GitHubLionel/wxMathPlot — GitHubLionel#20
@asmwarrior
Copy link
Author

I have my test code in this commit:

add a second Y axis testing code

I see that sometimes, the relative position/scale of the two curves depends on Y1 and Y2 axes are changing. From my point of view, they should be fixed.

One issue I see is the "tick line" will becomes interleaved, see below images:

image

When I use the mouse scroll to zoom in, it becomes like below:

image

@asmwarrior
Copy link
Author

Suppose curve1 is depends on Y1, and Y1 has the property: YOffset, YScale.

So, to draw curve1 on the screen(pixel coordinates), I have to: YPixel = (Y - YOffset) * YScale

For curve2 on Y2 axis, when draw on the screen, you have to Y2Pixel = (Y - Y2Offset) * Y2Scale

But when you zoom the screen, you have to adjust the YOffset, YScale, Y2Offset, Y2Scale. But I think their relative position and scale should be fixed.

Currently, I see the relative position and scale are not fixed.

asmwarrior added a commit to asmwarrior/wxMathPlot that referenced this issue May 10, 2024
@asmwarrior
Copy link
Author

OK, I think the below commit can fix such problem, see here:

* we should key the relative position and scale of the curves from Y1 and Y2 axes

@asmwarrior
Copy link
Author

This is the method I use, see the below image:

image

I need to calculate the new_posY2.

@asmwarrior
Copy link
Author

When I try to use the right mouse context menu, and click the "lock aspect" item, I see it still has some zoom issue.

@GitHubLionel
Copy link
Owner

Ok, I add your correction and add a test. Now it is working when we zoom an area, fit or resize. It is working also with lock aspect but have again a problem when we unlock aspect. But with a fit, thinks return good.

@asmwarrior
Copy link
Author

asmwarrior commented May 11, 2024

I think one issue remains: how to initialized the initial fit.

I mean for example, we have curve0 and curve1 on Y1, and curve2 and curve 3 and Y2. Now, I think the correct way to make a fit is:

Find the bounding box of "curve0 and curve1", and find the bounding box of "curve2 and curve3". Then, we need to find an initial m_posY and m_posY2. Suppose we need to set the Y2Factor value by myself.

If I remember correctly, when start the program, only the "curve0 and curve1" will fit to the canvas window.

In the future, maybe, we can make Y2Factor automatically set by the program. But in this case, the tick lines from Y1 and Y2 may be interleaved.

@GitHubLionel
Copy link
Owner

Yes need more reflections !
I have posted some modifications to have clean code.
I also add this property to configuration Series page.

@asmwarrior
Copy link
Author

Thanks.

Yes need more reflections !

What does this sentence mean?

I looked at your code, and I see you have a bool variable FitWithBound

I'm not sure what does this variable used for, it looks like in the function:

void mpWindow::OnSize(wxSizeEvent &WXUNUSED(event))
{
  // Try to fit again with the new window size:
  FitWithBound = true;
  Fit(m_desired);
#ifdef MATHPLOT_DO_LOGGING
  wxLogMessage(_T("mpWindow::OnSize() m_scrX = %d, m_scrY = %d"), m_scrX, m_scrY);
#endif // MATHPLOT_DO_LOGGING
}

And

void mpWindow::ZoomRect(wxPoint p0, wxPoint p1)
{
  // Compute the 2 corners in graph coordinates:
  double p0x = p2x(p0.x);
  double p0y = p2y(p0.y);
  double p1x = p2x(p1.x);
  double p1y = p2y(p1.y);

  // Order them:
  mpFloatRect zoom;
  zoom.Xmin = p0x < p1x ? p0x : p1x;
  zoom.Xmax = p0x > p1x ? p0x : p1x;
  zoom.Ymin = p0y < p1y ? p0y : p1y;
  zoom.Ymax = p0y > p1y ? p0y : p1y;

#ifdef MATHPLOT_DO_LOGGING
  wxLogMessage(_T("Zoom: (%f,%f)-(%f,%f)"), zoom.Xmin, zoom.Ymin, zoom.Xmax, zoom.Ymax);
#endif

  FitWithBound = true;
  Fit(zoom);
}

In the above two cases, the code goes to the first branch. In other cases, it goes to the else branch.

  if (FitWithBound)
  {
    m_posY2 += (m_posY - old_posY)/Y2Factor;
    FitWithBound = false;
  }
  else
    m_posY2 = (rect.Ymin + rect.Ymax) / 2 + (m_plotHeight / 2 + m_margin.top) / m_scaleY2;

I'm not understand this logic. Thanks.

@asmwarrior
Copy link
Author

when I start the demo program, I got such result:

image

It looks like the X coordinates is a combination of two curves(the blue curve0 and the red curve1). While the curve1 is truncated.

The expect result could be like below:
image

Currently, the expect result can be achieved by using the mouse to drag a rectangle which covers the two curves.

Another issue is the tick lines, it is expected that the Y1 and Y2 tick lines should be identical(overlap). I'm not sure how to implement this.

@GitHubLionel
Copy link
Owner

In last post, I try another approach by computing bound for Y and Y2.
Now, Y2Factor is unnecessary.
FitWithBound is a "workaround" for the moment.

@asmwarrior
Copy link
Author

In last post, I try another approach by computing bound for Y and Y2. Now, Y2Factor is unnecessary. FitWithBound is a "workaround" for the moment.

You "last post" means the last commit you pushed minutes ago?

Compute bound on Y and Y2?

@GitHubLionel
Copy link
Owner

Yes

@asmwarrior
Copy link
Author

I think the English translation is the "series", so you have "series 0", "series 1" for different curves.

@asmwarrior
Copy link
Author

Well, the scale not take account of the size of the marker. So it can be cut if the point is exactly at the beginning of the scale.

Yes, I know this. If I remember correctly, I have report such issue before.

Another point is if we take or not the limit of the range of the plot : i<100 or i<=100 for the sinus curve. Also the LIMIT used to define the bound in AddData method.

Sorry, I do not understand your sentence above.

I have updated the main branch.

Good, I see you merged all the changes in sandbox to the main folder.

@GitHubLionel
Copy link
Owner

LIMIT increase a little the bound.
So I replace LIMIT by a property m_limit_percent. Then if you call SetLimitPercent(0); before added points, you will have end point exactly on the axis.

@asmwarrior
Copy link
Author

In the function: void MathPlotConfigDialog::Initialize()

I have to use this else condition:

    else if (classname.IsSameAs(_T("mpScaleY")))
    {
      if (((mpScaleY*)axis)->IsY2Axis())
      {
        ChoiceAxis->Append(_T("Y2 axis - ") + axis->GetName());
      }
      else
        ChoiceAxis->Append(_T("Y axis - ") + axis->GetName());
    }

Because I have some mpScale derived class, which should not list in the axes list.

@asmwarrior
Copy link
Author

I think I have found a bug of the fit function

	// add a simple sinus serie
	mpFXYVector *serie = m_plot->GetXYSeries(0);
	for (int i = 0; i < 100; i++)
		serie->AddData(i / 10.0 + 40000, sin(i / 10.0), true);
	m_plot->Fit();  //  UpdateAll
	legend->SetNeedUpdate();

	mpFXYVector *serie2 = m_plot->GetXYSeries(1);
    for (int i = 0; i < 100; i++)
		serie2->AddData(i / 10.0 + 40000 + 5, 3 * sin(i / 10.0), true);
    //serie2->SetY2Axis(true);
    serie2->SetBrush(*wxRED);
	serie2->SetSymbol(mpsCircle);

	// Some decoration
	serie->SetBrush(*wxYELLOW);
	serie->SetSymbol(mpsCircle);

When started, I see the plot looks like below:

image

When I use the mouse to zoom to a rectangle, I got the correct result:

image

This is the latest git head code of the demo project.

@GitHubLionel
Copy link
Owner

GitHubLionel commented May 16, 2024

This is not a bug. Result is correct, this the effect of LIMIT now renamed m_limit_percent.
Add serie->SetLimitPercent(0); and serie2->SetLimitPercent(0); and you will have a more precise fit.

mpFXYVector *serie = m_plot->GetXYSeries(0);
  serie->SetLimitPercent(0);
  for (int i = 0; i < 100; i++)
    serie->AddData(i / 10.0 + 40000, sin(i / 10.0), true);
  m_plot->Fit();  //  UpdateAll
  legend->SetNeedUpdate();

  mpFXYVector *serie2 = m_plot->GetXYSeries(1);
  serie2->SetLimitPercent(0);
    for (int i = 0; i < 100; i++)
    serie2->AddData(i / 10.0 + 40000 + 5, 3 * sin(i / 10.0), true);
    //serie2->SetY2Axis(true);
    serie2->SetBrush(*wxRED);
  serie2->SetSymbol(mpsCircle);`

@asmwarrior
Copy link
Author

asmwarrior commented May 17, 2024

OK, thanks for the help. I remembered that I have reported such issue before, see here:

bugs in calculation the bounding box · Issue #18 · GitHubLionel/wxMathPlot

I think we should consider the actual x or y's range, not the values.

For example, if the x coordinates range is [40000, 40020], it should be the same as the [0, 20], because they have the same range. But what I reported in that issue is that currently, the gap is depends on the values, not on the ranges length.

So, for the current code:

    m_minX = xs[0] - (fabs(xs[0]) * m_limit_percent);
    m_maxX = xs[0] + (fabs(xs[0]) * m_limit_percent);
    m_minY = ys[0] - (fabs(ys[0]) * m_limit_percent);
    m_maxY = ys[0] + (fabs(ys[0]) * m_limit_percent);

This is NOT correct, it depends on the values, not one the range length.

It should be like below:

m_minX = xs[0] - (fabs(range_length_of_x) * m_limit_percent);

@asmwarrior
Copy link
Author

asmwarrior commented May 17, 2024

I read the code how to calculate the boundary of the x or y.

I see the logic is that, when you add each point, you need to calculate the bounding box. When calculate the bounding box's x limit(the minimal x or maximal x), you use the m_limit_percent.

I think this logic is not correct. When calculating the bounding box, we should NOT use the m_limit_percent, as you mentioned, you should make m_limit_percent == 0. So, you got an accurate bounding box.

But when you run the fit function, you have to expand the bounding box a little. So, for a line which has range [40000, 40020], you may has an actual range [40000 - 20*5%, 400020 + 20*5%], here the 20 is the range length, and 5% is the actual m_limit_percent we should set.

I hope you can understand my idea. Thanks.

@GitHubLionel
Copy link
Owner

GitHubLionel commented May 17, 2024

Ok, I try to explain why I use this method.
In my application, the points are not calculated in one time like in your sinus sample. They are computed step by step and added to the serie. To avoid total repainting each time I add new point, I try to anticipate the next point by increasing the bound of the added point.
An idea I just have maybe better is to compute the delta between previous point added and add this delta to the bond ...

@asmwarrior
Copy link
Author

Ok, I try to explain why I use this method. In my application, the points are not calculated in one time like in your sinus sample. They are computed step by step and added to the serie. To avoid total repainting each time I add new point, I try to anticipate the next point by increasing the bound of the added point. An idea I just have maybe better is to compute the delta between previous point added and add this delta to the bond ...

If I understand your idea, this is some kind of Kalman filter, but you still need a delta, not the absolute valve of the point.

I think my idea does not conflict with your idea, The delta is just a range. So you need to keep some space in the margin, In this case when new point added you don't need to repaint the whole plot chat, You just need to draw the new point.

@GitHubLionel
Copy link
Owner

Ok, I try a new approach with delta on X and Y.
It seem to work.

@asmwarrior
Copy link
Author

Ok, I try a new approach with delta on X and Y. It seem to work.

I see your new approach, but I still don't know the logic.

If I have several points like:

(1,2), (2,2), (3,2.5)

In this case, the y delta could be "0"? Is that correct?

@asmwarrior
Copy link
Author

I have such test code for the demo project:

	mpScaleY *rightAxis = new mpScaleY(wxT("Y2"), mpALIGN_RIGHT, true, true);
	m_plot->AddLayer(rightAxis);

and

	// add a simple sinus serie
	mpFXYVector *serie0 = m_plot->GetXYSeries(0);
    //serie0->SetLimitPercent(0);
	for (int i = 0; i < 100; i++)
		serie0->AddData(i / 10.0 + 40000, sin(i / 10.0), true);
    	// Some decoration
	serie0->SetBrush(*wxYELLOW);
	serie0->SetSymbol(mpsCircle);

	mpFXYVector *serie1 = m_plot->GetXYSeries(1);
    //serie1->SetLimitPercent(0);
    for (int i = 0; i < 100; i++)
		serie1->AddData(i / 10.0 + 40000 + 5, 3 * sin(i / 10.0) + 4.0, true);
    serie1->SetBrush(*wxRED);
	serie1->SetSymbol(mpsCircle);
	//serie1->SetY2Axis(true);

    mpFXYVector *serie2 = m_plot->GetXYSeries(2);
    //serie2->SetLimitPercent(0);
    for (int i = 0; i < 100; i++)
		serie2->AddData(i / 10.0 + 40000 + 5, -4.0, true);
    serie2->SetBrush(*wxBLUE);
	serie2->SetSymbol(mpsSquare);
    //serie2->SetY2Axis(true);

	legend->SetNeedUpdate();
	m_plot->Fit();  //  UpdateAll

You see, by default, all the 3 lines were added to the Y axis, and there is NO lines in the Y2 axis.

When start up, it looks like below:

image

This looks OK.

Now, in the configure dialog, I try to select the "serial 1" to the Y2 axis, see the below image:

image

After "Fit" the screen, the result looks wrong here, see the image below:

image

You can see the arrow in the above image, it should span the whole Y2 ranges.

@GitHubLionel
Copy link
Owner

Ok, I add a test on UpdateBBox.
Note that I see that I must correct the "Take care of scale : restrict bound" part.

@asmwarrior
Copy link
Author

Ok, I add a test on UpdateBBox.

image

I update to your current git head, and it looks OK now.

Thanks.

Note that I see that I must correct the "Take care of scale : restrict bound" part.

You mean you need to update/correct some code in the bounding box calculation?

@asmwarrior
Copy link
Author

asmwarrior commented May 18, 2024

boundingbox

I draw an image of how the bounding box can be calculated.

We should calculate the actual bounding box(the red dashed rectangle), and the outer blue rectangle is the window/chart size. There is a gap, such as 10% between those two rectangles in up/down/left/right 4 direction.

So, for the left(minimal X) direction:

m_extended_minX = m_actual_minX - (m_actual_range_X * m_limit_percent);

Here, we could still use the m_limit_percent, in my case, it is 10%.

I hope you can understand my idea.

When adding one data point, the m_actual_minX should be updated, and m_extended_minX will be extended by m_limit_percent factor.

@asmwarrior
Copy link
Author

asmwarrior commented May 18, 2024

How to draw a customized class object in the Y2 axis related coordinates.

Hi, I have a customized class like below:

class WXDLLIMPEXP_MATHPLOT HorizontalLine : public mpScale
{
public:
    HorizontalLine(const wxString& name = _T("label"), int flags = mpALIGN_CENTERY, bool grids = false)
        : mpScale(name, flags, grids)
    {
    }

    /** Layer plot handler.
     * This implementation will plot the ruler adjusted to the visible area.
     */
    virtual void DoPlot(wxDC& dc, mpWindow& w);

    /** Specifies that this is a Scale layer.
     * @return always \a TRUE
     * @sa mpLayer::IsScale
     */
    virtual bool IsScale(mpScaleType* scale)
    {
        *scale = NULL;
        return true;
    }

    virtual bool IsLogAxis();
    virtual void SetLogAxis(bool log);
    virtual int GetOrigin(mpWindow &w);
    virtual void DrawScaleName(wxDC &dc, mpWindow &w, int origin, int labelSize);

    /// the y coordinate of the horizontal line
    int m_Y = 0;

protected:

    DECLARE_DYNAMIC_CLASS(HorizontalLine)
};

This object draws like X axis, and has a m_Y field, so I can draw a horizontal line.

But what I want is that I would like to draw this horizontal line in the Y2's coordinates system.

I see that if I want to draw on a Y2's coordinates, I have to derive this class from the mpFunction, and later, those objects will exists in the legend's curve list. Since this is a custom class, I don't want to show this object in the legend. How to do that? Thanks.

@GitHubLionel
Copy link
Owner

You mean you need to update/correct some code in the bounding box calculation?

Yes because we have the choice between fixed or automatic bound. I need to update the code for Y2.

If we want extra marge, the more simple is to add this marge when we draw the box around the curve. For example, if we want 5 pixels, then in mpWindow::OnPaint method :

  // Draw background plot area
  trgDc->SetBrush(m_bgColour);
  trgDc->SetTextForeground(m_fgColour);
  trgDc->DrawRectangle(m_margin.left-5, m_margin.top-5, m_plotWidth+10, m_plotHeight+10);

For an horizontal line, why not use mpFX :

class HorizontalLine : public mpFX
{
    double m_yvalue;
  public:
    HorizontalLine (double yvalue) :
        mpFX(wxT("Horizontal line"), mpALIGN_LEFT)
    {
      m_yvalue = yvalue;
      wxPen FXpen(*wxGREEN, 1, wxPENSTYLE_SOLID);
      SetDrawOutsideMargins(false);
      SetContinuity(true);
      SetPen(FXpen);
      SetY2Axis(true);
    }
    virtual double GetY(double WXUNUSED(x))
    {
      return m_yvalue;
    }
    virtual double GetMinY()
    {
      return m_yvalue - 0.05;
    }
    virtual double GetMaxY()
    {
      return m_yvalue + 0.05;
    }
};

Then to add layer horizontal line :
m_plot->AddLayer(new HorizontalLine(3.0));

@asmwarrior
Copy link
Author

Hi, about the HorizontalLine class design, thanks.

It's working now, here is the code that I derived from mpFX

IMPLEMENT_DYNAMIC_CLASS(HorizontalLine, mpFunction)

void HorizontalLine::DoPlot(wxDC& dc, mpWindow& w)
{
    int orgy = GetOrigin(w);
    wxCoord iy = w.y2p(m_Y, UseY2Axis);

    // Draw nothing if we are outside margins
    if (!m_drawOutsideMargins && ((iy > (w.GetScreenY() - w.GetMarginBottom())) || (iy < w.GetMarginTop())))
      return;

    // Draw horizontal lines from boundary minX to boundary maxX
    dc.DrawLine(m_plotBondaries.startPx, iy, m_plotBondaries.endPx, iy);
}

Those lines are like threshold lines.

Also, I have disable the bounding box calculation:

    virtual bool HasBBox() override
    {
      return false;
    }

And the type I set is: m_type = mpLAYER_INFO;. So they are not shown in the legend.

@GitHubLionel
Copy link
Owner

Ok, I see what you want. Simply is to use mpFunction.
I add mpHorizontalLine and mpVerticalLine.
Now the only code you have to do is : m_plot->AddLayer(new mpHorizontalLine(3.0, *wxGREEN, true));

@asmwarrior
Copy link
Author

Ok, I see what you want. Simply is to use mpFunction. I add mpHorizontalLine and mpVerticalLine. Now the only code you have to do is : m_plot->AddLayer(new mpHorizontalLine(3.0, *wxGREEN, true));

Thanks, the new class works OK, See the image below, it is shown in the Y2 coordinates.

image

And here is the related code:

	// add a simple sinus serie
	mpFXYVector *serie0 = m_plot->GetXYSeries(0);
    //serie0->SetLimitPercent(0);
	for (int i = 0; i < 100; i++)
		serie0->AddData(i / 10.0 + 40000, sin(i / 10.0), true);
    	// Some decoration
	serie0->SetBrush(*wxYELLOW);
	serie0->SetSymbol(mpsCircle);

	mpFXYVector *serie1 = m_plot->GetXYSeries(1);
    for (int i = 0; i < 100; i++)
		serie1->AddData(i / 10.0 + 40000 + 5, 3 * sin(i / 10.0) + 4.0, true);
    serie1->SetBrush(*wxRED);
	serie1->SetSymbol(mpsCircle);
	serie1->SetY2Axis(true);

    mpFXYVector *serie2 = m_plot->GetXYSeries(2);
    for (int i = 0; i < 100; i++)
		serie2->AddData(i / 10.0 + 40000 + 5, -4.0, true);
    serie2->SetBrush(*wxBLUE);
	serie2->SetSymbol(mpsSquare);
    //serie2->SetY2Axis(true);


    mpHorizontalLine* hLine = new mpHorizontalLine(/*y*/ 6, /*color*/ *wxGREEN, /*useY2Axis*/ true);
    wxPen FXpen(*wxGREEN, 3, wxPENSTYLE_SOLID);
    hLine->SetPen(FXpen);
    m_plot->AddLayer(hLine);

	legend->SetNeedUpdate();
	m_plot->Fit();  //  UpdateAll

One minor question: is it possible to draw this horizontal line in the background as the "axis" does. I mean when drawing all the layer objects, those horizontal lines should be drawn before the curves.

@GitHubLionel
Copy link
Owner

GitHubLionel commented May 20, 2024

One minor question: is it possible to draw this horizontal line in the background as the "axis" does. I mean when drawing all the layer objects, those horizontal lines should be drawn before the curves.

Simply add horizontal line layer before series layer.
... no, not work !
We need something like z-index

@GitHubLionel
Copy link
Owner

Ok, I add z-order for plot layer. Not sure of order but we must not break the original order plot.
Also, replace color by pen in horizontal and vertical axis, so code is simply :

wxPen Hor_pen(*wxGREEN, 3, wxPENSTYLE_SOLID);
  m_plot->AddLayer(new mpHorizontalLine(5.0, Hor_pen, true));`

@asmwarrior
Copy link
Author

Ok, I add z-order for plot layer. Not sure of order but we must not break the original order plot. Also, replace color by pen in horizontal and vertical axis, so code is simply :

wxPen Hor_pen(*wxGREEN, 3, wxPENSTYLE_SOLID);
  m_plot->AddLayer(new mpHorizontalLine(5.0, Hor_pen, true));`

Thanks, it works OK now.

Another minor issue:

image

The circle and square are using the same size = 6, but they look different.

    case mpsCircle:
      dc.DrawCircle(x, y, m_symbolSize);
      break;

    case mpsSquare:
      dc.DrawRectangle(x - m_symbolSize2, y - m_symbolSize2, m_symbolSize, m_symbolSize);
      break;

It looks like the circle has radius = 6, but for square, the side = 6.

@GitHubLionel
Copy link
Owner

It looks like the circle has radius = 6, but for square, the side = 6.

Yes it is correct.

@asmwarrior
Copy link
Author

OK, I think I'm going to close this issue, because all the feature I requested is completed.

Thanks.

If I have other questions or issues, I think will create new issues.

asmwarrior added a commit to asmwarrior/wxMathPlot that referenced this issue May 29, 2024
see here for discussion:
two y axis with different coordinates, one on left and one on right · Issue GitHubLionel#20 · GitHubLionel/wxMathPlot — GitHubLionel#20
asmwarrior added a commit to asmwarrior/wxMathPlot that referenced this issue May 29, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants