INPaintCallBack.OnBeforePaint interfering with NChartControl.ImageExporter


https://www.nevron.com/Forum/Topic12066.aspx
Print Topic | Close Window

By Kevin Harrison - 7 Years Ago
This is a behaviour change between 16.8.8.12 and 17.3.6.12

On your helpful recommendation we added an INPaintCallBack and use OnBeforePaint to allow the user to change the symbol sizes on chart lines by turning the mouse wheel. This works fine in 16.8.8.12 and 17.3.6.12. However, we have found that the OnBeforePaint code affects chart export in 17.3.612 - the chart is not resized in the export when the export size is changed form the default size.

I have tracked this to these lines of code:

this.chartControl.document.Calculate();
this.chartControl.RecalcLayout();

If they are removed the export is the correct size but the symbol resizing has not been applied to the export.

Is there a different way to achieve this in 17.3.6.12?

I also notice that previously I could reorganise panel layout; e.g. moving the legend panel from right to left and the chart would redraw. Now I have to explicitly call NChartControl.Refresh().

Some general advice on the changes and recommended strategies would be much appreciated.

Thanks

Kevin
By Nevron Support - 7 Years Ago
Hi Kevin,
Can you share some code that replicates this - or at least a some general guidelines - it is not clear which properties of the chart you modify on BeforePaint, the sequence of export etc.
By Kevin Harrison - 7 Years Ago
Here is an extract from the code in the INPaintCallback.OnBeforePaint method, which shows what I am changing. Basically, I am restricting the maximum size of line and point symbols.

If I comment out the last two ChartControl calls, then the exported chart resizes correctly (it is set to Dock.Fill) but the symbol size isn't restricted. If I leave them in everything works correctly in 16.8.8.12, but in 17.3.6.12 (and 17.3.28.12), the chart does not "fill" in the exported image.

foreach (NSeries series in chart.Series)
{
NPointSeries pointSeries = series as NPointSeries;
NLineSeries lineSeries = series as NLineSeries;
if (pointSeries != null || lineSeries != null)
{
ChartLine chartLine = chartDefinition.ChartSeriesSet.HavingId((int) series.Tag) as ChartLine;
Debug.Assert(chartLine != null);
ChartSymbol symbol = chartLine.RenderedSymbol;
Debug.Assert(symbol != null);
if (symbol.SymbolSizeStyle == ChartSymbolSizeStyle.PercentageWithMaximum && !chartLine.SymbolUserResized)
{
requiresRecalculate = true; // triggered by any resize
double percentageSize = symbol.SymbolSize;
double maximumSize = symbol.MaximumSymbolSize;
NLength requiredMarkerSize = new NLength(Math.Min((float) maximumSize, (float) (percentageScaling*percentageSize)), NGraphicsUnit.Pixel);

if (pointSeries != null)
pointSeries.Size = requiredMarkerSize;
else
{
lineSeries.MarkerStyle.Width = requiredMarkerSize;
lineSeries.MarkerStyle.Height = requiredMarkerSize;
}
}
}
}

// Recalculate the layout only if necessary
// TODO: In Nevron 17.3.6.12 this code prevents an exported image resizing. But removing the code doesn't action the symbol size changes.
if (requiresRecalculate)
{
this.chartControl.document.Calculate();
this.chartControl.RecalcLayout();
}
By Nevron Support - 7 Years Ago

Hi Kevin,

We were not able to replicate the problem - we tested with the following code:

