Requirements and pre-conditions:
- Chart series may contain different point types:
- First, let's call them 'automatic' - which may have been acquired by some logger process, in ordered, evenly spaced moments in time.
- Other type is 'manual', - meaning, that thesee may be acquired as a result of explicit user-to-device iteration during the same measurement session, as an automatic ones. These manual points usually have an arbitrary distributed x, and sometimes even y values.
- Chart series should draw automatic and manual points differently, while allowing these points to be iterated as a members of the same sequence.
Solution:
I derived my series class from TCustomSeries, overriding DrawAllValues smethod, and adding some properties to add distinct style to the manual points.
An extra event handler allows to plug-in function to differentiate between automatic and manual points in user-specific way.
Declaration:
class EsCustomSeries; // Custom series class - adaptive painting of series with a small number of pints + // way to distinguish 'manual' points and draw them separately // typedef bool __fastcall (__closure *onCheckManualPointMarkT)(const EsCustomSeries* sender, int valIdx, const UnicodeString& mark); class EsCustomSeries : public TCustomSeries { public: __fastcall EsCustomSeries(TComponent* owner); __property TSeriesPointerStyle manualPointerStyle = {read=m_manualPointerStyle,write=manualPointerStyleSet}; __property TAlphaColor manualPointerColor = {read=m_manualPointerColor,write=manualPointerColorSet}; __property onCheckManualPointMarkT onCheckManualPointMark = {read=m_onCheckManualPointMark,write=m_onCheckManualPointMark}; protected: bool isManualPointMark(int valIdx, const UnicodeString& txt) const; void __fastcall manualPointerStyleSet(TSeriesPointerStyle ps); void __fastcall manualPointerColorSet(TAlphaColor clr); virtual void __fastcall DrawAllValues() override; protected: onCheckManualPointMarkT m_onCheckManualPointMark; TSeriesPointerStyle m_manualPointerStyle; TAlphaColor m_manualPointerColor; };
The main job is done in custom DrawAllValues
__fastcall EsCustomSeries::EsCustomSeries(TComponent* owner) : TCustomSeries(owner), m_onCheckManualPointMark(nullptr), m_manualPointerStyle(TSeriesPointerStyle::psNothing), m_manualPointerColor(claNull) {} //--------------------------------------------------------------------------- void __fastcall EsCustomSeries::manualPointerStyleSet(TSeriesPointerStyle ps) { if( m_manualPointerStyle != ps ) { m_manualPointerStyle = ps; Repaint(); } } //--------------------------------------------------------------------------- void __fastcall EsCustomSeries::manualPointerColorSet(TAlphaColor clr) { if( m_manualPointerColor != clr ) { m_manualPointerColor = clr; Repaint(); } } //--------------------------------------------------------------------------- bool EsCustomSeries::isManualPointMark(int valIdx, const UnicodeString& txt) const { if( m_onCheckManualPointMark ) return m_onCheckManualPointMark(this, valIdx, txt); return false; } //--------------------------------------------------------------------------- void __fastcall EsCustomSeries::DrawAllValues() { TCanvas3D* canvas = ParentChart->Canvas; if( !canvas ) return; std::unique_ptr<TTeeBlend> tmpBlend; if( Transparency > 0) tmpBlend.reset( canvas->BeginBlending( TeeZeroRect, Transparency ) ); int tmpFirst = FirstDisplayedIndex(); if( tmpFirst > 0 ) --tmpFirst; bool tmpFraction = false; double tmpFractionValue = 0; int tmpLast; int cnt; if( DisplayLast == 1 ) tmpLast = LastDisplayedIndex(); else { cnt = Count(); tmpLast = std::min( cnt-1, static_cast<int>(DisplayLast*cnt) ); if( tmpLast < tmpFirst ) tmpLast = tmpFirst; else if( tmpLast < cnt-1 ) tmpFractionValue = (DisplayLast*cnt) - static_cast<int>(DisplayLast*cnt); tmpFraction = tmpFractionValue > 0; } cnt = tmpLast-tmpFirst+1; std::vector<TPointF> linePoints; std::vector<int> lineIdxs; std::vector<TPointF> markPoints; linePoints.reserve(cnt); lineIdxs.reserve(cnt + (tmpFraction ? 1 : 0) ); markPoints.reserve( std::max( 1, cnt/2 ) ); if( tmpFraction ) ++tmpLast; for(int idx = tmpFirst; idx <= tmpLast; ++idx) { const TPointF& point = { static_cast<float>(CalcXPos(idx)), static_cast<float>(CalcYPos(idx)) }; const UnicodeString& markTxt = ValueMarkText[idx]; if( isManualPointMark(idx, markTxt) ) { markPoints.push_back( point ); continue; } linePoints.push_back( point ); lineIdxs.push_back( idx ); } if( tmpFraction ) --tmpLast; try { canvas->AssignVisiblePenColor( LinePen, LinePen->Color ); if( linePoints.size() > 2 ) { canvas->Polyline( &linePoints[0], linePoints.size()-1 ); } else if( Pointer->Visible && Pointer->Style != TSeriesPointerStyle::psNothing ) { for( size_t idx = 0; idx < linePoints.size(); ++idx ) { const TPointF& point = linePoints[idx]; Pointer->DrawPointer( canvas, false, point.X, point.Y, Pointer->VertSize, Pointer->HorizSize, ValueColor[ lineIdxs[idx] ], Pointer->Style ); } } if( Pointer->Visible && manualPointerStyle != TSeriesPointerStyle::psNothing && manualPointerColor != claNull && !markPoints.empty() ) { canvas->AssignVisiblePenColor( LinePen, manualPointerColor ); for( auto const& point : markPoints ) { Pointer->DrawPointer( canvas, false, point.X, point.Y, Pointer->VertSize, Pointer->HorizSize, manualPointerColor, manualPointerStyle ); } } } __finally { if( tmpBlend ) canvas->EndBlending( tmpBlend.get() ); } } //---------------------------------------------------------------------------
The resulting code draws automatic points as Pointers, if there are a few points (< 3) in automatic sequence.
The manual points are drawn separately from the automatic sequence, as pointer of specifc style and color.
The user code defines and plug-in handler to sort-off automatic and manual points from the series being drawn.
A small example code is below:
Create and add custom series to chart:
m_ser0 = new EsCustomSeries(chart_); m_ser0->LinePen->Color = claNavy; m_ser0->LinePen->Width = 2; m_ser0->Pointer->Visible = true; m_ser0->Pointer->Size = 10; m_ser0->Pointer->Style = TSeriesPointerStyle::psCircle; m_ser0->manualPointerStyle = TSeriesPointerStyle::psDiamond; m_ser0->manualPointerColor = claRed; m_ser0->onCheckManualPointMark = onCheckManualPointMark; m_ser0->ParentChart = chart_;
Fill series with random values, specifying some of them as manual ones using ValueMarkText series property
m_ser0->BeginUpdate(); m_ser0->FillSampleValues( ed_->Value ); // Fake manual points int cnt = m_ser0->Count(); int div = cnt/3; if( div < 1 ) div = 1; for(int idx = 0; idx < cnt-1; ++idx) { if( idx % ((idx % div) ? (idx % div) : 4) == 3 ) m_ser0->ValueMarkText[idx] = "manual"; } m_ser0->EndUpdate();
Implement the simplest manual point identification handler
bool __fastcall TForm1::onCheckManualPointMark(const EsCustomSeries* sender, int valIdx, const UnicodeString& mark) { return "manual" == mark; } //---------------------------------------------------------------------------
The result is as follows: