Page Index Toggle Pages: 1 Send TopicPrint
Normal Topic Discrete custom rendering on a timeseries (Read 2904 times)
hyper
YaBB Newbies
*
Offline


I Love MindFusion!

Posts: 14
Joined: Apr 8th, 2021
Discrete custom rendering on a timeseries
Sep 9th, 2021 at 8:14pm
Print Post  
Hi,
Please if you can help me, so far I've got more than a dozen time series traces, all working perfectly.  They render as connected successive lines, with various colors, thicknesses, transparencies, etc.

However, I now have a need for custom rendering of items on the same timeseries, which 1) do not have a line connecting successive elements, 2) whose elements render on the Y-Axis position, as usual, but these elements need to hold an additional  double float data value, and then each element needs to custom render itself  based on that value.

For example, maybe it renders a Square, with Solid color RED if its extra data value is negative, and Solid color GREEN when positive; and the Size of the square will be proportional to the absolute value of this extra data element.  The rendering is at the usual time series value on the Y-Axis, but it is custom.

This data may be "sparse" so nothing should be rendered when there is no data within the timeseries window.

I am not sure how to begin to do that; since I believe I'll have to write a custom renderer at least.

Can you point me in the right direction how to do this on a timeseries chart, a line chart, which I'm using for everything.
  
Back to top
 
IP Logged
 
Slavcho
God Member
*****
Offline


tech.support

Posts: 3147
Joined: Oct 19th, 2005
Re: Discrete custom rendering on a timeseries
Reply #1 - Sep 10th, 2021 at 7:56am
Print Post  
Hi,

The base series renderer class provides various EnumVisible* methods that let you enumerate points or segments within the plot's viewport. For drawing simple figures around data points, call EnumVisiblePoints:

Code
Select All
class MyRenderer : Renderer2D
{
    public MyRenderer() :
        base(new ObservableCollection<Series>())
    {
    }

    protected override void Draw(RenderContext context)
    {
        EnumVisiblePoints(context, (s, i, point) =>
        {
            var d = (float)Series[s].GetValue(i, 2) * 2;
            var color = d < 7 ? Color.Red : Color.Green;
            var rect = new System.Drawing.RectangleF(
                point.X - d / 2, point.Y - d / 2, d, d);

            using (var brush = new System.Drawing.SolidBrush(color))
                context.Graphics.FillRectangle(brush, rect);
        });
    }
}

var renderer = new MyRenderer();
renderer.Series.Add(new Series3D(
        new List<double> { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 },
        new List<double> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 },
        new List<double> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12 },
        null
    ));
lineChart.Plot.SeriesRenderers.Add(renderer);
 



Regards,
Slavcho
Mindfusion
  
Back to top
 
IP Logged
 
hyper
YaBB Newbies
*
Offline


I Love MindFusion!

Posts: 14
Joined: Apr 8th, 2021
Re: Discrete custom rendering on a timeseries
Reply #2 - Sep 20th, 2021 at 3:01am
Print Post  
Thank you very much for that elegant piece of code; however, I can't make it work.  I am 99.5% there; but something fundamental is lacking in my understanding of Lambdas, assuming your code actually runs; which I'm sure it does...

Could you simply tell me, within Draw, given the RenderContext, what is the Visibility Hit Test for coordinates X and Y ?  How is the visibility test done? and I will code the rest, most likely without using the EnumVisiblePoints and Lambdas...  sorry but I've spent at least 10 hours, and I need to fall back to coding it without those support functions.  SORRY.

hyper
  
Back to top
 
IP Logged
 
Slavcho
God Member
*****
Offline


tech.support

Posts: 3147
Joined: Oct 19th, 2005
Re: Discrete custom rendering on a timeseries
Reply #3 - Sep 20th, 2021 at 6:42am
Print Post  
Hi,

You can find running code in test project here -
https://mindfusion.eu/_samples/chart_winforms_renderer.zip

Are you trying to make this work in older .NET without lambdas? If you prefer to enumerate the points yourself anyway, you could call this protected seriesRenderer.GetPixel() method to convert data point to pixel for you, where you pass chart's Plot as 'component' argument -