using Nevron.Chart;
using Nevron.Chart.WinForm;
using Nevron.GraphicsCore;
using System;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
 public partial class Form1 : Form
 {
  public Form1()
  {
   InitializeComponent();
  }

  class MyPaintCallback : INPaintCallback
  {
   public MyPaintCallback(NChartControl chartControl)
   {
    m_ChartControl = chartControl;
   }
   public void OnAfterPaint(NPanel panel, NPanelPaintEventArgs eventArgs)
   {
    throw new NotImplementedException();
   }

   public void OnBeforePaint(NPanel panel, NPanelPaintEventArgs eventArgs)
   {
    bool requiresRecalculate = true;

    foreach (NSeries series in m_ChartControl.Charts[0].Series)
    {
     NPointSeries pointSeries = series as NPointSeries;
     if (pointSeries != null)
     {
      pointSeries.Size = new NLength(4, NGraphicsUnit.Pixel);
     }
    }

    if (requiresRecalculate)
    {
     m_ChartControl.document.Calculate();
     m_ChartControl.RecalcLayout();
    }
   }

   NChartControl m_ChartControl;
  }


  private void Form1_Load(object sender, EventArgs e)
  {
   NChart chart = nChartControl1.Charts[0];
   chart.DockMode = PanelDockMode.Fill;

   NPointSeries point = new NPointSeries();
   chart.Series.Add(point);
   Random rand = new Random();

   for (int i = 0; i < 20; i++)
   {
    point.Values.Add(rand.Next(100));
   }

   chart.PaintCallback = new MyPaintCallback(nChartControl1);

   nChartControl1.ImageExporter.SaveToFile("c:\\temp\\check.bmp", new NBitmapImageFormat());
  }
 }
}

And both the chart on the form and the exported image looked correct.

By Kevin Harrison - 7 Years Ago
"the chart is not resized in the export when the export size is changed from the default size."

You are using the default size.
By Nevron Support - 7 Years Ago
Hi Kevin,
The control works properly - when you call nChartControl.RecalcLayout it will use the default view context which has different size than the one in the view context that currently serves for image export - that's why you get the same size in the
chart rendered to bitmap as the one on the screen. The solution is to pass the view context of the current paint:

   public void OnBeforePaint(NPanel panel, NPanelPaintEventArgs eventArgs)
   {
    bool requiresRecalculate = true;

    foreach (NSeries series in m_ChartControl.Charts[0].Series)
    {
     NPointSeries pointSeries = series as NPointSeries;
     if (pointSeries != null)
     {
      pointSeries.Size = new NLength(4, NGraphicsUnit.Pixel);
     }
    }

    if (requiresRecalculate)
    {
     m_ChartControl.document.Calculate();
     m_ChartControl.document.RecalcLayout(eventArgs.Context);
    }
   }

We tested this approach and it was working OK. Let us know if you meet any problems.


By Kevin Harrison - 7 Years Ago
Thanks guys, that works. It really was a simple fix in the end.
Is there some documentation which gives an overview of this  context mechanism and how to use it?
Kind regards
Kevin
By Nevron Support - 7 Years Ago
Hi Kevin,
It is not documented but the general idea is that the context holds information about the current render surface (like render technology, size, resolution) as well as some means to paint on it - graphics, length conversion and other small classes. It in this particular case the size of the current view context differed than the size of the image export context and that messed up with the RecalcLayout.
By Kevin Harrison - 7 Years Ago
Hi guys

I have found an issue with the suggested code relating to 3D charts.
The chart area is blank; i.e. not rendered, if cartesianChart.Enable3D is set to true.
Cabn you suggest a solution pelase?

Thanks

Kevin
By Nevron Support - 7 Years Ago
Hi Kevin,
Indeed there is a problem in the 3D case - we just released a new SP of the control that fixes this issue. Also the code that recalculates the layout should be changed to:
m_ChartControl.document.RecalcLayout(m_ChartControl.document.CurrentRenderingContext);
Let us know if you meet any problems.
By Kevin Harrison - 7 Years Ago
Bugfinder strikes again! :-)

Thanks for the speedy response. Is the release you're referring to 17.6.16.12?
By Nevron Support - 7 Years Ago
Hi Kevin,
Yes that's the latest one - do you meet any problems?
By Kevin Harrison - 6 Years Ago
Hi guys
Sadly I  have found a problem with using
m_ChartControl.document.RecalcLayout(m_ChartControl.document.CurrentRenderingContext);
(using 18.4.26.12)
If this line is executed, then I find that none of the interactivity tools work
Interestingly, if I rotate the mouse wheel slightly then zoom starts working
If I have both DataZoom and DataPointDrag activated, then rotating the mouse wheel activates zoom, but not DataPointDrag.
However, if I perform a zoom, then data point drag starts working.

Can you shed some light on this please?
It is not easy for me to send you a sample project, so I hope you can reproduce this easily or suggest  a way for me to overcome this.

