ASE Home Page Products Download Purchase Support About ASE
ChartDirector Support
Forum HomeForum Home   SearchSearch

Message ListMessage List     Post MessagePost Message

  RealTimeViewPort only updates data in full plot chunks after the first plot
Posted by Rand on Dec-08-2020 07:57
Mult-iplot real time chart for test sequencing application.  Data comes in point by point and data sets are, within a few points, the same size per test type.

Code was originally RealTimeViewPort example, modified for XY plotting rather than bar charts.

The first full data set comes in fine, looks good while being plotted point by point, can zoom, scroll, etc.  Then there is a long pause with no data updates while more data comes in (not being plotted point by point anymore) then the whole plot data set appears at once on the chart.  Subsequent data sets appear in the same fashion.  The chart is interactive for the whole time (not frozen).

I use LineLayer::setXData() and addData()
All plots are on the same line layer (I tried multiple layers with setXData() only on first, no difference).
axis_x->setAutoScale( 0.0, 0.01, 0.0 );
axis_y->setAutoScale( 0.01, 0.00, 0.0 );
p_mgr->syncLinearAxisWithViewPort( "x", axis_x );

OnTimerChartUpdate stores incoming data points and does some chart config
vp_manager->setFullRange( "x", prop_data_x.front(), prop_data_x.back() );
and mouse zoom ratio setting.

Additionally, I never see any plot lines in the ViewPortControl though occasionally some part of a plot will be visible when zooming.  The viewer and control interact correctly as far as I can tell (I can zoom/scroll using the control and mouse zooming the viewer changes the vp size in the control, scaling appears correct for these operations).

Do you have any suggestions?  I can post source if needed.

  Re: RealTimeViewPort only updates data in full plot chunks after the first plot
Posted by Peter Kwan on Dec-08-2020 15:29
Hi Rand,

From your previous post, I think you are using wxWidgets.

The RealTimeViewPort is a multi-threaded example. The data are generated in a background thread and stored in a thread-safe queue. The chart is updated in the GUI thread by a timer.

When the timer ticks, OnChartUpdateTimer is called and several things occur:

- All the data pending in the thread-safe queue are copied to the data arrays.

- The "full x range" may change because there are new data.

- The visible x range may or may not change. Our sample code is design such that if the user is viewing the latest data, the chart will scroll with the new data. If the user is viewing older data, the chart will not scroll so that user will see a stable chart.

- The chart will update if it is scrolled or there are new visible data.

Note that if the data arrays are full, 5% of the oldest data will be removed to leave space for the new data. If the chart is displaying the removed data, it will scroll forward to skip the removed data.

In our sample code, the setAutoScale is not used for the x-axis. It is because with zooming and scrolling, the axis scale is controlled by the user, not by ChartDirector.

To troubleshoot the problem, I think we can try to trace why the chart is not updated with new data. I am not familiar with wxWidgets, but I think there should be some ways to log messages to the screen. In MFC/Qt, I use TRACE and qDebug. I read from online documentation that there seems to be a wxLogDebug function.

To trace the chart update, you may log messages in OnTimerChartUpdate. The last line of  OnTimerChartUpdate determines if the chart will be updated:

// Trigger the viewPortChanged event. Updates the chart if the axis scale has changed
// (scrolling or zooming) or if new data are added to the existing axis scale.
viewer->updateViewPort(axisScaleHasChanged || (duration < initialFullRange), false);

Some reasons why the chart is not updated are because the OnTimerChartUpdate is not run at all (the timer are not set correctly), or axisScaleHasChanged || (duration < initialFullRange) is not true. In the sample code, the OnTimerChartUpdate should run every 0.1 second. If the OnTimerChartUpdate does occur very rapidly, but the chart is not updated, you can check if axisScaleHasChanged is true or not, and trace the code to see why it is not set to true.

If the chart is updated, but nothing changes (not even the axis scale change), we may need to trace the charting part of the code to see the actual x data range it uses to obtain the viewPortTimeStamps, and why it uses that range.

