Fix typos
[TortoiseGit.git] / src / Utils / MiscUI / MyGraph.cpp
blob68ff2b7a538e678b5b79b168d125e19937375816
1 // MyGraph.cpp
3 #include "stdafx.h"
4 #include "MyGraph.h"
5 #include "BufferDC.h"
6 #include "Theme.h"
8 #include <cmath>
9 #define _USE_MATH_DEFINES
10 #include <math.h> // for M_PI
11 #include <memory>
13 #ifdef _DEBUG
14 #define new DEBUG_NEW
15 #undef THIS_FILE
16 static char THIS_FILE[] = __FILE__;
17 #endif
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.
25 #ifndef VALIDATE
26 #ifdef _DEBUG
27 #define VALIDATE ::AfxAssertValidObject(this, __FILE__ , __LINE__ ); \
28 _ASSERTE(IsKindOf(GetRuntimeClass()));
29 #else
30 #define VALIDATE
31 #endif
32 #endif
35 /////////////////////////////////////////////////////////////////////////////
36 // Constants.
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
49 // spacing).
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.
55 #ifndef M_PI
56 const double M_PI = 3.1415926535897932384626433832795;
57 #endif
59 /////////////////////////////////////////////////////////////////////////////
60 // MyGraphSeries
62 // Constructor.
63 MyGraphSeries::MyGraphSeries(const CString& sLabel /* = "" */ )
64 : m_sLabel(sLabel)
68 // Destructor.
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)
79 VALIDATE;
81 m_sLabel = sLabel;
85 void MyGraphSeries::SetData(int nGroup, int nValue)
87 VALIDATE;
88 _ASSERTE(0 <= nGroup);
90 m_dwaValues.SetAtGrow(nGroup, nValue);
94 void MyGraphSeries::SetTipRegion(int nGroup, const CRect& rc)
96 VALIDATE;
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)
108 VALIDATE;
109 _ASSERTE(0 <= nGroup);
110 ASSERT_VALID(prgn);
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);
121 delete prgnOld;
122 prgnOld = nullptr;
124 // Add the new region.
125 m_oaRegions.SetAtGrow(nGroup, prgn);
127 _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());
131 CString MyGraphSeries::GetLabel() const
133 VALIDATE;
135 return m_sLabel;
139 int MyGraphSeries::GetData(int nGroup) const
141 VALIDATE;
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
151 VALIDATE;
153 int nMax(0);
155 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
156 if(!bStackedGraph){
157 nMax = max(nMax, static_cast<int> (m_dwaValues.GetAt(nGroup)));
159 else{
160 nMax += static_cast<int> (m_dwaValues.GetAt(nGroup));
164 return nMax;
167 // Returns the average data value in this series.
168 int MyGraphSeries::GetAverageDataValue() const
170 VALIDATE;
172 int nTotal = 0;
174 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
175 nTotal += static_cast<int>(m_dwaValues.GetAt(nGroup));
178 if (m_dwaValues.IsEmpty())
179 return 0;
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
187 VALIDATE;
189 int nCount(0);
191 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
192 if (m_dwaValues.GetAt(nGroup)) {
193 ++nCount;
197 return nCount;
200 // Returns the sum of the data points for this series.
201 int MyGraphSeries::GetDataTotal() const
203 VALIDATE;
205 int nTotal(0);
207 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
208 nTotal += m_dwaValues.GetAt(nGroup);
211 return nTotal;
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
217 VALIDATE;
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)) {
224 return nGroup;
228 return -1;
231 // Get the series portion of the tip for this group in this series.
232 CString MyGraphSeries::GetTipText(int nGroup, const CString &unitString) const
234 VALIDATE;
235 _ASSERTE(0 <= nGroup);
236 _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());
238 CString sTip;
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);
244 return sTip;
248 /////////////////////////////////////////////////////////////////////////////
249 // MyGraph
251 // Constructor.
252 MyGraph::MyGraph(GraphType eGraphType /* = MyGraph::Pie */ , bool bStackedGraph /* = false */)
253 : m_nXAxisWidth(0)
254 , m_nYAxisHeight(0)
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();
266 // Destructor.
267 /* virtual */ MyGraph::~MyGraph()
271 BEGIN_MESSAGE_MAP(MyGraph, CStatic)
272 //{{AFX_MSG_MAP(MyGraph)
273 ON_WM_PAINT()
274 ON_WM_SIZE()
275 //}}AFX_MSG_MAP
276 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnNeedText)
277 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnNeedText)
278 END_MESSAGE_MAP()
280 // Called by the framework to allow other necessary sub classing to occur
281 // before the window is sub classed.
282 void MyGraph::PreSubclassWindow()
284 VALIDATE;
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");
301 bool bReturn(false);
302 UINT_PTR uiID(pNMHDR->idFrom);
304 // Notification in NT from automatically created tooltip.
305 if (0U != uiID) {
306 bReturn = true;
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());
317 #ifndef _UNICODE
318 if (TTN_NEEDTEXTA == pNMHDR->code) {
319 lstrcpyn(pTTTA->szText, sTipText, _countof(pTTTA->szText) - 1);
321 else {
322 _mbstowcsz(pTTTW->szText, sTipText, _countof(pTTTA->szText));
324 #else
325 if (pNMHDR->code == TTN_NEEDTEXTA) {
326 _wcstombsz(pTTTA->szText, sTipText, _countof(pTTTA->szText));
328 else {
329 lstrcpyn(pTTTW->szText, sTipText, _countof(pTTTA->szText) - 1);
331 #endif
333 *pResult = 0;
336 return bReturn;
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.
347 INT_PTR nReturn(0);
348 static bool bTipPopped(false);
349 static CPoint ptPrev(-1,-1);
351 if (point != ptPrev) {
352 ptPrev = point;
354 if (bTipPopped) {
355 bTipPopped = false;
356 nReturn = -1;
358 else {
359 ::Sleep(50);
360 bTipPopped = true;
362 pTI->hwnd = m_hWnd;
363 pTI->uId = reinterpret_cast<UINT_PTR>(m_hWnd);
364 pTI->lpszText = LPSTR_TEXTCALLBACK;
366 CRect rcWnd;
367 GetClientRect(&rcWnd);
368 pTI->rect = rcWnd;
369 nReturn = 1;
372 else {
373 nReturn = 1;
376 MyGraph::SpinTheMessageLoop();
378 return nReturn;
381 // Build the tip text for the part of the graph that the mouse is currently
382 // over.
383 CString MyGraph::GetTipText() const
385 VALIDATE;
387 CString sTip("");
389 // Get the position of the mouse.
390 CPoint pt;
391 VERIFY(::GetCursorPos(&pt));
392 ScreenToClient(&pt);
394 // Ask each part of the graph to check and see if the mouse is over it.
395 if (m_rcLegend.PtInRect(pt)) {
396 sTip = "Legend";
398 else if (m_rcTitle.PtInRect(pt)) {
399 sTip = "Title";
401 else {
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);
409 return sTip;
413 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
415 while (pos && sTip.IsEmpty()) {
416 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
417 ASSERT_VALID(pSeries);
419 int nGroup(0);
421 nGroup = pSeries->HitTest(pt,nGroup);
423 if (-1 != nGroup) {
424 if (!sTip.IsEmpty())
425 sTip += L", ";
426 sTip += m_saLegendLabels.GetAt(nGroup) + L": ";
427 sTip += pSeries->GetTipText(nGroup, m_sYAxisLabel);
428 nGroup++;
430 }while(-1 != nGroup);
434 return sTip;
437 // Handle WM_PAINT.
438 void MyGraph::OnPaint()
440 VALIDATE;
442 CBufferDC dc(this);
443 DrawGraph(dc);
446 // Handle WM_SIZE.
447 void MyGraph::OnSize(UINT nType, int cx, int cy)
449 VALIDATE;
451 CStatic::OnSize(nType, cx, cy);
453 Invalidate();
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)
460 VALIDATE;
462 m_eGraphType = e;
463 m_bStackedGraph = bStackedGraph;
466 // Calculate the current max legend label length in pixels.
467 int MyGraph::GetMaxLegendLabelLength(CDC& dc) const
469 VALIDATE;
470 ASSERT_VALID(&dc);
472 CString sMax;
473 int nMaxChars(-1);
474 CSize siz(-1,-1);
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);
491 return siz.cx;
494 // Returns the largest number of data points in any series.
495 int MyGraph::GetMaxSeriesSize() const
497 VALIDATE;
499 int nMax(0);
500 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
502 while (pos) {
503 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
504 ASSERT_VALID(pSeries);
506 nMax = max(nMax, static_cast<int>(pSeries->m_dwaValues.GetSize()));
509 return nMax;
512 // Returns the largest number of non-zero data points in any series.
513 int MyGraph::GetMaxNonZeroSeriesSize() const
515 VALIDATE;
517 int nMax(0);
518 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
520 while (pos) {
521 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
522 ASSERT_VALID(pSeries);
524 nMax = max(nMax, pSeries->GetNonZeroElementCount());
527 return nMax;
530 // Get the largest data value in all series.
531 int MyGraph::GetMaxDataValue() const
533 VALIDATE;
535 int nMax(0);
536 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
538 while (pos) {
539 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
540 ASSERT_VALID(pSeries);
542 nMax = max(nMax, pSeries->GetMaxDataValue(m_bStackedGraph));
545 return nMax;
548 // Get the average data value in all series.
549 int MyGraph::GetAverageDataValue() const
551 VALIDATE;
553 int nTotal = 0, nCount = 0;
554 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
556 while (pos) {
557 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
558 ASSERT_VALID(pSeries);
560 nTotal += pSeries->GetAverageDataValue();
561 ++nCount;
564 if (nCount == 0)
565 return 0;
567 return nTotal / nCount;
570 // How many series are populated?
571 int MyGraph::GetNonZeroSeriesCount() const
573 VALIDATE;
575 int nCount(0);
576 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
578 while (pos) {
579 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
580 ASSERT_VALID(pSeries);
582 if (0 < pSeries->GetNonZeroElementCount()) {
583 ++nCount;
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
593 VALIDATE;
594 _ASSERTE(! sLabel.IsEmpty());
596 for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {
597 if (0 == sLabel.CompareNoCase(m_saLegendLabels.GetAt(nGroup))) {
598 return nGroup;
602 return -1;
605 void MyGraph::Clear()
607 m_dwaColors.RemoveAll();
608 m_saLegendLabels.RemoveAll();
609 m_olMyGraphSeries.RemoveAll();
613 void MyGraph::AddSeries(MyGraphSeries& rMyGraphSeries)
615 VALIDATE;
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)
625 VALIDATE;
626 _ASSERTE(! sLabel.IsEmpty());
628 m_sXAxisLabel = sLabel;
632 void MyGraph::SetYAxisLabel(const CString& sLabel)
634 VALIDATE;
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)
644 VALIDATE;
646 // Add the group.
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());
653 while (pos) {
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);
662 return nGroup;
665 // Set this value to the legend.
666 void MyGraph::SetLegend(int nGroup, const CString& sLabel)
668 VALIDATE;
669 _ASSERTE(0 <= nGroup);
671 m_saLegendLabels.SetAtGrow(nGroup, sLabel);
675 void MyGraph::SetGraphTitle(const CString& sTitle)
677 VALIDATE;
678 _ASSERTE(! sTitle.IsEmpty());
680 m_sTitle = sTitle;
684 void MyGraph::DrawGraph(CDC& dc)
686 VALIDATE;
687 ASSERT_VALID(&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
699 // saturation.
700 int nColorsDelta(240 / GetMaxSeriesSize());
702 int baseColorL = 120;
703 int diffColorL = 60;
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.
706 #if 0
707 if (m_eGraphType == MyGraph::Line && !m_bStackedGraph) {
708 int backgroundLuma = (GetRValue(backgroundColor) + GetGValue(backgroundColor) + GetBValue(backgroundColor)) / 3;
709 if (backgroundLuma > 128) {
710 baseColorL = 70;
711 diffColorL = 50;
714 #endif
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.
728 CRect rcWnd;
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;
735 CBrush br;
736 VERIFY(br.CreateSolidBrush(backgroundColor));
737 dc.FillRect(rcWnd, &br);
738 br.DeleteObject();
740 // Draw graph title.
741 DrawTitle(dc);
743 // Set the axes and origin values.
744 SetupAxes(dc);
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) {
748 DrawLegend(dc);
750 else{
751 m_rcLegend.SetRectEmpty();
754 // Draw axes unless it's a pie.
755 if (m_eGraphType != MyGraph::GraphType::PieChart) {
756 DrawAxes(dc);
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)
776 VALIDATE;
777 ASSERT_VALID(&dc);
779 // Create the title font.
780 CFont fontTitle;
781 VERIFY(fontTitle.CreatePointFont(max(m_rcGraph.Width() / TITLE_DIVISOR, MIN_FONT_SIZE),
782 L"Arial", &dc));
783 CFont* pFontOld = dc.SelectObject(&fontTitle);
784 ASSERT_VALID(pFontOld);
786 // Draw the title.
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 |
796 DT_TOP);
798 VERIFY(dc.SelectObject(pFontOld));
799 fontTitle.DeleteObject();
802 // Set the axes and origin values.
803 void MyGraph::SetupAxes(CDC& dc)
805 VALIDATE;
806 ASSERT_VALID(&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;
817 else {
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.
834 CString sTickLabel;
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)
855 VALIDATE;
856 ASSERT_VALID(&dc);
858 // Create the legend font.
859 CFont fontLegend;
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.
864 LOGFONT lf = { 0 };
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())
895 ++nShownAuthors;
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) +
925 (GAP_PIXELS / 2));
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) {
933 // draw the dots
934 VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop, L"..."));
935 continue;
937 if (nGroup == nShownAuthors-1) {
938 // we show the last group instead of the scheduled group
939 nShownGroup = GetMaxSeriesSize()-1;
942 // Draw the label.
943 VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop,
944 m_saLegendLabels.GetAt(nShownGroup)));
946 // Determine the bar.
947 CRect rcBar;
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));
956 CBrush br(crBar);
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);
965 br.DeleteObject();
968 VERIFY(dc.SelectObject(pFontOld));
969 fontLegend.DeleteObject();
973 void MyGraph::DrawAxes(CDC& dc) const
975 VALIDATE;
976 ASSERT_VALID(&dc);
977 _ASSERTE(MyGraph::GraphType::PieChart != m_eGraphType);
979 dc.SetTextColor(CTheme::Instance().IsDarkTheme() ? CTheme::darkTextColor : GetSysColor(COLOR_WINDOWTEXT));
981 // Draw y axis.
982 dc.MoveTo(m_ptOrigin);
983 VERIFY(dc.LineTo(m_ptOrigin.x, m_ptOrigin.y - m_nYAxisHeight));
985 // Draw x axis.
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)),
991 m_ptOrigin.y));
993 else {
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.
1000 CFont fontXAxis;
1001 VERIFY(fontXAxis.CreatePointFont(m_nAxisLabelHeight, L"Arial", &dc));
1003 // Obtain the height of the font in device coordinates.
1004 LOGFONT pLF;
1005 VERIFY(fontXAxis.GetLogFont(&pLF));
1006 int fontHeightDC = pLF.lfHeight;
1008 // Create the y-axis label font.
1009 CFont fontYAxis;
1010 VERIFY(fontYAxis.CreateFont(
1011 /* nHeight */ fontHeightDC,
1012 /* nWidth */ 0,
1013 /* nEscapement */ 90 * 10,
1014 /* nOrientation */ 0,
1015 /* nWeight */ FW_DONTCARE,
1016 /* bItalic */ false,
1017 /* bUnderline */ false,
1018 /* cStrikeOut */ 0,
1019 ANSI_CHARSET,
1020 OUT_DEFAULT_PRECIS,
1021 CLIP_DEFAULT_PRECIS,
1022 PROOF_QUALITY,
1023 VARIABLE_PITCH | FF_DONTCARE,
1024 L"Arial")
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,
1032 m_sYAxisLabel));
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);
1043 int nTickStep = 1;
1044 while (10 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
1045 nTickStep *= 10;
1047 if (5 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
1048 nTickStep *= 5;
1049 if (2 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
1050 nTickStep *= 2;
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));
1067 // Draw tick label.
1068 CString sTickLabel;
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());
1078 int nSeries(0);
1080 while (pos) {
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()) {
1091 nSeriesSpace =
1092 (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
1093 (m_eGraphType == MyGraph::GraphType::Bar ?
1094 GetNonZeroSeriesCount() : static_cast<int>(m_olMyGraphSeries.GetCount()));
1096 else {
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));
1114 ++nSeries;
1118 VERIFY(dc.SelectObject(pFontOld));
1119 fontXAxis.DeleteObject();
1120 fontYAxis.DeleteObject();
1121 fontTickLabels.DeleteObject();
1125 void MyGraph::DrawSeriesBar(CDC& dc) const
1127 VALIDATE;
1128 ASSERT_VALID(&dc);
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)
1134 : m_nXAxisWidth;
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;
1153 else{
1154 barWidth = seriesSpace * INTERSERIES_PERCENT_USED;
1155 maxSeriesPlotSize = barWidth;
1158 // Iterate the series.
1159 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1160 int nSeries(0);
1162 while (pos) {
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 -
1170 maxSeriesPlotSize);
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;
1180 CRect rcBar;
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));
1194 CBrush br(crBar);
1195 CBrush* pBrushOld = dc.SelectObject(&br);
1196 ASSERT_VALID(pBrushOld);
1198 VERIFY(dc.Rectangle(rcBar));
1199 dc.SelectObject(pBrushOld);
1200 br.DeleteObject();
1202 if(!m_bStackedGraph){
1203 runningLeft += barWidth;
1208 ++nSeries;
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
1223 VALIDATE;
1224 ASSERT_VALID(&dc);
1225 _ASSERTE(!m_bStackedGraph);
1227 // Iterate the groups.
1228 CPoint ptLastLoc(0,0);
1229 int dataLastLoc(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());
1237 else
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());
1254 // Build objects.
1255 COLORREF crLine(m_dwaColors.GetAt(nGroup));
1256 CBrush br(crLine);
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.
1268 CPoint ptLoc(0,0);
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
1298 ptLastLoc = ptLoc;
1299 dataLastLoc = pSeries->GetData(nGroup);
1301 VERIFY(dc.SelectObject(pPenOld));
1302 penLine.DeleteObject();
1303 VERIFY(dc.SelectObject(pBrushOld));
1304 br.DeleteObject();
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
1316 VALIDATE;
1317 ASSERT_VALID(&dc);
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)) /
1334 nSeriesCount;
1336 else {
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++) {
1346 // Build objects.
1347 COLORREF crGroup(m_dwaColors.GetAt(nGroup));
1348 CBrush br(crGroup);
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) {
1363 CPoint ptLoc;
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;
1370 } else {
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);
1383 CPoint ptLoc;
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;
1390 } else {
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);
1399 // Draw polygon
1400 VERIFY(dc.Polygon(polygon.GetData(), static_cast<int>(polygon.GetSize())));
1402 VERIFY(dc.SelectObject(pPenOld));
1403 penLine.DeleteObject();
1404 VERIFY(dc.SelectObject(pBrushOld));
1405 br.DeleteObject();
1410 void MyGraph::DrawSeriesPie(CDC& dc) const
1412 VALIDATE;
1413 ASSERT_VALID(&dc);
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()) {
1422 // With legend box.
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));
1431 else {
1432 // Width is limiting factor.
1433 nSeriesSpace = nPieAndSpaceWidth;
1436 else {
1437 // No legend box.
1439 horizontalSpace = m_nXAxisWidth;
1441 // Height is limiting factor.
1442 if (m_nXAxisWidth > m_nYAxisHeight * (seriesCount ? seriesCount : 1)) {
1443 nSeriesSpace = m_nYAxisHeight;
1445 else {
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.
1455 CFont fontLabels;
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);
1461 // Draw each pie.
1462 int nPie(0);
1463 int nRadius(static_cast<int>(nSeriesSpace * INTERSERIES_PERCENT_USED / 2.0));
1464 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1466 while (pos) {
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()) {
1472 // Locate this pie.
1473 CPoint ptCenter;
1474 ptCenter.x = xOrigin + (nSeriesSpace * nPie) + nSeriesSpace / 2;
1475 ptCenter.y = m_ptOrigin.y - m_nYAxisHeight / 2;
1477 CRect rcPie;
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");
1512 --ptEnd.y;
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) {
1523 // Draw wedge.
1524 COLORREF crWedge(m_dwaColors.GetAt(nGroup));
1525 CBrush br(crWedge);
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());
1538 // Cleanup.
1539 dc.SelectObject(pBrushOld);
1540 br.DeleteObject();
1541 ptStart = ptEnd;
1546 ++nPie;
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
1563 VALIDATE;
1565 CPoint pt;
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;
1575 return pt;
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 */ )
1584 MSG msg = { 0 };
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) {
1596 break;
1598 // Special handling for this message.
1599 else if (WM_QUIT == msg.message) {
1600 ::PostQuitMessage(static_cast<int>(msg.wParam));
1601 break;
1603 // Allow one message (like WM_LBUTTONDOWN).
1604 else if (uiMsgAllowed == msg.message
1605 && ! AfxGetApp()->PreTranslateMessage(&msg)) {
1606 ::TranslateMessage(&msg);
1607 ::DispatchMessage(&msg);
1608 break;
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);
1621 return msg.message;
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");
1647 WORD wR(0);
1648 WORD wG(0);
1649 WORD wB(0);
1651 // Achromatic case.
1652 if (0 == wS) {
1653 wR = wG = wB = (wL * RGBMAX) / HLSMAX;
1655 if (UNDEFINED != wH) {
1656 _ASSERTE(! "ERROR");
1659 else {
1660 // Chromatic case.
1661 WORD Magic1(0);
1662 WORD Magic2(0);
1664 // Set up magic numbers.
1665 if (wL <= HLSMAX / 2) {
1666 Magic2 = (wL * (HLSMAX + wS) + (HLSMAX / 2)) / HLSMAX;
1668 else {
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.
1687 if (wH > HLSMAX) {
1688 wH -= HLSMAX;
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) {
1697 return w2;
1700 if (wH < (HLSMAX * 2) / 3) {
1701 return w1 + (((w2 - w1) * (((HLSMAX * 2) / 3) - wH) + (HLSMAX / 12)) / (HLSMAX / 6));
1703 else {
1704 return w1;