Thanks
Kevin
By Nevron Support - 6 Years Ago
Hi Kevin,
It's most likely something related to the selection, but its hard to tell like that. We tried to reproduce a problem but were unable to do so. However you can send us the state of the control after its broken to check it:
nChartControl1.Serializer.SaveControlStateToFile("c:\\temp\\chartstate.xml", Nevron.Serialization.PersistencyFormat.CustomXML, null);
Hopefully this will shed light as to why it fails to perform data point dragging after the layout.
By Kevin Harrison - 6 Years Ago
I've found what's happening and have a fix, though I'd like advice on whether it's something I'm doing or if there's a bug. My code boils down to:
public void OnBeforePaint(NPanel panel, NPanelPaintEventArgs eventArgs)
(
     DoStuff();
      if (requiresRecalculate)
            {
                this.chartControl.document.Calculate();
                 this.chartControl.document.RecalcLayout(this.chartControl.document.CurrentRenderingContext);
            }
 }

if requiresRecalculate is false then we're Ok and all data interactivity behaves as expected.

However, if requiresRecalculate is true, then when clicking on the rendered chart, OnBeforePaint is entered again, as though the chart hasn't finished rendering.
My use of the mouse wheel fixed it because DoStuff() makes requiresRecalculate false.
My solution is to add extra checking in DoStuff, so that once requiresRecalculate has fired once, then on the second entry DoStuff detects the action has occurred, so we are done and don't recalculate again.

Extra info:
I notice that OnBeforePaint is entered twice when displaying the chart.
The second time originates from the following code, specifically the bolded line. It seems there may be some unexpected interaction here? Should I achieve this in a different way or modify this code?

   /// <summary>Intercepts mouse move to allow a cursor change depending upon the object beneath</summary>
        /// <param name="sender">The sender.</param>
        /// <param name="e">The <see cref="NMouseEventArgs"/> instance containing the event data.</param>
        /// <remarks>Code idea supplied by Nevron</remarks>
        public override void OnMouseMove(object sender, NMouseEventArgs e)
        {
            if (e == null)
                return;

             NControlView view = (NControlView)this.GetView();

            NHitTestCacheService hitTestService = view.GetServiceOfType(typeof (NHitTestCacheService)) as NHitTestCacheService;
            if (hitTestService == null)
                return;

            NHitTestResult hitTestResult = new NHitTestResult(hitTestService.HitTest(new NPointF(e.X, e.Y)) as NChartNode);
            INMouseService mouseService = (INMouseService) view.GetServiceOfType(typeof (INMouseService));

            bool cursorSet = false;
            if (hitTestResult.ChartElement == ChartElement.DataPoint && hitTestResult.Chart != null)
            {
                CartesianChartDefinition cartesianChartDefinition = hitTestResult.Chart.Tag as CartesianChartDefinition;
                ChartSeries series = cartesianChartDefinition?.ChartSeriesSet.HavingId((int) hitTestResult.Series.Tag);
                if (series != null && series.AllowDragging)
                {
                    mouseService.Cursor = cartesianChartDefinition.ChartLayout == CartesianChartLayout.Vertical ? Cursors.SizeNS : Cursors.SizeWE;
                    cursorSet = true;
                }
            }

            if (!cursorSet)
                mouseService.Cursor = Cursors.Default;

        }

Many thanks

Kevin
 

By Nevron Support - 6 Years Ago
Hi Kevin,
It is normal to have a render pass on the chart when it detects that the image map has changed - internally it uses a special render to build an image map (raster or vector depending on the target graphics 3D or 2D). This image map links the chart elements to their visual representation. We can expose a property that will allow you to have the ability to check whether the current rendering is part of a selection render or display rendering pass. Will that solve the problem (that way you won't have to have a variable in DoStuff that checks whether it was called before that)?
By Kevin Harrison - 6 Years Ago
Thanks for the offer of the new variable, but now I understand it, I think I'll be fine.
I'm not actually adding a new variable in DoStuff.
DoStuff limits the size of the point symbol or line markers to a maximum, so I added a check for
       if (size != maxSize)
On the first pass this is true, so size is set to maxSize.
On the second pass, it isn't so requiresRecalculate is now false and we're done.

Many thanks as always

Kevin
By Nevron Support - 6 Years Ago

Hi Kevin,
Understood - we exposed the property anyway and it will be available in subsequent builds Smile Let us know if you meet any problems.