Code
Select All
protected PointF GetPixel(
	double valueX, Axis xAxis,
	double valueY, Axis yAxis,
	Components.Component component)
 



Before converting to pixel and drawing, you can call this method of X/Y Axis objects to determine if series value is within visible range -

Code
Select All
public bool InRange(double value)
{
	return EffectiveMinValue <= value && value <= EffectiveMaxValue;
} 



If you have a lot of points in the series and implement the Series interface to advertise them as sorted (IsSorted method), EnumVisiblePoints will do a binary search to determine the visible range and iterate only over points inside that range. If it doesn't know if points are sorted, it loops over all of them, calls axis.InRange to check visibility, and if point is visible it converts to pixel and invokes the callback lambda / delegate.

Regards,
Slavcho
  
Back to top
 
IP Logged
 
hyper
YaBB Newbies
*
Offline


I Love MindFusion!

Posts: 14
Joined: Apr 8th, 2021
Re: Discrete custom rendering on a timeseries
Reply #4 - Sep 20th, 2021 at 1:17pm
Print Post  
Thank you very much!

No, I'm using the latest and greatest C# language spec,
and am a highly competent coder, limited use of Lambdas,
mostly for dispatching Task calls onto GUI threads, etc.

This info will enable me to make it work !

Thanks so much for your quick support!

hyper
  
Back to top
 
IP Logged
 
hyper
YaBB Newbies
*
Offline


I Love MindFusion!

Posts: 14
Joined: Apr 8th, 2021
Re: Discrete custom rendering on a timeseries
Reply #5 - Sep 20th, 2021 at 1:52pm
Print Post  

On opening the project, I had these errors, so the Designer
was hesitating to open...  versioning maybe?

Severity      Code      Description      Project      File      Line      Suppression State

Error      CS1061      'LineChart' does not contain a definition for 'NearestElement' and no accessible extension method 'NearestElement' accepting a first argument of type 'LineChart' could be found (are you missing a using directive or an assembly reference?)      LineChart      C:\vcrepos\chart_winforms_renderer\LineChart\MainForm.cs      275      Active

Error      CS1061      'LineChart' does not contain a definition for 'YLabelAlignment' and no accessible extension method 'YLabelAlignment' accepting a first argument of type 'LineChart' could be found (are you missing a using directive or an assembly reference?)      LineChart      C:\vcrepos\chart_winforms_renderer\LineChart\MainForm.Designer.cs      78      Active

In the meantime I can study the raw source code.
  
Back to top
 
IP Logged
 
Slavcho
God Member
*****
Offline


tech.support

Posts: 3147
Joined: Oct 19th, 2005
Re: Discrete custom rendering on a timeseries
Reply #6 - Sep 20th, 2021 at 2:55pm
Print Post  
Right, NearestElement is from 4.1.3 so you are probably on older version. Anyway that's from testing out other things and not concerning the custom renderer, so you should be able to comment out missing members and try it.

Regards,
Slavcho
  
Back to top
 
IP Logged
 
hyper
YaBB Newbies
*
Offline


I Love MindFusion!

Posts: 14
Joined: Apr 8th, 2021
Re: Discrete custom rendering on a timeseries
Reply #7 - Sep 20th, 2021 at 9:01pm
Print Post  
OK, I am making good progress, but have a problem with the X axis.

These are realtime lineCharts, and on all my other dozen or so default series (2D), mapped onto the same Plot, I am using a long value for the x axis, which is derived, I think from:

DateTime now = DateTime.UtcNow;
long ticks = now.Ticks; // the x axis timestamp

This works great for all of my default rendered series. The xAxis timebase is perfect.

However, now with the new renderer I believe I have to use a double for the xAxis timebase; and that is clearly not working.

List<double> doubleDateTimestampList; // was long, now required to be double for 3D ? So Convert.ToDouble(long) ?

So I need to understand how to map the data onto the xAxis with the new renderer; given that I don't think I can use the long
data type any more...?