Regards
Peter Kwan

  Re: RealTimeViewPort only updates data in full plot chunks after the first plot
Posted by Rand on Dec-10-2020 20:46
Attachments:
Peter,
Thank you for the information.
I was able to get most of what I need working.  A note about wxWidgets.  wxWidgets can be considered the same as MFC for our discussions.  It wraps the native GUI objects.  It was originally an MFC wrapper (on Windows) with some custom controls for the missing components that were available on other systems, such as Linux.

I still have the following issues to solve (see chart image at end of post):
Data point label appears to be blending the selected label background (chart background color) with the label text color.
My intent is to have a label with the chart background color, data color text, and a bright green border the same as the data point indicator circle.  How do I accomplish that?

The second problem I am having is that nothing appears in the ViewPortControl (but it interacts with ViewPort zooming).  Code follows:
I put the initialization code for ViewPort and ViewPortControl last in case those might be interesting.
I have also linked the control and manager with setViewPortControl(control) and setViewer(manager)
The process kicks off with OnTimerChartUpdate()'s final line updataViewPort(true, false):
Execution Trace:
OnTimerChartUpdate(wxTimerEvent &)
OnViewPortChanged(wxCommandEvent &)
DoDrawViewPort(void)
DoVpMgrSetChart(XYChart *)
DoDrawViewPortControl(void)
DoVpCtrlSetChart(XYChart *)
OnTimerChartUpdate(wxTimerEvent &)
...

