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 interaction 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;
}
//---------------------------------------------------------------------------
{multithumb}The result is as follows: