9 #define _USE_MATH_DEFINES
10 #include <math.h> // for M_PI
16 static char THIS_FILE
[] = __FILE__
;
20 /////////////////////////////////////////////////////////////////////////////
21 // This macro can be called at the beginning and ending of every
22 // method. It is identical to saying "ASSERT_VALID(); ASSERT_KINDOF();"
23 // but is written like this so that VALIDATE can be a macro. It is useful
24 // as an "early warning" that something has gone wrong with "this" object.
27 #define VALIDATE ::AfxAssertValidObject(this, __FILE__ , __LINE__ ); \
28 _ASSERTE(IsKindOf(GetRuntimeClass()));
35 /////////////////////////////////////////////////////////////////////////////
38 #define TICK_PIXELS 4 // Size of tick marks.
39 #define GAP_PIXELS 6 // Better if an even value.
40 #define LEGEND_COLOR_BAR_WIDTH_PIXELS 50 // Width of color bar.
41 #define LEGEND_COLOR_BAR_GAP_PIXELS 1 // Space between color bars.
42 #define Y_AXIS_TICK_COUNT_TARGET 5 // How many ticks should be there on the y axis.
43 #define MIN_FONT_SIZE 70 // The minimum font-size in pt*10.
44 #define LEGEND_VISIBILITY_THRESHOLD 300 // The width of the graph in pixels when the legend gets hidden.
46 #define INTERSERIES_PERCENT_USED 0.85 // How much of the graph is
47 // used for bars/pies (the
48 // rest is for inter-series
51 #define TITLE_DIVISOR 5 // Scale font to graph width.
52 #define LEGEND_DIVISOR 8 // Scale font to graph height.
53 #define Y_AXIS_LABEL_DIVISOR 6 // Scale font to graph height.
56 const double M_PI
= 3.1415926535897932384626433832795;
59 /////////////////////////////////////////////////////////////////////////////
63 MyGraphSeries::MyGraphSeries(const CString
& sLabel
/* = "" */ )
69 /* virtual */ MyGraphSeries::~MyGraphSeries()
71 for (int nGroup
= 0; nGroup
< m_oaRegions
.GetSize(); ++nGroup
) {
72 delete m_oaRegions
.GetAt(nGroup
);
77 void MyGraphSeries::SetLabel(const CString
& sLabel
)
85 void MyGraphSeries::SetData(int nGroup
, int nValue
)
88 _ASSERTE(0 <= nGroup
);
90 m_dwaValues
.SetAtGrow(nGroup
, nValue
);
94 void MyGraphSeries::SetTipRegion(int nGroup
, const CRect
& rc
)
98 auto prgnNew
= std::make_unique
<CRgn
>();
99 ASSERT_VALID(prgnNew
.get());
101 VERIFY(prgnNew
->CreateRectRgnIndirect(rc
));
102 SetTipRegion(nGroup
, prgnNew
.release());
106 void MyGraphSeries::SetTipRegion(int nGroup
, CRgn
* prgn
)
109 _ASSERTE(0 <= nGroup
);
112 // If there is an existing region, delete it.
113 CRgn
* prgnOld
= nullptr;
115 if (nGroup
< m_oaRegions
.GetSize())
117 prgnOld
= m_oaRegions
.GetAt(nGroup
);
118 ASSERT_NULL_OR_POINTER(prgnOld
, CRgn
);
124 // Add the new region.
125 m_oaRegions
.SetAtGrow(nGroup
, prgn
);
127 _ASSERTE(m_oaRegions
.GetSize() <= m_dwaValues
.GetSize());
131 CString
MyGraphSeries::GetLabel() const
139 int MyGraphSeries::GetData(int nGroup
) const
142 _ASSERTE(0 <= nGroup
);
143 _ASSERTE(m_dwaValues
.GetSize() > nGroup
);
145 return m_dwaValues
[nGroup
];
148 // Returns the largest data value in this series.
149 int MyGraphSeries::GetMaxDataValue(bool bStackedGraph
) const
155 for (int nGroup
= 0; nGroup
< m_dwaValues
.GetSize(); ++nGroup
) {
157 nMax
= max(nMax
, static_cast<int> (m_dwaValues
.GetAt(nGroup
)));
160 nMax
+= static_cast<int> (m_dwaValues
.GetAt(nGroup
));
167 // Returns the average data value in this series.
168 int MyGraphSeries::GetAverageDataValue() const
174 for (int nGroup
= 0; nGroup
< m_dwaValues
.GetSize(); ++nGroup
) {
175 nTotal
+= static_cast<int>(m_dwaValues
.GetAt(nGroup
));
178 if (m_dwaValues
.IsEmpty())
181 return nTotal
/ static_cast<int>(m_dwaValues
.GetSize());
184 // Returns the number of data points that are not zero.
185 int MyGraphSeries::GetNonZeroElementCount() const
191 for (int nGroup
= 0; nGroup
< m_dwaValues
.GetSize(); ++nGroup
) {
192 if (m_dwaValues
.GetAt(nGroup
)) {
200 // Returns the sum of the data points for this series.
201 int MyGraphSeries::GetDataTotal() const
207 for (int nGroup
= 0; nGroup
< m_dwaValues
.GetSize(); ++nGroup
) {
208 nTotal
+= m_dwaValues
.GetAt(nGroup
);
214 // Returns which group (if any) the sent point lies within in this series.
215 int MyGraphSeries::HitTest(const CPoint
& pt
, int searchStart
= 0) const
219 for (int nGroup
= searchStart
; nGroup
< m_oaRegions
.GetSize(); ++nGroup
) {
220 CRgn
* prgnData
= m_oaRegions
.GetAt(nGroup
);
221 ASSERT_NULL_OR_POINTER(prgnData
, CRgn
);
223 if (prgnData
&& prgnData
->PtInRegion(pt
)) {
231 // Get the series portion of the tip for this group in this series.
232 CString
MyGraphSeries::GetTipText(int nGroup
, const CString
&unitString
) const
235 _ASSERTE(0 <= nGroup
);
236 _ASSERTE(m_oaRegions
.GetSize() <= m_dwaValues
.GetSize());
240 sTip
.Format(L
"%d %s (%d%%)", m_dwaValues
.GetAt(nGroup
),
241 static_cast<LPCWSTR
>(unitString
),
242 GetDataTotal() ? static_cast<int>(100.0 * static_cast<double>(m_dwaValues
.GetAt(nGroup
)) / static_cast<double>(GetDataTotal())) : 0);
248 /////////////////////////////////////////////////////////////////////////////
252 MyGraph::MyGraph(GraphType eGraphType
/* = MyGraph::Pie */ , bool bStackedGraph
/* = false */)
255 , m_nAxisLabelHeight(0)
256 , m_nAxisTickLabelHeight(0)
257 , m_eGraphType(eGraphType
)
258 , m_bStackedGraph(bStackedGraph
)
260 m_ptOrigin
.x
= m_ptOrigin
.y
= 0;
261 m_rcGraph
.SetRectEmpty();
262 m_rcLegend
.SetRectEmpty();
263 m_rcTitle
.SetRectEmpty();
267 /* virtual */ MyGraph::~MyGraph()
271 BEGIN_MESSAGE_MAP(MyGraph
, CStatic
)
272 //{{AFX_MSG_MAP(MyGraph)
276 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW
, 0, 0xFFFF, OnNeedText
)
277 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA
, 0, 0xFFFF, OnNeedText
)
280 // Called by the framework to allow other necessary sub classing to occur
281 // before the window is sub classed.
282 void MyGraph::PreSubclassWindow()
286 CStatic::PreSubclassWindow();
288 VERIFY(EnableToolTips(true));
292 /////////////////////////////////////////////////////////////////////////////
293 // MyGraph message handlers
295 // Handle the tooltip messages. Returns true to mean message was handled.
296 BOOL
MyGraph::OnNeedText(UINT
/*uiId*/, NMHDR
* pNMHDR
, LRESULT
* pResult
)
298 _ASSERTE(pNMHDR
&& "Bad parameter passed");
299 _ASSERTE(pResult
&& "Bad parameter passed");
302 UINT_PTR
uiID(pNMHDR
->idFrom
);
304 // Notification in NT from automatically created tooltip.
308 // Need to handle both ANSI and UNICODE versions of the message.
309 TOOLTIPTEXTA
* pTTTA
= reinterpret_cast<TOOLTIPTEXTA
*> (pNMHDR
);
310 ASSERT_POINTER(pTTTA
, TOOLTIPTEXTA
);
312 TOOLTIPTEXTW
* pTTTW
= reinterpret_cast<TOOLTIPTEXTW
*> (pNMHDR
);
313 ASSERT_POINTER(pTTTW
, TOOLTIPTEXTW
);
315 CString
sTipText(GetTipText());
318 if (TTN_NEEDTEXTA
== pNMHDR
->code
) {
319 lstrcpyn(pTTTA
->szText
, sTipText
, _countof(pTTTA
->szText
) - 1);
322 _mbstowcsz(pTTTW
->szText
, sTipText
, _countof(pTTTA
->szText
));
325 if (pNMHDR
->code
== TTN_NEEDTEXTA
) {
326 _wcstombsz(pTTTA
->szText
, sTipText
, _countof(pTTTA
->szText
));
329 lstrcpyn(pTTTW
->szText
, sTipText
, _countof(pTTTA
->szText
) - 1);
339 // The framework calls this member function to determine whether a point is in
340 // the bounding rectangle of the specified tool.
341 INT_PTR
MyGraph::OnToolHitTest(CPoint point
, TOOLINFO
* pTI
) const
343 _ASSERTE(pTI
&& "Bad parameter passed");
345 // This works around the problem of the tip remaining visible when you move
346 // the mouse to various positions over this control.
348 static bool bTipPopped(false);
349 static CPoint
ptPrev(-1,-1);
351 if (point
!= ptPrev
) {
363 pTI
->uId
= reinterpret_cast<UINT_PTR
>(m_hWnd
);
364 pTI
->lpszText
= LPSTR_TEXTCALLBACK
;
367 GetClientRect(&rcWnd
);
376 MyGraph::SpinTheMessageLoop();
381 // Build the tip text for the part of the graph that the mouse is currently
383 CString
MyGraph::GetTipText() const
389 // Get the position of the mouse.
391 VERIFY(::GetCursorPos(&pt
));
394 // Ask each part of the graph to check and see if the mouse is over it.
395 if (m_rcLegend
.PtInRect(pt
)) {
398 else if (m_rcTitle
.PtInRect(pt
)) {
402 int maxXAxis
= m_ptOrigin
.x
+ (m_nXAxisWidth
- m_rcLegend
.Width() - (GAP_PIXELS
* 2));
403 if (pt
.x
>= m_ptOrigin
.x
&& pt
.x
<= maxXAxis
) {
404 int average
= GetAverageDataValue();
405 int nMaxDataValue
= max(GetMaxDataValue(), 1);
406 double barTop
= m_ptOrigin
.y
- static_cast<double>(m_nYAxisHeight
) * (average
/ static_cast<double>(nMaxDataValue
));
407 if (pt
.y
>= barTop
- 2 && pt
.y
<= barTop
+ 2) {
408 sTip
.Format(L
"Average: %d %s (%d%%)", average
, static_cast<LPCWSTR
>(m_sYAxisLabel
), nMaxDataValue
? (100 * average
/ nMaxDataValue
) : 0);
413 POSITION
pos(m_olMyGraphSeries
.GetHeadPosition());
415 while (pos
&& sTip
.IsEmpty()) {
416 MyGraphSeries
* pSeries
= m_olMyGraphSeries
.GetNext(pos
);
417 ASSERT_VALID(pSeries
);
421 nGroup
= pSeries
->HitTest(pt
,nGroup
);
426 sTip
+= m_saLegendLabels
.GetAt(nGroup
) + L
": ";
427 sTip
+= pSeries
->GetTipText(nGroup
, m_sYAxisLabel
);
430 }while(-1 != nGroup
);
438 void MyGraph::OnPaint()
447 void MyGraph::OnSize(UINT nType
, int cx
, int cy
)
451 CStatic::OnSize(nType
, cx
, cy
);
456 // Change the type of the graph; the caller should call Invalidate() on this
457 // window to make the effect of this change visible.
458 void MyGraph::SetGraphType(GraphType e
, bool bStackedGraph
)
463 m_bStackedGraph
= bStackedGraph
;
466 // Calculate the current max legend label length in pixels.
467 int MyGraph::GetMaxLegendLabelLength(CDC
& dc
) const
476 // First get max number of characters.
477 for (int nGroup
= 0; nGroup
< m_saLegendLabels
.GetSize(); ++nGroup
) {
478 int nLabelLength(m_saLegendLabels
.GetAt(nGroup
).GetLength());
480 if (nMaxChars
< nLabelLength
) {
481 nMaxChars
= nLabelLength
;
482 sMax
= m_saLegendLabels
.GetAt(nGroup
);
486 // Now calculate the pixels.
487 siz
= dc
.GetTextExtent(sMax
);
489 _ASSERTE(-1 < siz
.cx
);
494 // Returns the largest number of data points in any series.
495 int MyGraph::GetMaxSeriesSize() const
500 POSITION
pos(m_olMyGraphSeries
.GetHeadPosition());
503 MyGraphSeries
* pSeries
= m_olMyGraphSeries
.GetNext(pos
);
504 ASSERT_VALID(pSeries
);
506 nMax
= max(nMax
, static_cast<int>(pSeries
->m_dwaValues
.GetSize()));
512 // Returns the largest number of non-zero data points in any series.
513 int MyGraph::GetMaxNonZeroSeriesSize() const
518 POSITION
pos(m_olMyGraphSeries
.GetHeadPosition());
521 MyGraphSeries
* pSeries
= m_olMyGraphSeries
.GetNext(pos
);
522 ASSERT_VALID(pSeries
);
524 nMax
= max(nMax
, pSeries
->GetNonZeroElementCount());
530 // Get the largest data value in all series.
531 int MyGraph::GetMaxDataValue() const
536 POSITION
pos(m_olMyGraphSeries
.GetHeadPosition());
539 MyGraphSeries
* pSeries
= m_olMyGraphSeries
.GetNext(pos
);
540 ASSERT_VALID(pSeries
);
542 nMax
= max(nMax
, pSeries
->GetMaxDataValue(m_bStackedGraph
));
548 // Get the average data value in all series.
549 int MyGraph::GetAverageDataValue() const
553 int nTotal
= 0, nCount
= 0;
554 POSITION
pos(m_olMyGraphSeries
.GetHeadPosition());
557 MyGraphSeries
* pSeries
= m_olMyGraphSeries
.GetNext(pos
);
558 ASSERT_VALID(pSeries
);
560 nTotal
+= pSeries
->GetAverageDataValue();
567 return nTotal
/ nCount
;
570 // How many series are populated?
571 int MyGraph::GetNonZeroSeriesCount() const
576 POSITION
pos(m_olMyGraphSeries
.GetHeadPosition());
579 MyGraphSeries
* pSeries
= m_olMyGraphSeries
.GetNext(pos
);
580 ASSERT_VALID(pSeries
);
582 if (0 < pSeries
->GetNonZeroElementCount()) {
587 return nCount
? nCount
: 1;
590 // Returns the group number for the sent label; -1 if not found.
591 int MyGraph::LookupLabel(const CString
& sLabel
) const
594 _ASSERTE(! sLabel
.IsEmpty());
596 for (int nGroup
= 0; nGroup
< m_saLegendLabels
.GetSize(); ++nGroup
) {
597 if (0 == sLabel
.CompareNoCase(m_saLegendLabels
.GetAt(nGroup
))) {
605 void MyGraph::Clear()
607 m_dwaColors
.RemoveAll();
608 m_saLegendLabels
.RemoveAll();
609 m_olMyGraphSeries
.RemoveAll();
613 void MyGraph::AddSeries(MyGraphSeries
& rMyGraphSeries
)
616 ASSERT_VALID(&rMyGraphSeries
);
617 _ASSERTE(m_saLegendLabels
.GetSize() == rMyGraphSeries
.m_dwaValues
.GetSize());
619 m_olMyGraphSeries
.AddTail(&rMyGraphSeries
);
623 void MyGraph::SetXAxisLabel(const CString
& sLabel
)
626 _ASSERTE(! sLabel
.IsEmpty());
628 m_sXAxisLabel
= sLabel
;
632 void MyGraph::SetYAxisLabel(const CString
& sLabel
)
635 _ASSERTE(! sLabel
.IsEmpty());
637 m_sYAxisLabel
= sLabel
;
640 // Returns the group number added. Also, makes sure that all the series have
641 // this many elements.
642 int MyGraph::AppendGroup(const CString
& sLabel
)
647 int nGroup(static_cast<int>(m_saLegendLabels
.GetSize()));
648 SetLegend(nGroup
, sLabel
);
650 // Make sure that all series have this element.
651 POSITION
pos(m_olMyGraphSeries
.GetHeadPosition());
654 MyGraphSeries
* pSeries
= m_olMyGraphSeries
.GetNext(pos
);
655 ASSERT_VALID(pSeries
);
657 if (nGroup
>= pSeries
->m_dwaValues
.GetSize()) {
658 pSeries
->m_dwaValues
.SetAtGrow(nGroup
, 0);
665 // Set this value to the legend.
666 void MyGraph::SetLegend(int nGroup
, const CString
& sLabel
)
669 _ASSERTE(0 <= nGroup
);
671 m_saLegendLabels
.SetAtGrow(nGroup
, sLabel
);
675 void MyGraph::SetGraphTitle(const CString
& sTitle
)
678 _ASSERTE(! sTitle
.IsEmpty());
684 void MyGraph::DrawGraph(CDC
& dc
)
689 if (GetMaxSeriesSize()) {
690 dc
.SetBkMode(TRANSPARENT
);
692 dc
.SetTextColor(CTheme::Instance().IsDarkTheme() ? CTheme::darkTextColor
: GetSysColor(COLOR_WINDOWTEXT
));
693 auto themePen
= CreatePen(PS_SOLID
, 1, CTheme::Instance().IsDarkTheme() ? CTheme::darkTextColor
: GetSysColor(COLOR_WINDOWTEXT
));
694 auto themeBrush
= CreateSolidBrush(CTheme::Instance().IsDarkTheme() ? CTheme::darkBkColor
: GetSysColor(COLOR_WINDOW
));
695 auto oldThemePen
= dc
.SelectObject(themePen
);
696 auto oldThemeBrush
= dc
.SelectObject(themeBrush
);
698 // Populate the colors as a group of evenly spaced colors of maximum
700 int nColorsDelta(240 / GetMaxSeriesSize());
702 int baseColorL
= 120;
704 DWORD backgroundColor
= CTheme::Instance().IsDarkTheme() ? CTheme::darkBkColor
: GetSysColor(COLOR_WINDOW
);
705 // If graph is a non-stacked line graph, use darker colors if system window color is light.
707 if (m_eGraphType
== MyGraph::Line
&& !m_bStackedGraph
) {
708 int backgroundLuma
= (GetRValue(backgroundColor
) + GetGValue(backgroundColor
) + GetBValue(backgroundColor
)) / 3;
709 if (backgroundLuma
> 128) {
715 for (WORD nGroup
= 0; nGroup
< GetMaxSeriesSize(); ++nGroup
) {
716 WORD colorH
= static_cast<WORD
>(nColorsDelta
* nGroup
);
717 WORD colorL
= static_cast<WORD
>(baseColorL
+ (diffColorL
* (nGroup
% 2)));
718 WORD colorS
= static_cast<WORD
>(180) + (30 * ((1 - nGroup
% 2) * (nGroup
% 3)));
719 COLORREF
cr(MyGraph::HLStoRGB(colorH
, colorL
, colorS
)); // Populate colors cleverly
720 m_dwaColors
.SetAtGrow(nGroup
, cr
);
723 // Reduce the graphable area by the frame window and status bar. We will
724 // leave GAP_PIXELS pixels blank on all sides of the graph. So top-left
725 // side of graph is at GAP_PIXELS,GAP_PIXELS and the bottom-right side
726 // of graph is at (m_rcGraph.Height() - GAP_PIXELS), (m_rcGraph.Width() -
727 // GAP_PIXELS). These settings are altered by axis labels and legends.
729 GetClientRect(&rcWnd
);
730 m_rcGraph
.left
= GAP_PIXELS
;
731 m_rcGraph
.top
= GAP_PIXELS
;
732 m_rcGraph
.right
= rcWnd
.Width() - GAP_PIXELS
;
733 m_rcGraph
.bottom
= rcWnd
.Height() - GAP_PIXELS
;
736 VERIFY(br
.CreateSolidBrush(backgroundColor
));
737 dc
.FillRect(rcWnd
, &br
);
743 // Set the axes and origin values.
746 // Draw legend if there is one and there's enough space.
747 if (m_saLegendLabels
.GetSize() && m_rcGraph
.right
-m_rcGraph
.left
> LEGEND_VISIBILITY_THRESHOLD
) {
751 m_rcLegend
.SetRectEmpty();
754 // Draw axes unless it's a pie.
755 if (m_eGraphType
!= MyGraph::GraphType::PieChart
) {
759 // Draw series data and labels.
760 switch (m_eGraphType
) {
761 case MyGraph::GraphType::Bar
: DrawSeriesBar(dc
); break;
762 case MyGraph::GraphType::Line
: if (m_bStackedGraph
) DrawSeriesLineStacked(dc
); else DrawSeriesLine(dc
); break;
763 case MyGraph::GraphType::PieChart
: DrawSeriesPie(dc
); break;
764 default: _ASSERTE(! "Bad default case"); break;
766 dc
.SelectObject(oldThemePen
);
767 dc
.SelectObject(oldThemeBrush
);
768 DeleteObject(themeBrush
);
769 DeleteObject(themePen
);
773 // Draw graph title; size is proportionate to width.
774 void MyGraph::DrawTitle(CDC
& dc
)
779 // Create the title font.
781 VERIFY(fontTitle
.CreatePointFont(max(m_rcGraph
.Width() / TITLE_DIVISOR
, MIN_FONT_SIZE
),
783 CFont
* pFontOld
= dc
.SelectObject(&fontTitle
);
784 ASSERT_VALID(pFontOld
);
787 m_rcTitle
.SetRect(GAP_PIXELS
, GAP_PIXELS
, m_rcGraph
.Width() + GAP_PIXELS
,
788 m_rcGraph
.Height() + GAP_PIXELS
);
790 dc
.DrawText(m_sTitle
, m_rcTitle
, DT_CENTER
| DT_NOPREFIX
| DT_SINGLELINE
|
791 DT_TOP
| DT_CALCRECT
);
793 m_rcTitle
.right
= m_rcGraph
.Width() + GAP_PIXELS
;
795 dc
.DrawText(m_sTitle
, m_rcTitle
, DT_CENTER
| DT_NOPREFIX
| DT_SINGLELINE
|
798 VERIFY(dc
.SelectObject(pFontOld
));
799 fontTitle
.DeleteObject();
802 // Set the axes and origin values.
803 void MyGraph::SetupAxes(CDC
& dc
)
808 // Since pie has no axis lines, set to full size minus GAP_PIXELS on each
809 // side. These are needed for legend to plot itself.
810 if (MyGraph::GraphType::PieChart
== m_eGraphType
)
812 m_nXAxisWidth
= m_rcGraph
.Width() - (GAP_PIXELS
* 2);
813 m_nYAxisHeight
= m_rcGraph
.Height() - m_rcTitle
.bottom
;
814 m_ptOrigin
.x
= GAP_PIXELS
;
815 m_ptOrigin
.y
= m_rcGraph
.Height() - GAP_PIXELS
;
818 // Bar and Line graphs.
820 // Need to find out how wide the biggest Y-axis tick label is
822 // Get and store height of axis label font.
823 m_nAxisLabelHeight
= max(m_rcGraph
.Height() / Y_AXIS_LABEL_DIVISOR
, MIN_FONT_SIZE
);
824 // Get and store height of tick label font.
825 m_nAxisTickLabelHeight
= max(int(m_nAxisLabelHeight
*0.8), MIN_FONT_SIZE
);
827 CFont fontTickLabels
;
828 VERIFY(fontTickLabels
.CreatePointFont(m_nAxisTickLabelHeight
, L
"Arial", &dc
));
829 // Select font and store the old.
830 CFont
* pFontOld
= dc
.SelectObject(&fontTickLabels
);
831 ASSERT_VALID(pFontOld
);
833 // Obtain tick label dimensions.
835 sTickLabel
.Format(L
"%d", GetMaxDataValue());
836 CSize
sizTickLabel(dc
.GetTextExtent(sTickLabel
));
838 // Set old font object again and delete temporary font object.
839 VERIFY(dc
.SelectObject(pFontOld
));
840 fontTickLabels
.DeleteObject();
842 // Determine axis specifications.
843 m_ptOrigin
.x
= m_rcGraph
.left
+ m_nAxisLabelHeight
/10 + 2*GAP_PIXELS
844 + sizTickLabel
.cx
+ GAP_PIXELS
+ TICK_PIXELS
;
845 m_ptOrigin
.y
= m_rcGraph
.bottom
- m_nAxisLabelHeight
/10 - 2*GAP_PIXELS
-
846 sizTickLabel
.cy
- GAP_PIXELS
- TICK_PIXELS
;
847 m_nYAxisHeight
= m_ptOrigin
.y
- m_rcTitle
.bottom
- (2 * GAP_PIXELS
);
848 m_nXAxisWidth
= (m_rcGraph
.Width() - GAP_PIXELS
) - m_ptOrigin
.x
;
853 void MyGraph::DrawLegend(CDC
& dc
)
858 // Create the legend font.
860 int pointFontHeight
= max(m_rcGraph
.Height() / LEGEND_DIVISOR
, MIN_FONT_SIZE
);
861 VERIFY(fontLegend
.CreatePointFont(pointFontHeight
, L
"Arial", &dc
));
863 // Get the height of each label.
865 VERIFY(fontLegend
.GetLogFont(&lf
));
866 // just in case the font height is invalid (zero), use min().
867 int nLabelHeight
= max(1l, abs(lf
.lfHeight
));
869 // Get number of legend entries
870 int nLegendEntries
= max(1, GetMaxSeriesSize());
872 // Calculate optimal label height = AvailableLegendHeight/AllAuthors
873 // Use a buffer of (GAP_PIXELS / 2) on each side inside the legend, and in addition the same
874 // gab above and below the legend frame, so in total 2*GAP_PIXELS
875 double optimalLabelHeight
= double(m_rcGraph
.Height() - 2*GAP_PIXELS
)/nLegendEntries
;
877 // Now relate the LabelHeight to the PointFontHeight
878 int optimalPointFontHeight
= int(pointFontHeight
*optimalLabelHeight
/nLabelHeight
);
880 // Limit the optimal PointFontHeight to the available range
881 optimalPointFontHeight
= min( max(optimalPointFontHeight
, MIN_FONT_SIZE
), pointFontHeight
);
883 // If the optimalPointFontHeight is different from the initial one, create a new legend font
884 if (optimalPointFontHeight
!= pointFontHeight
) {
885 fontLegend
.DeleteObject();
886 VERIFY(fontLegend
.CreatePointFont(optimalPointFontHeight
, L
"Arial", &dc
));
887 VERIFY(fontLegend
.GetLogFont(&lf
));
888 nLabelHeight
= max(1l, abs(lf
.lfHeight
));
891 // Calculate maximum number of authors that can be shown with the current label height
892 int nShownAuthors
= (m_rcGraph
.Height() - 2*GAP_PIXELS
)/nLabelHeight
- 1;
893 // Fix rounding errors.
894 if (nShownAuthors
+1 == GetMaxSeriesSize())
897 // Get number of authors to be shown.
898 nShownAuthors
= min(nShownAuthors
, GetMaxSeriesSize());
899 // nShownAuthors contains now the number of authors
901 CFont
* pFontOld
= dc
.SelectObject(&fontLegend
);
902 ASSERT_VALID(pFontOld
);
904 // Determine actual size of legend. A buffer of (GAP_PIXELS / 2) on each side,
905 // plus the height of each label based on the pint size of the font.
906 int nLegendHeight
= (GAP_PIXELS
/ 2) + (nShownAuthors
* nLabelHeight
) + (GAP_PIXELS
/ 2);
907 // Draw the legend border. Allow LEGEND_COLOR_BAR_PIXELS pixels for
908 // display of label bars.
909 m_rcLegend
.top
= (m_rcGraph
.Height() - nLegendHeight
) / 2;
910 m_rcLegend
.bottom
= m_rcLegend
.top
+ nLegendHeight
;
911 m_rcLegend
.right
= m_rcGraph
.Width() - GAP_PIXELS
;
912 m_rcLegend
.left
= m_rcLegend
.right
- GetMaxLegendLabelLength(dc
) -
913 LEGEND_COLOR_BAR_WIDTH_PIXELS
;
914 VERIFY(dc
.Rectangle(m_rcLegend
));
916 int skipped_row
= -1; // if != -1, this is the row that we show the ... in
917 if (nShownAuthors
< GetMaxSeriesSize())
918 skipped_row
= nShownAuthors
-2;
920 dc
.SetTextColor(CTheme::Instance().IsDarkTheme() ? CTheme::darkTextColor
: GetSysColor(COLOR_WINDOWTEXT
));
921 // Draw each group's label and bar.
922 for (int nGroup
= 0; nGroup
< nShownAuthors
; ++nGroup
) {
924 int nLabelTop(m_rcLegend
.top
+ (nGroup
* nLabelHeight
) +
927 int nShownGroup
= nGroup
; // introduce helper variable to avoid code duplication
929 // Do we have a skipped row?
930 if (skipped_row
!= -1)
932 if (nGroup
== skipped_row
) {
934 VERIFY(dc
.TextOut(m_rcLegend
.left
+ GAP_PIXELS
, nLabelTop
, L
"..."));
937 if (nGroup
== nShownAuthors
-1) {
938 // we show the last group instead of the scheduled group
939 nShownGroup
= GetMaxSeriesSize()-1;
943 VERIFY(dc
.TextOut(m_rcLegend
.left
+ GAP_PIXELS
, nLabelTop
,
944 m_saLegendLabels
.GetAt(nShownGroup
)));
946 // Determine the bar.
948 rcBar
.left
= m_rcLegend
.left
+ GAP_PIXELS
+ GetMaxLegendLabelLength(dc
) + GAP_PIXELS
;
949 rcBar
.top
= nLabelTop
+ LEGEND_COLOR_BAR_GAP_PIXELS
;
950 rcBar
.right
= m_rcLegend
.right
- GAP_PIXELS
;
951 rcBar
.bottom
= rcBar
.top
+ nLabelHeight
- LEGEND_COLOR_BAR_GAP_PIXELS
;
952 VERIFY(dc
.Rectangle(rcBar
));
954 // Draw bar for group.
955 COLORREF
crBar(m_dwaColors
.GetAt(nShownGroup
));
958 CBrush
* pBrushOld
= dc
.SelectObject(&br
);
959 ASSERT_VALID(pBrushOld
);
961 rcBar
.DeflateRect(LEGEND_COLOR_BAR_GAP_PIXELS
, LEGEND_COLOR_BAR_GAP_PIXELS
);
962 dc
.FillRect(rcBar
, &br
);
964 dc
.SelectObject(pBrushOld
);
968 VERIFY(dc
.SelectObject(pFontOld
));
969 fontLegend
.DeleteObject();
973 void MyGraph::DrawAxes(CDC
& dc
) const
977 _ASSERTE(MyGraph::GraphType::PieChart
!= m_eGraphType
);
979 dc
.SetTextColor(CTheme::Instance().IsDarkTheme() ? CTheme::darkTextColor
: GetSysColor(COLOR_WINDOWTEXT
));
982 dc
.MoveTo(m_ptOrigin
);
983 VERIFY(dc
.LineTo(m_ptOrigin
.x
, m_ptOrigin
.y
- m_nYAxisHeight
));
986 dc
.MoveTo(m_ptOrigin
);
988 if (m_saLegendLabels
.GetSize()) {
989 VERIFY(dc
.LineTo(m_ptOrigin
.x
+
990 (m_nXAxisWidth
- m_rcLegend
.Width() - (GAP_PIXELS
* 2)),
994 VERIFY(dc
.LineTo(m_ptOrigin
.x
+ m_nXAxisWidth
, m_ptOrigin
.y
));
997 // Note: m_nAxisLabelHeight and m_nAxisTickLabelHeight have been calculated in SetupAxis()
999 // Create the x-axis label font.
1001 VERIFY(fontXAxis
.CreatePointFont(m_nAxisLabelHeight
, L
"Arial", &dc
));
1003 // Obtain the height of the font in device coordinates.
1005 VERIFY(fontXAxis
.GetLogFont(&pLF
));
1006 int fontHeightDC
= pLF
.lfHeight
;
1008 // Create the y-axis label font.
1010 VERIFY(fontYAxis
.CreateFont(
1011 /* nHeight */ fontHeightDC
,
1013 /* nEscapement */ 90 * 10,
1014 /* nOrientation */ 0,
1015 /* nWeight */ FW_DONTCARE
,
1016 /* bItalic */ false,
1017 /* bUnderline */ false,
1021 CLIP_DEFAULT_PRECIS
,
1023 VARIABLE_PITCH
| FF_DONTCARE
,
1027 // Set the y-axis label font and draw the label.
1028 CFont
* pFontOld
= dc
.SelectObject(&fontYAxis
);
1029 ASSERT_VALID(pFontOld
);
1030 CSize
sizYLabel(dc
.GetTextExtent(m_sYAxisLabel
));
1031 VERIFY(dc
.TextOut(GAP_PIXELS
, (m_rcGraph
.Height() + sizYLabel
.cx
) / 2,
1034 // Set the x-axis label font and draw the label.
1035 VERIFY(dc
.SelectObject(&fontXAxis
));
1036 CSize
sizXLabel(dc
.GetTextExtent(m_sXAxisLabel
));
1037 VERIFY(dc
.TextOut(m_ptOrigin
.x
+ (m_nXAxisWidth
- sizXLabel
.cx
) / 2,
1038 m_rcGraph
.bottom
- GAP_PIXELS
- sizXLabel
.cy
, m_sXAxisLabel
));
1040 // chose suitable tick step (1, 2, 5, 10, 20, 50, etc.)
1041 int nMaxDataValue(GetMaxDataValue());
1042 nMaxDataValue
= max(nMaxDataValue
, 1);
1044 while (10 * nTickStep
* Y_AXIS_TICK_COUNT_TARGET
<= nMaxDataValue
)
1047 if (5 * nTickStep
* Y_AXIS_TICK_COUNT_TARGET
<= nMaxDataValue
)
1049 if (2 * nTickStep
* Y_AXIS_TICK_COUNT_TARGET
<= nMaxDataValue
)
1052 // We hardwire TITLE_DIVISOR y-axis ticks here for simplicity.
1053 int nTickCount(nMaxDataValue
/ nTickStep
);
1054 double tickSpace
= static_cast<double>(m_nYAxisHeight
) * nTickStep
/ static_cast<double>(nMaxDataValue
);
1056 // create tick label font and set it in the device context
1057 CFont fontTickLabels
;
1058 VERIFY(fontTickLabels
.CreatePointFont(m_nAxisTickLabelHeight
, L
"Arial", &dc
));
1059 VERIFY(dc
.SelectObject(&fontTickLabels
));
1061 for (int nTick
= 0; nTick
< nTickCount
; ++nTick
)
1063 int nTickYLocation
= static_cast<int>(m_ptOrigin
.y
- tickSpace
* (nTick
+ 1) + 0.5);
1064 dc
.MoveTo(m_ptOrigin
.x
- TICK_PIXELS
, nTickYLocation
);
1065 VERIFY(dc
.LineTo(m_ptOrigin
.x
+ TICK_PIXELS
, nTickYLocation
));
1069 sTickLabel
.Format(L
"%d", nTickStep
* (nTick
+ 1));
1070 CSize
sizTickLabel(dc
.GetTextExtent(sTickLabel
));
1072 VERIFY(dc
.TextOut(m_ptOrigin
.x
- GAP_PIXELS
- sizTickLabel
.cx
- TICK_PIXELS
,
1073 nTickYLocation
- sizTickLabel
.cy
/2, sTickLabel
));
1076 // Draw X axis tick marks.
1077 POSITION
pos(m_olMyGraphSeries
.GetHeadPosition());
1081 MyGraphSeries
* pSeries
= m_olMyGraphSeries
.GetNext(pos
);
1082 ASSERT_VALID(pSeries
);
1084 // Ignore unpopulated series if bar chart.
1085 if (m_eGraphType
!= MyGraph::GraphType::Bar
||
1086 0 < pSeries
->GetNonZeroElementCount()) {
1087 // Get the spacing of the series.
1088 int nSeriesSpace(0);
1090 if (m_saLegendLabels
.GetSize()) {
1092 (m_nXAxisWidth
- m_rcLegend
.Width() - (GAP_PIXELS
* 2)) /
1093 (m_eGraphType
== MyGraph::GraphType::Bar
?
1094 GetNonZeroSeriesCount() : static_cast<int>(m_olMyGraphSeries
.GetCount()));
1097 nSeriesSpace
= m_nXAxisWidth
/ (m_eGraphType
== MyGraph::GraphType::Bar
?
1098 GetNonZeroSeriesCount() : static_cast<int>(m_olMyGraphSeries
.GetCount()));
1101 int nTickXLocation(m_ptOrigin
.x
+ ((nSeries
+ 1) * nSeriesSpace
) -
1102 (nSeriesSpace
/ 2));
1104 dc
.MoveTo(nTickXLocation
, m_ptOrigin
.y
- TICK_PIXELS
);
1105 VERIFY(dc
.LineTo(nTickXLocation
, m_ptOrigin
.y
+ TICK_PIXELS
));
1107 // Draw x-axis tick label.
1108 CString
sTickLabel(pSeries
->GetLabel());
1109 CSize
sizTickLabel(dc
.GetTextExtent(sTickLabel
));
1111 VERIFY(dc
.TextOut(nTickXLocation
- (sizTickLabel
.cx
/ 2),
1112 m_ptOrigin
.y
+ TICK_PIXELS
+ GAP_PIXELS
, sTickLabel
));
1118 VERIFY(dc
.SelectObject(pFontOld
));
1119 fontXAxis
.DeleteObject();
1120 fontYAxis
.DeleteObject();
1121 fontTickLabels
.DeleteObject();
1125 void MyGraph::DrawSeriesBar(CDC
& dc
) const
1130 // How much space does each series get (includes inter series space)?
1131 // We ignore series whose members are all zero.
1132 double availableSpace
= m_saLegendLabels
.GetSize()
1133 ? m_nXAxisWidth
- m_rcLegend
.Width() - (GAP_PIXELS
* 2)
1136 double seriesSpace
= availableSpace
/ static_cast<double>(GetNonZeroSeriesCount());
1138 // Determine width of bars. Data points with a value of zero are assumed
1139 // to be empty. This is a bad assumption.
1140 double barWidth(0.0);
1142 // This is the width of the largest series (no inter series space).
1143 double maxSeriesPlotSize(0.0);
1145 if(!m_bStackedGraph
){
1146 int seriessize
= GetMaxNonZeroSeriesSize();
1147 barWidth
= seriessize
? seriesSpace
/ seriessize
: 0;
1148 if (1 < GetNonZeroSeriesCount()) {
1149 barWidth
*= INTERSERIES_PERCENT_USED
;
1151 maxSeriesPlotSize
= GetMaxNonZeroSeriesSize() * barWidth
;
1154 barWidth
= seriesSpace
* INTERSERIES_PERCENT_USED
;
1155 maxSeriesPlotSize
= barWidth
;
1158 // Iterate the series.
1159 POSITION
pos(m_olMyGraphSeries
.GetHeadPosition());
1163 MyGraphSeries
* pSeries
= m_olMyGraphSeries
.GetNext(pos
);
1164 ASSERT_VALID(pSeries
);
1166 // Ignore unpopulated series.
1167 if (0 < pSeries
->GetNonZeroElementCount()) {
1168 // Draw each bar; empty bars are not drawn.
1169 double runningLeft(m_ptOrigin
.x
+ (nSeries
+ 1) * seriesSpace
-
1172 double stackAccumulator(0.0);
1174 for (int nGroup
= 0; nGroup
< GetMaxSeriesSize(); ++nGroup
) {
1175 if (pSeries
->GetData(nGroup
)) {
1176 int nMaxDataValue(GetMaxDataValue());
1177 nMaxDataValue
= max(nMaxDataValue
, 1);
1178 double barTop
= m_ptOrigin
.y
- static_cast<double>(m_nYAxisHeight
) * pSeries
->GetData(nGroup
) / static_cast<double>(nMaxDataValue
) - stackAccumulator
;
1181 rcBar
.left
= static_cast<int>(runningLeft
);
1182 rcBar
.top
= static_cast<int>(barTop
);
1183 // Make adjacent bar borders overlap, so there's only one pixel border line between them.
1184 rcBar
.right
= static_cast<int>(runningLeft
+ barWidth
) + 1;
1185 rcBar
.bottom
= static_cast<int>(static_cast<double>(m_ptOrigin
.y
) - stackAccumulator
) + 1;
1187 if(m_bStackedGraph
){
1188 stackAccumulator
= static_cast<double>(m_ptOrigin
.y
) - barTop
;
1191 pSeries
->SetTipRegion(nGroup
, rcBar
);
1193 COLORREF
crBar(m_dwaColors
.GetAt(nGroup
));
1195 CBrush
* pBrushOld
= dc
.SelectObject(&br
);
1196 ASSERT_VALID(pBrushOld
);
1198 VERIFY(dc
.Rectangle(rcBar
));
1199 dc
.SelectObject(pBrushOld
);
1202 if(!m_bStackedGraph
){
1203 runningLeft
+= barWidth
;
1212 if (!m_bStackedGraph
) {
1213 int nMaxDataValue
= max(GetMaxDataValue(), 1);
1214 double barTop
= m_ptOrigin
.y
- static_cast<double>(m_nYAxisHeight
) * (GetAverageDataValue() / static_cast<double>(nMaxDataValue
));
1215 dc
.MoveTo(m_ptOrigin
.x
, static_cast<int>(barTop
));
1216 VERIFY(dc
.LineTo(m_ptOrigin
.x
+ (m_nXAxisWidth
- m_rcLegend
.Width() - (GAP_PIXELS
* 2)), static_cast<int>(barTop
)));
1221 void MyGraph::DrawSeriesLine(CDC
& dc
) const
1225 _ASSERTE(!m_bStackedGraph
);
1227 // Iterate the groups.
1228 CPoint
ptLastLoc(0,0);
1231 for (int nGroup
= 0; nGroup
< GetMaxSeriesSize(); nGroup
++) {
1232 // How much space does each series get (includes inter series space)?
1233 int nSeriesSpace(0);
1235 if (m_saLegendLabels
.GetSize())
1236 nSeriesSpace
= (m_nXAxisWidth
- m_rcLegend
.Width() - (GAP_PIXELS
* 2)) / static_cast<int>(m_olMyGraphSeries
.GetCount());
1238 nSeriesSpace
= m_nXAxisWidth
/ static_cast<int>(m_olMyGraphSeries
.GetCount());
1240 // Determine width of bars.
1241 int nMaxSeriesSize(GetMaxSeriesSize());
1242 nMaxSeriesSize
= max(nMaxSeriesSize
, 1);
1243 int nBarWidth(nSeriesSpace
/ nMaxSeriesSize
);
1245 if (1 < m_olMyGraphSeries
.GetCount())
1246 nBarWidth
= static_cast<int>(static_cast<double>(nBarWidth
) * INTERSERIES_PERCENT_USED
);
1248 // This is the width of the largest series (no inter series space).
1249 //int nMaxSeriesPlotSize(GetMaxSeriesSize() * nBarWidth);
1251 // Iterate the series.
1252 POSITION
pos(m_olMyGraphSeries
.GetHeadPosition());
1255 COLORREF
crLine(m_dwaColors
.GetAt(nGroup
));
1257 CBrush
* pBrushOld
= dc
.SelectObject(&br
);
1258 ASSERT_VALID(pBrushOld
);
1259 CPen
penLine(PS_SOLID
, 1, crLine
);
1260 CPen
* pPenOld
= dc
.SelectObject(&penLine
);
1261 ASSERT_VALID(pPenOld
);
1263 for (int nSeries
= 0; nSeries
< m_olMyGraphSeries
.GetCount(); ++nSeries
) {
1264 MyGraphSeries
* pSeries
= m_olMyGraphSeries
.GetNext(pos
);
1265 ASSERT_VALID(pSeries
);
1267 // Get x and y location of center of ellipse.
1270 ptLoc
.x
= m_ptOrigin
.x
+ (((nSeries
+ 1) * nSeriesSpace
) -
1271 (nSeriesSpace
/ 2));
1273 int nMaxDataValue(GetMaxDataValue());
1274 nMaxDataValue
= max(nMaxDataValue
, 1);
1275 double dLineHeight(pSeries
->GetData(nGroup
) * m_nYAxisHeight
/
1276 double(nMaxDataValue
));
1278 ptLoc
.y
= static_cast<int>(static_cast<double>(m_ptOrigin
.y
) - dLineHeight
);
1281 // Draw line back to last data member.
1282 if (nSeries
> 0 && (pSeries
->GetData(nGroup
)!=0 || dataLastLoc
!= 0)) {
1283 dc
.MoveTo(ptLastLoc
.x
, ptLastLoc
.y
- 1);
1284 VERIFY(dc
.LineTo(ptLoc
.x
- 1, ptLoc
.y
- 1));
1287 // Now draw ellipse.
1288 CRect
rcEllipse(ptLoc
.x
- 3, ptLoc
.y
- 3, ptLoc
.x
+ 3, ptLoc
.y
+ 3);
1289 if(pSeries
->GetData(nGroup
)!=0){
1290 VERIFY(dc
.Ellipse(rcEllipse
));
1292 if (m_olMyGraphSeries
.GetCount() < 40)
1294 pSeries
->SetTipRegion(nGroup
, rcEllipse
);
1297 // Save last pt and data
1299 dataLastLoc
= pSeries
->GetData(nGroup
);
1301 VERIFY(dc
.SelectObject(pPenOld
));
1302 penLine
.DeleteObject();
1303 VERIFY(dc
.SelectObject(pBrushOld
));
1307 int nMaxDataValue
= max(GetMaxDataValue(), 1);
1308 double barTop
= m_ptOrigin
.y
- static_cast<double>(m_nYAxisHeight
) * (GetAverageDataValue() / static_cast<double>(nMaxDataValue
));
1309 dc
.MoveTo(m_ptOrigin
.x
, static_cast<int>(barTop
));
1310 VERIFY(dc
.LineTo(m_ptOrigin
.x
+ (m_nXAxisWidth
- m_rcLegend
.Width() - (GAP_PIXELS
* 2)), static_cast<int>(barTop
)));
1314 void MyGraph::DrawSeriesLineStacked(CDC
& dc
) const
1318 _ASSERTE(m_bStackedGraph
);
1320 int nSeriesCount
= static_cast<int>(m_olMyGraphSeries
.GetCount());
1322 CArray
<int> stackAccumulator
;
1323 stackAccumulator
.SetSize(nSeriesCount
);
1325 CArray
<CPoint
> polygon
;
1326 // Special case: if we only have single series, make polygon
1327 // a bar instead of one pixel line.
1328 polygon
.SetSize(nSeriesCount
==1 ? 4 : nSeriesCount
* 2);
1330 // How much space does each series get?
1331 int nSeriesSpace(0);
1332 if (m_saLegendLabels
.GetSize()) {
1333 nSeriesSpace
= (m_nXAxisWidth
- m_rcLegend
.Width() - (GAP_PIXELS
* 2)) /
1337 nSeriesSpace
= m_nXAxisWidth
/ nSeriesCount
;
1340 int nMaxDataValue(GetMaxDataValue());
1341 nMaxDataValue
= max(nMaxDataValue
, 1);
1342 double dYScaling
= double(m_nYAxisHeight
) / nMaxDataValue
;
1344 // Iterate the groups.
1345 for (int nGroup
= 0; nGroup
< GetMaxSeriesSize(); nGroup
++) {
1347 COLORREF
crGroup(m_dwaColors
.GetAt(nGroup
));
1349 CBrush
* pBrushOld
= dc
.SelectObject(&br
);
1350 ASSERT_VALID(pBrushOld
);
1351 // For polygon outline, use average of this and previous color, and darken it.
1352 COLORREF
crPrevGroup(nGroup
> 0 ? m_dwaColors
.GetAt(nGroup
-1) : crGroup
);
1353 COLORREF crOutline
= RGB(
1354 (GetRValue(crGroup
)+GetRValue(crPrevGroup
))/3,
1355 (GetGValue(crGroup
)+GetGValue(crPrevGroup
))/3,
1356 (GetBValue(crGroup
)+GetBValue(crPrevGroup
))/3);
1357 CPen
penLine(PS_SOLID
, 1, crOutline
);
1358 CPen
* pPenOld
= dc
.SelectObject(&penLine
);
1359 ASSERT_VALID(pPenOld
);
1361 // Construct bottom part of polygon from current stack accumulator
1362 for (int nPolyBottom
= 0; nPolyBottom
< nSeriesCount
; ++nPolyBottom
) {
1364 ptLoc
.x
= m_ptOrigin
.x
+ (((nPolyBottom
+ 1) * nSeriesSpace
) - (nSeriesSpace
/ 2));
1365 double dLineHeight((stackAccumulator
[nPolyBottom
]) * dYScaling
);
1366 ptLoc
.y
= static_cast<int>(static_cast<double>(m_ptOrigin
.y
) - dLineHeight
);
1368 if (nSeriesCount
> 1) {
1369 polygon
[nSeriesCount
-nPolyBottom
-1] = ptLoc
;
1371 // special case: when there's one series, make polygon a bar
1372 polygon
[0] = CPoint(ptLoc
.x
-GAP_PIXELS
/2, ptLoc
.y
);
1373 polygon
[1] = CPoint(ptLoc
.x
+GAP_PIXELS
/2, ptLoc
.y
);
1377 // Iterate the series, construct upper part of polygon and upadte stack accumulator
1378 POSITION
pos(m_olMyGraphSeries
.GetHeadPosition());
1379 for (int nSeries
= 0; nSeries
< nSeriesCount
; ++nSeries
) {
1380 MyGraphSeries
* pSeries
= m_olMyGraphSeries
.GetNext(pos
);
1381 ASSERT_VALID(pSeries
);
1384 ptLoc
.x
= m_ptOrigin
.x
+ (((nSeries
+ 1) * nSeriesSpace
) -
1385 (nSeriesSpace
/ 2));
1386 double dLineHeight((pSeries
->GetData(nGroup
) + stackAccumulator
[nSeries
]) * dYScaling
);
1387 ptLoc
.y
= static_cast<int>(static_cast<double>(m_ptOrigin
.y
) - dLineHeight
);
1388 if (nSeriesCount
> 1) {
1389 polygon
[nSeriesCount
+nSeries
] = ptLoc
;
1391 // special case: when there's one series, make polygon a bar
1392 polygon
[2] = CPoint(ptLoc
.x
+GAP_PIXELS
/2, ptLoc
.y
);
1393 polygon
[3] = CPoint(ptLoc
.x
-GAP_PIXELS
/2, ptLoc
.y
);
1396 stackAccumulator
[nSeries
] += pSeries
->GetData(nGroup
);
1400 VERIFY(dc
.Polygon(polygon
.GetData(), static_cast<int>(polygon
.GetSize())));
1402 VERIFY(dc
.SelectObject(pPenOld
));
1403 penLine
.DeleteObject();
1404 VERIFY(dc
.SelectObject(pBrushOld
));
1410 void MyGraph::DrawSeriesPie(CDC
& dc
) const
1415 // Determine width of pie display area (pie and space).
1416 int nSeriesSpace(0);
1418 int seriesCount
= GetNonZeroSeriesCount();
1419 int horizontalSpace(0);
1421 if (m_saLegendLabels
.GetSize()) {
1424 horizontalSpace
= m_nXAxisWidth
- m_rcLegend
.Width() - (GAP_PIXELS
* 2);
1425 int nPieAndSpaceWidth(horizontalSpace
/ (seriesCount
? seriesCount
: 1));
1427 // Height is limiting factor.
1428 if (nPieAndSpaceWidth
> m_nYAxisHeight
- (GAP_PIXELS
* 2)) {
1429 nSeriesSpace
= (m_nYAxisHeight
- (GAP_PIXELS
* 2));
1432 // Width is limiting factor.
1433 nSeriesSpace
= nPieAndSpaceWidth
;
1439 horizontalSpace
= m_nXAxisWidth
;
1441 // Height is limiting factor.
1442 if (m_nXAxisWidth
> m_nYAxisHeight
* (seriesCount
? seriesCount
: 1)) {
1443 nSeriesSpace
= m_nYAxisHeight
;
1446 // Width is limiting factor.
1447 nSeriesSpace
= m_nXAxisWidth
/ (seriesCount
? seriesCount
: 1);
1451 // Make pies be centered horizontally
1452 int xOrigin
= m_ptOrigin
.x
+ GAP_PIXELS
+ (horizontalSpace
- nSeriesSpace
* seriesCount
) / 2;
1454 // Create font for labels.
1456 int pointFontHeight
= max(m_rcGraph
.Height() / Y_AXIS_LABEL_DIVISOR
, MIN_FONT_SIZE
);
1457 VERIFY(fontLabels
.CreatePointFont(pointFontHeight
, L
"Arial", &dc
));
1458 CFont
* pFontOld
= dc
.SelectObject(&fontLabels
);
1459 ASSERT_VALID(pFontOld
);
1463 int nRadius(static_cast<int>(nSeriesSpace
* INTERSERIES_PERCENT_USED
/ 2.0));
1464 POSITION
pos(m_olMyGraphSeries
.GetHeadPosition());
1467 MyGraphSeries
* pSeries
= m_olMyGraphSeries
.GetNext(pos
);
1468 ASSERT_VALID(pSeries
);
1470 // Don't leave a space for empty pies.
1471 if (0 < pSeries
->GetNonZeroElementCount()) {
1474 ptCenter
.x
= xOrigin
+ (nSeriesSpace
* nPie
) + nSeriesSpace
/ 2;
1475 ptCenter
.y
= m_ptOrigin
.y
- m_nYAxisHeight
/ 2;
1478 rcPie
.left
= ptCenter
.x
- nRadius
;
1479 rcPie
.right
= ptCenter
.x
+ nRadius
;
1480 rcPie
.top
= ptCenter
.y
- nRadius
;
1481 rcPie
.bottom
= ptCenter
.y
+ nRadius
;
1483 // Draw series label.
1484 CSize
sizPieLabel(dc
.GetTextExtent(pSeries
->GetLabel()));
1486 VERIFY(dc
.TextOut(ptCenter
.x
- (sizPieLabel
.cx
/ 2),
1487 ptCenter
.y
+ nRadius
+ GAP_PIXELS
, pSeries
->GetLabel()));
1489 // How much do the wedges total to?
1490 double dPieTotal(pSeries
->GetDataTotal());
1492 // Draw each wedge in this pie.
1493 CPoint
ptStart(rcPie
.left
, ptCenter
.y
);
1494 double dRunningWedgeTotal(0.0);
1496 for (int nGroup
= 0; nGroup
< m_saLegendLabels
.GetSize(); ++nGroup
) {
1497 // Ignore empty wedges.
1498 if (0 < pSeries
->GetData(nGroup
)) {
1499 // Get the degrees of this wedge.
1500 dRunningWedgeTotal
+= pSeries
->GetData(nGroup
);
1501 double dPercent(dRunningWedgeTotal
* 100.0 / dPieTotal
);
1502 double degrees(360.0 * dPercent
/ 100.0);
1504 // Find the location of the wedge's endpoint.
1505 CPoint
ptEnd(WedgeEndFromDegrees(degrees
, ptCenter
, nRadius
));
1507 // Special case: a wedge that takes up the whole pie would
1508 // otherwise be confused with an empty wedge.
1509 bool drawEmptyWedges
= false;
1510 if (1 == pSeries
->GetNonZeroElementCount()) {
1511 _ASSERTE(360 == static_cast<int>(degrees
) && ptStart
== ptEnd
&& "This is the problem we're correcting");
1513 drawEmptyWedges
= true;
1516 // If the wedge is zero size or very narrow, don't paint it.
1517 // If pie is small, and wedge data is small, we might get a wedges
1518 // where center and both endpoints lie on the same coordinate,
1519 // and endpoints differ only in one pixel. GDI draws such pie as whole pie,
1520 // so we just skip them instead.
1521 int distance
= abs(ptStart
.x
-ptEnd
.x
) + abs(ptStart
.y
-ptEnd
.y
);
1522 if (drawEmptyWedges
|| distance
> 1) {
1524 COLORREF
crWedge(m_dwaColors
.GetAt(nGroup
));
1526 CBrush
* pBrushOld
= dc
.SelectObject(&br
);
1527 ASSERT_VALID(pBrushOld
);
1528 VERIFY(dc
.Pie(rcPie
, ptStart
, ptEnd
));
1530 // Create a region from the path we create.
1531 VERIFY(dc
.BeginPath());
1532 VERIFY(dc
.Pie(rcPie
, ptStart
, ptEnd
));
1533 VERIFY(dc
.EndPath());
1534 auto prgnWedge
= std::make_unique
<CRgn
>();
1535 VERIFY(prgnWedge
->CreateFromPath(&dc
));
1536 pSeries
->SetTipRegion(nGroup
, prgnWedge
.release());
1539 dc
.SelectObject(pBrushOld
);
1550 // Draw X axis title
1551 CSize
sizXLabel(dc
.GetTextExtent(m_sXAxisLabel
));
1552 VERIFY(dc
.TextOut(xOrigin
+ (nSeriesSpace
* nPie
- sizXLabel
.cx
)/2,
1553 m_ptOrigin
.y
- m_nYAxisHeight
/2 + nRadius
+ GAP_PIXELS
*2 + sizXLabel
.cy
, m_sXAxisLabel
));
1555 VERIFY(dc
.SelectObject(pFontOld
));
1556 fontLabels
.DeleteObject();
1559 // Convert degrees to x and y coords.
1560 CPoint
MyGraph::WedgeEndFromDegrees(double degrees
, const CPoint
& ptCenter
,
1561 double radius
) const
1567 double radians
= degrees
/ 360.0 * M_PI
* 2.0;
1569 pt
.x
= static_cast<int>(radius
* cos(radians
));
1570 pt
.x
= ptCenter
.x
- pt
.x
;
1572 pt
.y
= static_cast<int>(radius
* sin(radians
));
1573 pt
.y
= ptCenter
.y
+ pt
.y
;
1578 // Spin The Message Loop: C++ version. See "Advanced Windows Programming",
1579 // M. Heller, p. 153, and the MS TechNet CD, PSS ID Number: Q99999.
1580 /* static */ UINT
MyGraph::SpinTheMessageLoop(bool bNoDrawing
/* = false */ ,
1581 bool bOnlyDrawing
/* = false */ ,
1582 UINT uiMsgAllowed
/* = WM_NULL */ )
1585 while (::PeekMessage(&msg
, nullptr, 0, 0, PM_REMOVE
)) {
1586 // Do painting only.
1587 if (bOnlyDrawing
&& WM_PAINT
== msg
.message
) {
1588 ::TranslateMessage(&msg
);
1589 ::DispatchMessage(&msg
);
1591 // Update user interface.
1592 AfxGetApp()->OnIdle(0);
1594 // Do everything *but* painting.
1595 else if (bNoDrawing
&& WM_PAINT
== msg
.message
) {
1598 // Special handling for this message.
1599 else if (WM_QUIT
== msg
.message
) {
1600 ::PostQuitMessage(static_cast<int>(msg
.wParam
));
1603 // Allow one message (like WM_LBUTTONDOWN).
1604 else if (uiMsgAllowed
== msg
.message
1605 && ! AfxGetApp()->PreTranslateMessage(&msg
)) {
1606 ::TranslateMessage(&msg
);
1607 ::DispatchMessage(&msg
);
1610 // This is the general case.
1611 else if (! bOnlyDrawing
&& ! AfxGetApp()->PreTranslateMessage(&msg
)) {
1612 ::TranslateMessage(&msg
);
1613 ::DispatchMessage(&msg
);
1615 // Update user interface, then free temporary objects.
1616 AfxGetApp()->OnIdle(0);
1617 AfxGetApp()->OnIdle(1);
1625 /////////////////////////////////////////////////////////////////////////////
1626 // Conversion routines: RGB to HLS (Red-Green-Blue to Hue-Luminosity-Saturation).
1627 // See Microsoft KnowledgeBase article Q29240.
1629 #define HLSMAX 240 // H,L, and S vary over 0-HLSMAX
1630 #define RGBMAX 255 // R,G, and B vary over 0-RGBMAX
1631 // HLSMAX BEST IF DIVISIBLE BY 6
1632 // RGBMAX, HLSMAX must each fit in a byte (255).
1634 #define UNDEFINED (HLSMAX * 2 / 3) // Hue is undefined if Saturation is 0
1635 // (grey-scale). This value determines
1636 // where the Hue scrollbar is initially
1637 // set for achromatic colors.
1640 // Convert HLS to RGB.
1641 /* static */ COLORREF
MyGraph::HLStoRGB(WORD wH
, WORD wL
, WORD wS
)
1643 _ASSERTE(240 >= wH
&& "Illegal hue value");
1644 _ASSERTE(240 >= wL
&& "Illegal lum value");
1645 _ASSERTE(240 >= wS
&& "Illegal sat value");
1653 wR
= wG
= wB
= (wL
* RGBMAX
) / HLSMAX
;
1655 if (UNDEFINED
!= wH
) {
1656 _ASSERTE(! "ERROR");
1664 // Set up magic numbers.
1665 if (wL
<= HLSMAX
/ 2) {
1666 Magic2
= (wL
* (HLSMAX
+ wS
) + (HLSMAX
/ 2)) / HLSMAX
;
1669 Magic2
= wL
+ wS
- ((wL
* wS
) + (HLSMAX
/ 2)) / HLSMAX
;
1672 Magic1
= 2 * wL
- Magic2
;
1674 // Get RGB, change units from HLSMAX to RGBMAX.
1675 wR
= (HueToRGB(Magic1
, Magic2
, wH
+ (HLSMAX
/ 3)) * RGBMAX
+ (HLSMAX
/ 2)) / HLSMAX
;
1676 wG
= (HueToRGB(Magic1
, Magic2
, wH
) * RGBMAX
+ (HLSMAX
/ 2)) / HLSMAX
;
1677 wB
= (HueToRGB(Magic1
, Magic2
, wH
- (HLSMAX
/ 3)) * RGBMAX
+ (HLSMAX
/ 2)) / HLSMAX
;
1680 return RGB(wR
,wG
,wB
);
1683 // Utility routine for HLStoRGB.
1684 /* static */ WORD
MyGraph::HueToRGB(WORD w1
, WORD w2
, WORD wH
)
1686 // Range check: note values passed add/subtract thirds of range.
1691 // Return r, g, or b value from this tridrant.
1692 if (wH
< HLSMAX
/ 6) {
1693 return w1
+ (((w2
- w1
) * wH
+ (HLSMAX
/ 12)) / (HLSMAX
/ 6));
1696 if (wH
< HLSMAX
/ 2) {
1700 if (wH
< (HLSMAX
* 2) / 3) {
1701 return w1
+ (((w2
- w1
) * (((HLSMAX
* 2) / 3) - wH
) + (HLSMAX
/ 12)) / (HLSMAX
/ 6));