void HLIRealTimeViewPort::DoDrawViewPortControl()
{
    auto vp_ctrl { m_properties.vp_control };
    XYChart* chart{nullptr};
    if( vp_ctrl )
    {
        auto vp_mgr { vp_ctrl->getViewer() };
        if( vp_mgr )
        {
            const auto& prop_data_x { m_properties.data_x_map[m_properties.plot_idx_x_select] };
            const auto& prop_data_y{ m_properties.data_y_map };
            if( !prop_data_x.empty() )
            {
                chart = DoVpCtrlSetupChart();
                if( chart )
                {
                    auto* plot_area { DoVpCtrlConfigPlotArea( chart ) };
                    auto* axis_x { DoVpCtrlConfigAxisX( chart ) };
                    auto axis_y { DoVpCtrlConfigAxisY( chart ) };
                    auto* line_layer { DoVpCtrlConfigLineLayer( chart ) };
                    if( plot_area && axis_x && axis_y && line_layer )
                    {
                        line_layer->setXData(prop_data_x.Array());
                        for( const auto& [plot_idx, data_vec] : prop_data_y )
                        {
                            auto color{ map_graph_plot_index_to_plot_color.at( plot_idx )};
                            auto name{ map_graph_plot_index_to_plot_label_suffix.at( plot_idx ).c_str() };
                            line_layer->addDataSet( data_vec.Array(), color, name  )->setDataSymbol(Chart::GlassSphere2Shape);
                        }
                        axis_x->setLinearScale( prop_data_x.front(), prop_data_x.back() );
                    }// if( plot_area && axis_x && axis_y && line_layer )
                    else
                    {
                        // _TODO Log fail
                        delete chart;
                        chart = nullptr;
                    }
                }// if( chart )
                else
                {
                    // _TODO Log fail
                }
            }
        }// if( vp_mgr )
        DoVpCtrlSetChart( chart );
    }
}
void HLIRealTimeViewPort::DoVpCtrlSetChart( XYChart* chart )
{
    auto* vp_ctrl{m_properties.vp_control};
    if( vp_ctrl )
    {
        delete vp_ctrl->getChart();
        vp_ctrl->setChart( chart );
    }
}
XYChart* HLIRealTimeViewPort::DoVpCtrlSetupChart()
{
    XYChart* chart{nullptr};
    auto* vp_ctrl{m_properties.vp_control};
    if( vp_ctrl )
    {
        auto width = m_properties.vp_control_chart_size.GetWidth();
        auto height = m_properties.vp_control_chart_size.GetHeight();
        chart = new XYChart( width, height, COLOR_XYGRAPH_BACKGROUND, COLOR_XYGRAPH_BORDER );
        if( chart )
        {
            chart->setDefaultFonts( FONT_NAME_GRAPH_TEXT, FONT_NAME_GRAPH_TEXT, FONT_NAME_GRAPH_TEXT, FONT_NAME_GRAPH_TEXT );
            //chart->addTitle( "VP Ctrl Title", FONT_NAME_GRAPH_TEXT, SIZE_FONT_GRAPH_TITLE_POINT, COLOR_XYGRAPH_TEXT );
            chart->setClipping();
        }
        else
        {
            // _TODO Log fail
        }
    }
    else
    {
        // _TODO Log fail
    }
    return chart;
}
Axis* HLIRealTimeViewPort::DoVpCtrlConfigAxisX( XYChart* chart )
{
    if( chart )
    {
        auto* axis_x{chart->xAxis()};
        if( axis_x )
        {
            axis_x->setColors( COLOR_XYGRAPH_AXIS, COLOR_XYGRAPH_TEXT, COLOR_XYGRAPH_TEXT, COLOR_XYGRAPH_GRID_LINE );
            axis_x->setLabelStyle( FONT_NAME_GRAPH_TEXT, SIZE_FONT_GRAPH_AXIS_LABEL_POINT );
            axis_x->setTickLength( -1 );
            axis_x->setTickDensity( 75 );
            // Put the x-axis labels inside the plot area
            axis_x->setLabelGap( -1 );
            axis_x->setLabelAlignment( 1 );
            // TODO Add type to select label format
            axis_x->setLabelFormat( "{value|P1}" );
            axis_x->setRounding(false, false);
            return axis_x;
        }
    }
    // _TODO Log fail
    return nullptr;
}
Axis* HLIRealTimeViewPort::DoVpCtrlConfigAxisY( XYChart* chart )
{
    // Set the y axis stem and labels to transparent (that is, hide the labels)
    if( chart )
    {
        auto* axis_y { chart->yAxis() };
        if( axis_y )
        {
            axis_y->setColors( Chart::Transparent, Chart::Transparent );
            axis_y->setLabelFormat( "{value|P2}" );
            axis_y->setRounding( false, false );
            return axis_y;
        }
    }
    // _TODO Log fail
    return nullptr;
}
PlotArea* HLIRealTimeViewPort::DoVpCtrlConfigPlotArea( XYChart* chart )
{
    if( chart )
    {
        auto* plot_area { chart->setPlotArea(
            1, 1, chart->getWidth() - 3, chart->getHeight() - 3,
            COLOR_XYGRAPH_BACKGROUND, -1, Chart::Transparent,
            COLOR_XYGRAPH_GRID_LINE, COLOR_XYGRAPH_GRID_LINE ) };
        return plot_area;
    }
    // _TODO Log fail
    return nullptr;
}
LineLayer* HLIRealTimeViewPort::DoVpCtrlConfigLineLayer( XYChart* chart )
{
    if( chart )
    {
        auto* line_layer { chart->addLineLayer() };
        if( line_layer )
        {
            line_layer->setFastLineMode();
            line_layer->setLineWidth( 1 );
            // The data sets are combined by drawing them independently, overlapping each others.
            line_layer->setDataCombineMethod(Chart::Overlay);
            return line_layer;
        }
    }
    // _TODO Log fail
    return nullptr;
}
wxChartViewer* HLIRealTimeViewPort::DoCreateVpMgr()
{
    wxChartViewer* vp_mgr{nullptr};
    auto size { GetSize() };
    auto style_vp { wxTAB_TRAVERSAL | wxNO_BORDER };
    auto vp_mgr_size { wxSize( size.GetWidth() - 15, size.GetHeight() * 0.8 ) };
    vp_mgr = new wxChartViewer( this, ID_VIEW_PORT_MGR, wxDefaultPosition, vp_mgr_size, style_vp );
    if( vp_mgr )
    {
        vp_mgr->SetBackgroundColour( GetBackgroundColour() );
        vp_mgr->setMouseWheelZoomRatio( 1.1 );
        vp_mgr->setMouseUsage( Chart::MouseUsageDefault );
        m_properties.vp_manager_chart_size = wxSize(vp_mgr->GetSize());
        m_properties.vp_manager_chart_size.DecBy(wxPoint{3,3});
    }
    else
    {
        m_log->critical("{} The ViewPortManager* was nullptr");
    }
    return vp_mgr;
}
wxViewPortControl* HLIRealTimeViewPort::DoCreateVpCtrl()
{
    wxViewPortControl* vp_ctrl{nullptr};
    auto size { GetSize() };
    auto style_vp { wxTAB_TRAVERSAL | wxNO_BORDER };
    auto vp_ctrl_size { wxSize( size.GetWidth() - 15, size.GetHeight() * 0.1 ) };
    vp_ctrl = new wxViewPortControl( this, ID_VIEW_PORT_CTRL, wxDefaultPosition, vp_ctrl_size, style_vp );
    if( vp_ctrl )
    {
        vp_ctrl->setViewPortExternalColor( 0x1A1A1A );
        vp_ctrl->setViewPortFillColor( COLOR_XYGRAPH_BACKGROUND );
        vp_ctrl->setViewPortBorderColor( 0x3399FF );
        vp_ctrl->setViewPortBorderWidth( 1 );
        vp_ctrl->setSelectionBorderColor( 0xFF0000 ); // Which is this?
        //vp_ctrl->setSelectionBorderWidth( 1 );
        vp_ctrl->setMouseMargin( 10, 10 );
        vp_ctrl->setDragInsideToMove( true );
        vp_ctrl->setDragBorderToResize( true );
        vp_ctrl->setDragOutsideToSelect( true );
        vp_ctrl->setClickToCenter( true );
        m_properties.vp_control_chart_size = wxSize( vp_ctrl->GetSize() );
        m_properties.vp_control_chart_size.DecBy( wxPoint { 3, 3 } );
    }
    else
    {
        m_log->critical( "{} The ViewPortControl* was nullptr" );
    }
    return vp_ctrl;
}
rtvp1.png

  Re: RealTimeViewPort only updates data in full plot chunks after the first plot