Maybe this is all confused, but I assumed that converting the long into a double would serve the purpose of representing the xAxis timebase. Should that work?

Maybe you could help me understand that issue, even though I'm not explaining it very well...? In my defense, this is a huge realtime order entry and analysis system which extends beyond what I could chart with NinjaTrader, and so I've adopted a couple of MindFusion charts in order to be able to do the charting complexity, so I'm just barely proficient with it.... Mea culpa.


[EDIT] all of my Realtime data is buffered, of course, and then inserted into the MindFusion chart only on the GUI thread, and Invalidated/refreshed only a couple of times per second at a controlled rate...

[EDIT2] This Draw is invoked as expected, and I think it does the testing whether the data is in the visible region, perhaps the general logic isn't right... Usually I don't use "var" but use the specific type; but I'm sure that's fine...?

Code
Select All
            protected override void Draw(RenderContext context) {
                // verified if (DRAW_PRINT) parentForm.getStrategy().ninjaPrint2(" ...calling Draw");
                if (_seriesIndex<0) {
                    parentForm.getStrategy().ninjaPrint2(" Draw seriesIndex not provided ! ");
                    return;
                }
                if (_myPlot == null) {
                    parentForm.getStrategy().ninjaPrint2(" Draw Plot reference not provided ! ");
                    return;
                }
                Axis xAxis = context.GetXAxis();
                Axis yAxis = context.GetYAxis();
                // we can determine whether a data point falls within the visible Axes
                // _myPlot is the "component" we will use for the visibility testing
                for (int i=0; i<mySeries.yValuesList.Count; i++) { // all are same length
                    double x = mySeries.GetValue(i, DATE_DIMENSION); // x axis realtime timestamp
                    double y = mySeries.GetValue(i, YVALUE_DIMENSION); // y axis "price'
                    // now test whether the data values are within the visible Axes intervals
                    if ( !xAxis.InRange(x)) continue;
                    if ( !yAxis.InRange(y)) continue;
                    parentForm.getStrategy().ninjaPrint2(" Draw x: "+x+" y: "+y+" drawn");
                    // it is within the visible plot area; so now convert the data to a pixel
                    PointF myPoint = GetPixel(
                        x, xAxis,
                        y, yAxis,
                        _myPlot);
                    var d = (float)(mySeries.GetValue(i, DATA3D_DIMENSION) * 6); // the 3rd dimension size/color square
                    var color = d < 0 ? Color.Red : Color.Green;
                    var rect = new System.Drawing.RectangleF(
                        myPoint.X - d / 2, myPoint.Y - d / 2, d, d);

                    using (Brush brush = new System.Drawing.SolidBrush(color))
                        context.Graphics.FillRectangle(brush, rect);
                }
            } // end Draw

 



hyper
  
Back to top
 
IP Logged
 
hyper
YaBB Newbies
*
Offline


I Love MindFusion!

Posts: 14
Joined: Apr 8th, 2021
Re: Discrete custom rendering on a timeseries
Reply #8 - Sep 20th, 2021 at 9:52pm
Print Post  
I think I am failing to convert the long representing ticks from DateTime.UtcNow.Ticks properly into a huge positive double precision value properly.  Maybe my long type should use the ulong type (unsigned); before conversion to the double?...  I think that's a part of my problem...

hyper
  
Back to top
 
IP Logged
 
hyper
YaBB Newbies
*
Offline


I Love MindFusion!

Posts: 14
Joined: Apr 8th, 2021
Re: Discrete custom rendering on a timeseries
Reply #9 - Sep 21st, 2021 at 1:11am
Print Post  
OK, THANKS FOR THE HELP.

I've figured it all out; and had forgotten about the proportionality calculations which needed to be done on the X Axis value which is returned by the Series.GetValue(index, dimension) and now things are working well.

Thanks for the good examples, and the quick help !!  It's always hard when you don't do this graphics stuff for a long time; and then have to return to it... Smiley

hyper


  
Back to top
 
IP Logged
 
Page Index Toggle Pages: 1
Send TopicPrint