Posted by Peter Kwan on Dec-10-2020 23:57
Attachments:
Hi Rand,

Your chart background color is 0x121212. I found that you label background color is a fixed color 0x01184274. (See image at the end of this message.) The 0x121212 happens to be equal to 1184274 in decimal.

When you draw the label, you may have use the chart background color to generate the CDML text. To do so, your code must format the number in hexademical. In our own sample code below (from the "Realtime Chart with Zoom/Scroill" sample code), we use "hex" to make sure the color is formatted as hexdecimal.

ostringstream label;
label << "<*font,bgColor=" << hex << color << "*> " << .... some othe content ...;

Without the "hex", the 0x121212 will become 1184274. ChartDirector will interpret any color it receives to be hexadecimal, that why the background becomes 0x01184274.

For the ViewPortControl, I cannot find anything abnormal with the code. To troubleshoot the problem, I suggest to replace the entire "if (vp_ctrl) ...." block with:

if( vp_ctrl )
{
   double dataX[] = {0, 1, 2, 3, 4};
   double dataY[] = {10, 20, 30, 20, time(0) % 60};
   const int size = (int)(sizeof(dataX)/sizeof(*dataX));

   XYChart* c = new XYChart(250, 250);
   c->setPlotArea(30, 20, 200, 200);
   LineLayer* layer = c->addLineLayer(DoubleArray(dataY, size));
   layer->setXData(DoubleArray(dataX, size));
   c->makeChart("simpleline.png");

   delete vp_ctrl->getChart();
   vp_ctrl->setChart( chart );
}

In this way, we can confirm if the code in inside the if (vp_ctrl) has actually been executed, and whether vp_ctrl can display anything. (Sometimes a control can be created in a disabled state or something similar.) The above code also saves the chart as an image "simpleline.png", so you can confirm if the chart is actually created. The "time(0) % 60" ensures subsequent charts will be different, so you can see if the chart is being updated.

If the above works, you can slowly change it to your own code. For example, you can change setChart with your DoVpCtrlSetChart, and change "new XYChart/setPlotArea" to your DoVpCtrlSetupChart. Finally, you can try to use your real data to see if it works. Hopefully, this can identify the cause of the problem.

Regards
Peter Kwan
labels.png

  Re: RealTimeViewPort only updates data in full plot chunks after the first plot
Posted by Rand on Dec-15-2020 08:16
Good eye on the missing hex tag in the format string, I had forgotten the format of the tag and also forgot to drop a // TODO :(

I removed the ViewPortControl in favor of a scrollbar to save vertical space because we are now going to also have tables under the graph.  I should be more careful what I tell management :)

Kudos on the library.  Once I got past my lack of drawing experience it was fairly easy to get the chart up and running.  I am handling the autoscaling in custom code because we want to compress the Y to the minimum needed by the data.  I found through experimentation that major tick density seems to be the key to keeping a tight auto scale.  Is that the best way to do this type of scaling?

Coming from a backend developer background I found that my thinking about placement got much smoother once I internalized Quadrant IV as an array of pixels.  My thought process was stuck in Quadrant I for awhile.

I have another question about custom HTML image maps, I will start another thread for that.

Thanks again, and nice job on the toolkit.  My only complaint is the docs seem to be out of date for version 6.3.  I can point out some areas if you would like details.

  Re: RealTimeViewPort only updates data in full plot chunks after the first plot
Posted by Peter Kwan on Dec-15-2020 13:32
Hi Rand,

For auto-scaling, you may use Axis.setAutoScale, Axis.setRounding and Axis.setTickDensity to further compress the axis scale.

Let me use a simple example. Suppose your data are from 2.847 to 9.87.

(a) The minimum possible y-axis scale is 1.347 to 9.87 to match the data.

(b) By default, ChartDirector will ensure the axis scale to start and end at a labelled position. ChartDirector will use nice numbers (like 2, 4, 6, ..., but not 1.37, 2.74, 4.11) as labels. One possibility for the labels can be 0, 2, 4, 6, 8, 10. So the axis range is 0 - 10. If the axis label and tick density allows more labels on the axis, ChartDirector may choose 1, 2, 3, .... 10 as the labels, which is more compressed than 0 to 10.

(c) Some people may prefer the chart content not to get too close to the top and bottom plot area boundary. By default, ChartDirector will extend the scale if necessary to ensure the top and bottom 10% of the scale has no data, with the restriction that the scale will not extend pass the zero point. In the above example, the 9.87 is too close to 10, so ChartDirector may choose 0, 2, 4, ....12 as the axis scale.

The above explains why setTickDensity can affect the axis scale.

Axis.setRounding can be used to disable (a) extending the axis end points to labelled points.

Axis.setAutoScale can be used to configure (c) the 10% margin at the end points of the
axis and also another parameter called zero affinity (that ChartDirector gives 0 some preference when choose the labels).

The minimum auto-scale can be achieved with:

// no margin, no zero affinity, no extending to labelled position
c->yAxis()->setAutoScale(0, 0, 0);
c->yAxis()->setRounding(false, false);

For the documentation error, please do let me know what are the errors so we can fix it.

Regards
Peter Kwan

  Re: RealTimeViewPort only updates data in full plot chunks after the first plot
Posted by Peter Kwan on Dec-16-2020 10:57
Hi Rand,

I just notice I made a mistake in the above example. The data range are supposed to be  1.347 to 9.87, not 2.847 to 9.87.

Regards
Peter Kwan