Variable cannot be less than zero so don't check about that
[TortoiseGit.git] / src / Utils / MiscUI / MyGraph.cpp
blob2da194e44f4ea44ef6f38bece145f07f1fc84be5
1 // MyGraph.cpp
3 #include "stdafx.h"
4 #include "MyGraph.h"
5 #include "BufferDC.h"
7 #include <cmath>
8 #include <memory>
10 #ifdef _DEBUG
11 #define new DEBUG_NEW
12 #undef THIS_FILE
13 static char THIS_FILE[] = __FILE__;
14 #endif
17 /////////////////////////////////////////////////////////////////////////////
18 // This macro can be called at the beginning and ending of every
19 // method. It is identical to saying "ASSERT_VALID(); ASSERT_KINDOF();"
20 // but is written like this so that VALIDATE can be a macro. It is useful
21 // as an "early warning" that something has gone wrong with "this" object.
22 #ifndef VALIDATE
23 #ifdef _DEBUG
24 #define VALIDATE ::AfxAssertValidObject(this, __FILE__ , __LINE__ ); \
25 _ASSERTE(IsKindOf(GetRuntimeClass()));
26 #else
27 #define VALIDATE
28 #endif
29 #endif
32 /////////////////////////////////////////////////////////////////////////////
33 // Constants.
35 #define TICK_PIXELS 4 // Size of tick marks.
36 #define GAP_PIXELS 6 // Better if an even value.
37 #define LEGEND_COLOR_BAR_WIDTH_PIXELS 50 // Width of color bar.
38 #define LEGEND_COLOR_BAR_GAP_PIXELS 1 // Space between color bars.
39 #define Y_AXIS_TICK_COUNT_TARGET 5 // How many ticks should be there on the y axis.
40 #define MIN_FONT_SIZE 70 // The minimum font-size in pt*10.
41 #define LEGEND_VISIBILITY_THRESHOLD 300 // The width of the graph in pixels when the legend gets hidden.
43 #define INTERSERIES_PERCENT_USED 0.85 // How much of the graph is
44 // used for bars/pies (the
45 // rest is for inter-series
46 // spacing).
48 #define TITLE_DIVISOR 5 // Scale font to graph width.
49 #define LEGEND_DIVISOR 8 // Scale font to graph height.
50 #define Y_AXIS_LABEL_DIVISOR 6 // Scale font to graph height.
52 const double PI = 3.1415926535897932384626433832795;
54 /////////////////////////////////////////////////////////////////////////////
55 // MyGraphSeries
57 // Constructor.
58 MyGraphSeries::MyGraphSeries(const CString& sLabel /* = "" */ )
59 : m_sLabel(sLabel)
63 // Destructor.
64 /* virtual */ MyGraphSeries::~MyGraphSeries()
66 for (int nGroup = 0; nGroup < m_oaRegions.GetSize(); ++nGroup) {
67 delete m_oaRegions.GetAt(nGroup);
72 void MyGraphSeries::SetLabel(const CString& sLabel)
74 VALIDATE;
76 m_sLabel = sLabel;
80 void MyGraphSeries::SetData(int nGroup, int nValue)
82 VALIDATE;
83 _ASSERTE(0 <= nGroup);
85 m_dwaValues.SetAtGrow(nGroup, nValue);
89 void MyGraphSeries::SetTipRegion(int nGroup, const CRect& rc)
91 VALIDATE;
93 std::unique_ptr<CRgn> prgnNew (new CRgn);
94 ASSERT_VALID(prgnNew.get());
96 VERIFY(prgnNew->CreateRectRgnIndirect(rc));
97 SetTipRegion(nGroup, prgnNew.release());
101 void MyGraphSeries::SetTipRegion(int nGroup, CRgn* prgn)
103 VALIDATE;
104 _ASSERTE(0 <= nGroup);
105 ASSERT_VALID(prgn);
107 // If there is an existing region, delete it.
108 CRgn* prgnOld = NULL;
110 if (nGroup < m_oaRegions.GetSize())
112 prgnOld = m_oaRegions.GetAt(nGroup);
113 ASSERT_NULL_OR_POINTER(prgnOld, CRgn);
116 delete prgnOld;
117 prgnOld = NULL;
119 // Add the new region.
120 m_oaRegions.SetAtGrow(nGroup, prgn);
122 _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());
126 CString MyGraphSeries::GetLabel() const
128 VALIDATE;
130 return m_sLabel;
134 int MyGraphSeries::GetData(int nGroup) const
136 VALIDATE;
137 _ASSERTE(0 <= nGroup);
138 _ASSERTE(m_dwaValues.GetSize() > nGroup);
140 return m_dwaValues[nGroup];
143 // Returns the largest data value in this series.
144 int MyGraphSeries::GetMaxDataValue(bool bStackedGraph) const
146 VALIDATE;
148 int nMax(0);
150 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
151 if(!bStackedGraph){
152 nMax = max(nMax, static_cast<int> (m_dwaValues.GetAt(nGroup)));
154 else{
155 nMax += static_cast<int> (m_dwaValues.GetAt(nGroup));
159 return nMax;
162 // Returns the average data value in this series.
163 int MyGraphSeries::GetAverageDataValue() const
165 VALIDATE;
167 int nTotal = 0;
169 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
170 nTotal += static_cast<int> (m_dwaValues.GetAt(nGroup));
173 if (m_dwaValues.GetSize() == 0)
174 return 0;
176 return nTotal / m_dwaValues.GetSize();
179 // Returns the number of data points that are not zero.
180 int MyGraphSeries::GetNonZeroElementCount() const
182 VALIDATE;
184 int nCount(0);
186 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
188 if (m_dwaValues.GetAt(nGroup)) {
189 ++nCount;
193 return nCount;
196 // Returns the sum of the data points for this series.
197 int MyGraphSeries::GetDataTotal() const
199 VALIDATE;
201 int nTotal(0);
203 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
204 nTotal += m_dwaValues.GetAt(nGroup);
207 return nTotal;
210 // Returns which group (if any) the sent point lies within in this series.
211 int MyGraphSeries::HitTest(const CPoint& pt, int searchStart = 0) const
213 VALIDATE;
215 for (int nGroup = searchStart; nGroup < m_oaRegions.GetSize(); ++nGroup) {
216 CRgn* prgnData = m_oaRegions.GetAt(nGroup);
217 ASSERT_NULL_OR_POINTER(prgnData, CRgn);
219 if (prgnData && prgnData->PtInRegion(pt)) {
220 return nGroup;
224 return -1;
227 // Get the series portion of the tip for this group in this series.
228 CString MyGraphSeries::GetTipText(int nGroup, const CString &unitString) const
230 VALIDATE;
231 _ASSERTE(0 <= nGroup);
232 _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());
234 CString sTip;
236 sTip.Format(_T("%d %s (%d%%)"), m_dwaValues.GetAt(nGroup),
237 (LPCTSTR)unitString,
238 GetDataTotal() ? (int) (100.0 * (double) m_dwaValues.GetAt(nGroup) /
239 (double) GetDataTotal()) : 0);
241 return sTip;
245 /////////////////////////////////////////////////////////////////////////////
246 // MyGraph
248 // Constructor.
249 MyGraph::MyGraph(GraphType eGraphType /* = MyGraph::Pie */ , bool bStackedGraph /* = false */)
250 : m_nXAxisWidth(0)
251 , m_nYAxisHeight(0)
252 , m_nAxisLabelHeight(0)
253 , m_nAxisTickLabelHeight(0)
254 , m_eGraphType(eGraphType)
255 , m_bStackedGraph(bStackedGraph)
257 m_ptOrigin.x = m_ptOrigin.y = 0;
258 m_rcGraph.SetRectEmpty();
259 m_rcLegend.SetRectEmpty();
260 m_rcTitle.SetRectEmpty();
263 // Destructor.
264 /* virtual */ MyGraph::~MyGraph()
268 BEGIN_MESSAGE_MAP(MyGraph, CStatic)
269 //{{AFX_MSG_MAP(MyGraph)
270 ON_WM_PAINT()
271 ON_WM_SIZE()
272 //}}AFX_MSG_MAP
273 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnNeedText)
274 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnNeedText)
275 END_MESSAGE_MAP()
277 // Called by the framework to allow other necessary sub classing to occur
278 // before the window is sub classed.
279 void MyGraph::PreSubclassWindow()
281 VALIDATE;
283 CStatic::PreSubclassWindow();
285 VERIFY(EnableToolTips(true));
289 /////////////////////////////////////////////////////////////////////////////
290 // MyGraph message handlers
292 // Handle the tooltip messages. Returns true to mean message was handled.
293 BOOL MyGraph::OnNeedText(UINT /*uiId*/, NMHDR* pNMHDR, LRESULT* pResult)
295 _ASSERTE(pNMHDR && "Bad parameter passed");
296 _ASSERTE(pResult && "Bad parameter passed");
298 bool bReturn(false);
299 UINT_PTR uiID(pNMHDR->idFrom);
301 // Notification in NT from automatically created tooltip.
302 if (0U != uiID) {
303 bReturn = true;
305 // Need to handle both ANSI and UNICODE versions of the message.
306 TOOLTIPTEXTA* pTTTA = reinterpret_cast<TOOLTIPTEXTA*> (pNMHDR);
307 ASSERT_POINTER(pTTTA, TOOLTIPTEXTA);
309 TOOLTIPTEXTW* pTTTW = reinterpret_cast<TOOLTIPTEXTW*> (pNMHDR);
310 ASSERT_POINTER(pTTTW, TOOLTIPTEXTW);
312 CString sTipText(GetTipText());
314 #ifndef _UNICODE
315 if (TTN_NEEDTEXTA == pNMHDR->code) {
316 lstrcpyn(pTTTA->szText, sTipText, _countof(pTTTA->szText));
318 else {
319 _mbstowcsz(pTTTW->szText, sTipText, _countof(pTTTA->szText));
321 #else
322 if (pNMHDR->code == TTN_NEEDTEXTA) {
323 _wcstombsz(pTTTA->szText, sTipText, _countof(pTTTA->szText));
325 else {
326 lstrcpyn(pTTTW->szText, sTipText, _countof(pTTTA->szText));
328 #endif
330 *pResult = 0;
333 return bReturn;
336 // The framework calls this member function to determine whether a point is in
337 // the bounding rectangle of the specified tool.
338 INT_PTR MyGraph::OnToolHitTest(CPoint point, TOOLINFO* pTI) const
340 _ASSERTE(pTI && "Bad parameter passed");
342 // This works around the problem of the tip remaining visible when you move
343 // the mouse to various positions over this control.
344 INT_PTR nReturn(0);
345 static bool bTipPopped(false);
346 static CPoint ptPrev(-1,-1);
348 if (point != ptPrev) {
349 ptPrev = point;
351 if (bTipPopped) {
352 bTipPopped = false;
353 nReturn = -1;
355 else {
356 ::Sleep(50);
357 bTipPopped = true;
359 pTI->hwnd = m_hWnd;
360 pTI->uId = (UINT_PTR) m_hWnd;
361 pTI->lpszText = LPSTR_TEXTCALLBACK;
363 CRect rcWnd;
364 GetClientRect(&rcWnd);
365 pTI->rect = rcWnd;
366 nReturn = 1;
369 else {
370 nReturn = 1;
373 MyGraph::SpinTheMessageLoop();
375 return nReturn;
378 // Build the tip text for the part of the graph that the mouse is currently
379 // over.
380 CString MyGraph::GetTipText() const
382 VALIDATE;
384 CString sTip("");
386 // Get the position of the mouse.
387 CPoint pt;
388 VERIFY(::GetCursorPos(&pt));
389 ScreenToClient(&pt);
391 // Ask each part of the graph to check and see if the mouse is over it.
392 if (m_rcLegend.PtInRect(pt)) {
393 sTip = "Legend";
395 else if (m_rcTitle.PtInRect(pt)) {
396 sTip = "Title";
398 else {
399 int maxXAxis = m_ptOrigin.x + (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2));
400 if (pt.x >= m_ptOrigin.x && pt.x <= maxXAxis) {
401 int average = GetAverageDataValue();
402 int nMaxDataValue = max(GetMaxDataValue(), 1);
403 double barTop = m_ptOrigin.y - (double)m_nYAxisHeight *
404 (average / (double)nMaxDataValue);
405 if (pt.y >= barTop - 2 && pt.y <= barTop + 2) {
406 sTip.Format(_T("Average: %d %s (%d%%)"), average, m_sYAxisLabel, nMaxDataValue ? (100 * average / nMaxDataValue) : 0);
407 return sTip;
411 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
413 while (pos && sTip.IsEmpty()) {
414 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
415 ASSERT_VALID(pSeries);
417 int nGroup(0);
419 nGroup = pSeries->HitTest(pt,nGroup);
421 if (-1 != nGroup) {
422 if("" != sTip){
423 sTip += _T(", ");
425 sTip += m_saLegendLabels.GetAt(nGroup) + _T(": ");
426 sTip += pSeries->GetTipText(nGroup, m_sYAxisLabel);
427 nGroup++;
429 }while(-1 != nGroup);
433 return sTip;
436 // Handle WM_PAINT.
437 void MyGraph::OnPaint()
439 VALIDATE;
441 CBufferDC dc(this);
442 DrawGraph(dc);
445 // Handle WM_SIZE.
446 void MyGraph::OnSize(UINT nType, int cx, int cy)
448 VALIDATE;
450 CStatic::OnSize(nType, cx, cy);
452 Invalidate();
455 // Change the type of the graph; the caller should call Invalidate() on this
456 // window to make the effect of this change visible.
457 void MyGraph::SetGraphType(GraphType e, bool bStackedGraph)
459 VALIDATE;
461 m_eGraphType = e;
462 m_bStackedGraph = bStackedGraph;
465 // Calculate the current max legend label length in pixels.
466 int MyGraph::GetMaxLegendLabelLength(CDC& dc) const
468 VALIDATE;
469 ASSERT_VALID(&dc);
471 CString sMax;
472 int nMaxChars(-1);
473 CSize siz(-1,-1);
475 // First get max number of characters.
476 for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {
477 int nLabelLength(m_saLegendLabels.GetAt(nGroup).GetLength());
479 if (nMaxChars < nLabelLength) {
480 nMaxChars = nLabelLength;
481 sMax = m_saLegendLabels.GetAt(nGroup);
485 // Now calculate the pixels.
486 siz = dc.GetTextExtent(sMax);
488 _ASSERTE(-1 < siz.cx);
490 return siz.cx;
493 // Returns the largest number of data points in any series.
494 int MyGraph::GetMaxSeriesSize() const
496 VALIDATE;
498 int nMax(0);
499 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
501 while (pos) {
502 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
503 ASSERT_VALID(pSeries);
505 nMax = max(nMax, (int)pSeries->m_dwaValues.GetSize());
508 return nMax;
511 // Returns the largest number of non-zero data points in any series.
512 int MyGraph::GetMaxNonZeroSeriesSize() const
514 VALIDATE;
516 int nMax(0);
517 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
519 while (pos) {
520 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
521 ASSERT_VALID(pSeries);
523 nMax = max(nMax, pSeries->GetNonZeroElementCount());
526 return nMax;
529 // Get the largest data value in all series.
530 int MyGraph::GetMaxDataValue() const
532 VALIDATE;
534 int nMax(0);
535 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
537 while (pos) {
538 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
539 ASSERT_VALID(pSeries);
541 nMax = max(nMax, pSeries->GetMaxDataValue(m_bStackedGraph));
544 return nMax;
547 // Get the average data value in all series.
548 int MyGraph::GetAverageDataValue() const
550 VALIDATE;
552 int nTotal = 0, nCount = 0;
553 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
555 while (pos) {
556 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
557 ASSERT_VALID(pSeries);
559 nTotal += pSeries->GetAverageDataValue();
560 ++nCount;
563 if (nCount == 0)
564 return 0;
566 return nTotal / nCount;
569 // How many series are populated?
570 int MyGraph::GetNonZeroSeriesCount() const
572 VALIDATE;
574 int nCount(0);
575 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
577 while (pos) {
578 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
579 ASSERT_VALID(pSeries);
581 if (0 < pSeries->GetNonZeroElementCount()) {
582 ++nCount;
586 return nCount ? nCount : 1;
589 // Returns the group number for the sent label; -1 if not found.
590 int MyGraph::LookupLabel(const CString& sLabel) const
592 VALIDATE;
593 _ASSERTE(! sLabel.IsEmpty());
595 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((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) {
655 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
656 ASSERT_VALID(pSeries);
658 if (nGroup >= pSeries->m_dwaValues.GetSize()) {
659 pSeries->m_dwaValues.SetAtGrow(nGroup, 0);
663 return nGroup;
666 // Set this value to the legend.
667 void MyGraph::SetLegend(int nGroup, const CString& sLabel)
669 VALIDATE;
670 _ASSERTE(0 <= nGroup);
672 m_saLegendLabels.SetAtGrow(nGroup, sLabel);
676 void MyGraph::SetGraphTitle(const CString& sTitle)
678 VALIDATE;
679 _ASSERTE(! sTitle.IsEmpty());
681 m_sTitle = sTitle;
685 void MyGraph::DrawGraph(CDC& dc)
687 VALIDATE;
688 ASSERT_VALID(&dc);
690 if (GetMaxSeriesSize()) {
691 dc.SetBkMode(TRANSPARENT);
693 // Populate the colors as a group of evenly spaced colors of maximum
694 // saturation.
695 int nColorsDelta(240 / GetMaxSeriesSize());
697 int baseColorL = 120;
698 int diffColorL = 60;
699 DWORD backgroundColor = ::GetSysColor(COLOR_WINDOW);
700 // If graph is a non-stacked line graph, use darker colors if system window color is light.
701 #if 0
702 if (m_eGraphType == MyGraph::Line && !m_bStackedGraph) {
703 int backgroundLuma = (GetRValue(backgroundColor) + GetGValue(backgroundColor) + GetBValue(backgroundColor)) / 3;
704 if (backgroundLuma > 128) {
705 baseColorL = 70;
706 diffColorL = 50;
709 #endif
710 for (WORD nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup) {
711 WORD colorH = (WORD)(nColorsDelta * nGroup);
712 WORD colorL = (WORD)(baseColorL+(diffColorL*(nGroup%2)));
713 WORD colorS = (WORD)(180)+(30*((1-nGroup%2)*(nGroup%3)));
714 COLORREF cr(MyGraph::HLStoRGB(colorH, colorL, colorS)); // Populate colors cleverly
715 m_dwaColors.SetAtGrow(nGroup, cr);
718 // Reduce the graphable area by the frame window and status bar. We will
719 // leave GAP_PIXELS pixels blank on all sides of the graph. So top-left
720 // side of graph is at GAP_PIXELS,GAP_PIXELS and the bottom-right side
721 // of graph is at (m_rcGraph.Height() - GAP_PIXELS), (m_rcGraph.Width() -
722 // GAP_PIXELS). These settings are altered by axis labels and legends.
723 CRect rcWnd;
724 GetClientRect(&rcWnd);
725 m_rcGraph.left = GAP_PIXELS;
726 m_rcGraph.top = GAP_PIXELS;
727 m_rcGraph.right = rcWnd.Width() - GAP_PIXELS;
728 m_rcGraph.bottom = rcWnd.Height() - GAP_PIXELS;
730 CBrush br;
731 VERIFY(br.CreateSolidBrush(backgroundColor));
732 dc.FillRect(rcWnd, &br);
733 br.DeleteObject();
735 // Draw graph title.
736 DrawTitle(dc);
738 // Set the axes and origin values.
739 SetupAxes(dc);
741 // Draw legend if there is one and there's enough space.
742 if (m_saLegendLabels.GetSize() && m_rcGraph.right-m_rcGraph.left > LEGEND_VISIBILITY_THRESHOLD) {
743 DrawLegend(dc);
745 else{
746 m_rcLegend.SetRectEmpty();
749 // Draw axes unless it's a pie.
750 if (m_eGraphType != MyGraph::PieChart) {
751 DrawAxes(dc);
754 // Draw series data and labels.
755 switch (m_eGraphType) {
756 case MyGraph::Bar: DrawSeriesBar(dc); break;
757 case MyGraph::Line: if (m_bStackedGraph) DrawSeriesLineStacked(dc); else DrawSeriesLine(dc); break;
758 case MyGraph::PieChart: DrawSeriesPie(dc); break;
759 default: _ASSERTE(! "Bad default case"); break;
764 // Draw graph title; size is proportionate to width.
765 void MyGraph::DrawTitle(CDC& dc)
767 VALIDATE;
768 ASSERT_VALID(&dc);
770 // Create the title font.
771 CFont fontTitle;
772 VERIFY(fontTitle.CreatePointFont(max(m_rcGraph.Width() / TITLE_DIVISOR, MIN_FONT_SIZE),
773 _T("Arial"), &dc));
774 CFont* pFontOld = dc.SelectObject(&fontTitle);
775 ASSERT_VALID(pFontOld);
777 // Draw the title.
778 m_rcTitle.SetRect(GAP_PIXELS, GAP_PIXELS, m_rcGraph.Width() + GAP_PIXELS,
779 m_rcGraph.Height() + GAP_PIXELS);
781 dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE |
782 DT_TOP | DT_CALCRECT);
784 m_rcTitle.right = m_rcGraph.Width() + GAP_PIXELS;
786 dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE |
787 DT_TOP);
789 VERIFY(dc.SelectObject(pFontOld));
790 fontTitle.DeleteObject();
793 // Set the axes and origin values.
794 void MyGraph::SetupAxes(CDC& dc)
796 VALIDATE;
797 ASSERT_VALID(&dc);
799 // Since pie has no axis lines, set to full size minus GAP_PIXELS on each
800 // side. These are needed for legend to plot itself.
801 if (MyGraph::PieChart == m_eGraphType) {
802 m_nXAxisWidth = m_rcGraph.Width() - (GAP_PIXELS * 2);
803 m_nYAxisHeight = m_rcGraph.Height() - m_rcTitle.bottom;
804 m_ptOrigin.x = GAP_PIXELS;
805 m_ptOrigin.y = m_rcGraph.Height() - GAP_PIXELS;
807 else {
808 // Bar and Line graphs.
810 // Need to find out how wide the biggest Y-axis tick label is
812 // Get and store height of axis label font.
813 m_nAxisLabelHeight = max(m_rcGraph.Height() / Y_AXIS_LABEL_DIVISOR, MIN_FONT_SIZE);
814 // Get and store height of tick label font.
815 m_nAxisTickLabelHeight = max(int(m_nAxisLabelHeight*0.8), MIN_FONT_SIZE);
817 CFont fontTickLabels;
818 VERIFY(fontTickLabels.CreatePointFont(m_nAxisTickLabelHeight, _T("Arial"), &dc));
819 // Select font and store the old.
820 CFont* pFontOld = dc.SelectObject(&fontTickLabels);
821 ASSERT_VALID(pFontOld);
823 // Obtain tick label dimensions.
824 CString sTickLabel;
825 sTickLabel.Format(_T("%d"), GetMaxDataValue());
826 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));
828 // Set old font object again and delete temporary font object.
829 VERIFY(dc.SelectObject(pFontOld));
830 fontTickLabels.DeleteObject();
832 // Determine axis specifications.
833 m_ptOrigin.x = m_rcGraph.left + m_nAxisLabelHeight/10 + 2*GAP_PIXELS
834 + sizTickLabel.cx + GAP_PIXELS + TICK_PIXELS;
835 m_ptOrigin.y = m_rcGraph.bottom - m_nAxisLabelHeight/10 - 2*GAP_PIXELS -
836 sizTickLabel.cy - GAP_PIXELS - TICK_PIXELS;
837 m_nYAxisHeight = m_ptOrigin.y - m_rcTitle.bottom - (2 * GAP_PIXELS);
838 m_nXAxisWidth = (m_rcGraph.Width() - GAP_PIXELS) - m_ptOrigin.x;
843 void MyGraph::DrawLegend(CDC& dc)
845 VALIDATE;
846 ASSERT_VALID(&dc);
848 // Create the legend font.
849 CFont fontLegend;
850 int pointFontHeight = max(m_rcGraph.Height() / LEGEND_DIVISOR, MIN_FONT_SIZE);
851 VERIFY(fontLegend.CreatePointFont(pointFontHeight, _T("Arial"), &dc));
853 // Get the height of each label.
854 LOGFONT lf;
855 ::SecureZeroMemory(&lf, sizeof(lf));
856 VERIFY(fontLegend.GetLogFont(&lf));
857 int nLabelHeight(abs(lf.lfHeight));
859 // Get number of legend entries
860 int nLegendEntries = max(1, GetMaxSeriesSize());
862 // Calculate optimal label height = AvailableLegendHeight/AllAuthors
863 // Use a buffer of (GAP_PIXELS / 2) on each side inside the legend, and in addition the same
864 // gab above and below the legend frame, so in total 2*GAP_PIXELS
865 double optimalLabelHeight = double(m_rcGraph.Height() - 2*GAP_PIXELS)/nLegendEntries;
867 // Now relate the LabelHeight to the PointFontHeight
868 int optimalPointFontHeight = int(pointFontHeight*optimalLabelHeight/nLabelHeight);
870 // Limit the optimal PointFontHeight to the available range
871 optimalPointFontHeight = min( max(optimalPointFontHeight, MIN_FONT_SIZE), pointFontHeight);
873 // If the optimalPointFontHeight is different from the initial one, create a new legend font
874 if (optimalPointFontHeight != pointFontHeight) {
875 fontLegend.DeleteObject();
876 VERIFY(fontLegend.CreatePointFont(optimalPointFontHeight, _T("Arial"), &dc));
877 VERIFY(fontLegend.GetLogFont(&lf));
878 nLabelHeight = abs(lf.lfHeight);
881 // Calculate maximum number of authors that can be shown with the current label height
882 int nShownAuthors = (m_rcGraph.Height() - 2*GAP_PIXELS)/nLabelHeight - 1;
883 // Fix rounding errors.
884 if (nShownAuthors+1 == GetMaxSeriesSize())
885 ++nShownAuthors;
887 // Get number of authors to be shown.
888 nShownAuthors = min(nShownAuthors, GetMaxSeriesSize());
889 // nShownAuthors contains now the number of authors
891 CFont* pFontOld = dc.SelectObject(&fontLegend);
892 ASSERT_VALID(pFontOld);
894 // Determine actual size of legend. A buffer of (GAP_PIXELS / 2) on each side,
895 // plus the height of each label based on the pint size of the font.
896 int nLegendHeight = (GAP_PIXELS / 2) + (nShownAuthors * nLabelHeight) + (GAP_PIXELS / 2);
897 // Draw the legend border. Allow LEGEND_COLOR_BAR_PIXELS pixels for
898 // display of label bars.
899 m_rcLegend.top = (m_rcGraph.Height() - nLegendHeight) / 2;
900 m_rcLegend.bottom = m_rcLegend.top + nLegendHeight;
901 m_rcLegend.right = m_rcGraph.Width() - GAP_PIXELS;
902 m_rcLegend.left = m_rcLegend.right - GetMaxLegendLabelLength(dc) -
903 LEGEND_COLOR_BAR_WIDTH_PIXELS;
904 VERIFY(dc.Rectangle(m_rcLegend));
906 int skipped_row = -1; // if != -1, this is the row that we show the ... in
907 if (nShownAuthors < GetMaxSeriesSize())
908 skipped_row = nShownAuthors-2;
909 // Draw each group's label and bar.
910 for (int nGroup = 0; nGroup < nShownAuthors; ++nGroup) {
912 int nLabelTop(m_rcLegend.top + (nGroup * nLabelHeight) +
913 (GAP_PIXELS / 2));
915 int nShownGroup = nGroup; // introduce helper variable to avoid code duplication
917 // Do we have a skipped row?
918 if (skipped_row != -1)
920 if (nGroup == skipped_row) {
921 // draw the dots
922 VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop, _T("...") ));
923 continue;
925 if (nGroup == nShownAuthors-1) {
926 // we show the last group instead of the scheduled group
927 nShownGroup = GetMaxSeriesSize()-1;
930 // Draw the label.
931 VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop,
932 m_saLegendLabels.GetAt(nShownGroup)));
934 // Determine the bar.
935 CRect rcBar;
936 rcBar.left = m_rcLegend.left + GAP_PIXELS + GetMaxLegendLabelLength(dc) + GAP_PIXELS;
937 rcBar.top = nLabelTop + LEGEND_COLOR_BAR_GAP_PIXELS;
938 rcBar.right = m_rcLegend.right - GAP_PIXELS;
939 rcBar.bottom = rcBar.top + nLabelHeight - LEGEND_COLOR_BAR_GAP_PIXELS;
940 VERIFY(dc.Rectangle(rcBar));
942 // Draw bar for group.
943 COLORREF crBar(m_dwaColors.GetAt(nShownGroup));
944 CBrush br(crBar);
946 CBrush* pBrushOld = dc.SelectObject(&br);
947 ASSERT_VALID(pBrushOld);
949 rcBar.DeflateRect(LEGEND_COLOR_BAR_GAP_PIXELS, LEGEND_COLOR_BAR_GAP_PIXELS);
950 dc.FillRect(rcBar, &br);
952 dc.SelectObject(pBrushOld);
953 br.DeleteObject();
956 VERIFY(dc.SelectObject(pFontOld));
957 fontLegend.DeleteObject();
961 void MyGraph::DrawAxes(CDC& dc) const
963 VALIDATE;
964 ASSERT_VALID(&dc);
965 _ASSERTE(MyGraph::PieChart != m_eGraphType);
967 dc.SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));
969 // Draw y axis.
970 dc.MoveTo(m_ptOrigin);
971 VERIFY(dc.LineTo(m_ptOrigin.x, m_ptOrigin.y - m_nYAxisHeight));
973 // Draw x axis.
974 dc.MoveTo(m_ptOrigin);
976 if (m_saLegendLabels.GetSize()) {
978 VERIFY(dc.LineTo(m_ptOrigin.x +
979 (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)),
980 m_ptOrigin.y));
982 else {
983 VERIFY(dc.LineTo(m_ptOrigin.x + m_nXAxisWidth, m_ptOrigin.y));
986 // Note: m_nAxisLabelHeight and m_nAxisTickLabelHeight have been calculated in SetupAxis()
988 // Create the x-axis label font.
989 CFont fontXAxis;
990 VERIFY(fontXAxis.CreatePointFont(m_nAxisLabelHeight, _T("Arial"), &dc));
992 // Obtain the height of the font in device coordinates.
993 LOGFONT pLF;
994 VERIFY(fontXAxis.GetLogFont(&pLF));
995 int fontHeightDC = pLF.lfHeight;
997 // Create the y-axis label font.
998 CFont fontYAxis;
999 VERIFY(fontYAxis.CreateFont(
1000 /* nHeight */ fontHeightDC,
1001 /* nWidth */ 0,
1002 /* nEscapement */ 90 * 10,
1003 /* nOrientation */ 0,
1004 /* nWeight */ FW_DONTCARE,
1005 /* bItalic */ false,
1006 /* bUnderline */ false,
1007 /* cStrikeOut */ 0,
1008 ANSI_CHARSET,
1009 OUT_DEFAULT_PRECIS,
1010 CLIP_DEFAULT_PRECIS,
1011 PROOF_QUALITY,
1012 VARIABLE_PITCH | FF_DONTCARE,
1013 _T("Arial"))
1016 // Set the y-axis label font and draw the label.
1017 CFont* pFontOld = dc.SelectObject(&fontYAxis);
1018 ASSERT_VALID(pFontOld);
1019 CSize sizYLabel(dc.GetTextExtent(m_sYAxisLabel));
1020 VERIFY(dc.TextOut(GAP_PIXELS, (m_rcGraph.Height() + sizYLabel.cx) / 2,
1021 m_sYAxisLabel));
1023 // Set the x-axis label font and draw the label.
1024 VERIFY(dc.SelectObject(&fontXAxis));
1025 CSize sizXLabel(dc.GetTextExtent(m_sXAxisLabel));
1026 VERIFY(dc.TextOut(m_ptOrigin.x + (m_nXAxisWidth - sizXLabel.cx) / 2,
1027 m_rcGraph.bottom - GAP_PIXELS - sizXLabel.cy, m_sXAxisLabel));
1029 // chose suitable tick step (1, 2, 5, 10, 20, 50, etc.)
1030 int nMaxDataValue(GetMaxDataValue());
1031 nMaxDataValue = max(nMaxDataValue, 1);
1032 int nTickStep = 1;
1033 while (10 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
1034 nTickStep *= 10;
1036 if (5 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
1037 nTickStep *= 5;
1038 if (2 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
1039 nTickStep *= 2;
1041 // We hardwire TITLE_DIVISOR y-axis ticks here for simplicity.
1042 int nTickCount(nMaxDataValue / nTickStep);
1043 double tickSpace = (double)m_nYAxisHeight * nTickStep / (double)nMaxDataValue;
1045 // create tick label font and set it in the device context
1046 CFont fontTickLabels;
1047 VERIFY(fontTickLabels.CreatePointFont(m_nAxisTickLabelHeight, _T("Arial"), &dc));
1048 VERIFY(dc.SelectObject(&fontTickLabels));
1050 for (int nTick = 0; nTick < nTickCount; ++nTick)
1052 int nTickYLocation = static_cast<int>(m_ptOrigin.y - tickSpace * (nTick + 1) + 0.5);
1053 dc.MoveTo(m_ptOrigin.x - TICK_PIXELS, nTickYLocation);
1054 VERIFY(dc.LineTo(m_ptOrigin.x + TICK_PIXELS, nTickYLocation));
1056 // Draw tick label.
1057 CString sTickLabel;
1058 sTickLabel.Format(_T("%d"), nTickStep * (nTick+1));
1059 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));
1061 VERIFY(dc.TextOut(m_ptOrigin.x - GAP_PIXELS - sizTickLabel.cx - TICK_PIXELS,
1062 nTickYLocation - sizTickLabel.cy/2, sTickLabel));
1065 // Draw X axis tick marks.
1066 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1067 int nSeries(0);
1069 while (pos) {
1071 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1072 ASSERT_VALID(pSeries);
1074 // Ignore unpopulated series if bar chart.
1075 if (m_eGraphType != MyGraph::Bar ||
1076 0 < pSeries->GetNonZeroElementCount()) {
1078 // Get the spacing of the series.
1079 int nSeriesSpace(0);
1081 if (m_saLegendLabels.GetSize()) {
1083 nSeriesSpace =
1084 (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
1085 (m_eGraphType == MyGraph::Bar ?
1086 GetNonZeroSeriesCount() : (int)m_olMyGraphSeries.GetCount());
1088 else {
1089 nSeriesSpace = m_nXAxisWidth / (m_eGraphType == MyGraph::Bar ?
1090 GetNonZeroSeriesCount() : (int)m_olMyGraphSeries.GetCount());
1093 int nTickXLocation(m_ptOrigin.x + ((nSeries + 1) * nSeriesSpace) -
1094 (nSeriesSpace / 2));
1096 dc.MoveTo(nTickXLocation, m_ptOrigin.y - TICK_PIXELS);
1097 VERIFY(dc.LineTo(nTickXLocation, m_ptOrigin.y + TICK_PIXELS));
1099 // Draw x-axis tick label.
1100 CString sTickLabel(pSeries->GetLabel());
1101 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));
1103 VERIFY(dc.TextOut(nTickXLocation - (sizTickLabel.cx / 2),
1104 m_ptOrigin.y + TICK_PIXELS + GAP_PIXELS, sTickLabel));
1106 ++nSeries;
1110 VERIFY(dc.SelectObject(pFontOld));
1111 fontXAxis.DeleteObject();
1112 fontYAxis.DeleteObject();
1113 fontTickLabels.DeleteObject();
1117 void MyGraph::DrawSeriesBar(CDC& dc) const
1119 VALIDATE;
1120 ASSERT_VALID(&dc);
1122 // How much space does each series get (includes inter series space)?
1123 // We ignore series whose members are all zero.
1124 double availableSpace = m_saLegendLabels.GetSize()
1125 ? m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)
1126 : m_nXAxisWidth;
1128 double seriesSpace = availableSpace / (double)GetNonZeroSeriesCount();
1130 // Determine width of bars. Data points with a value of zero are assumed
1131 // to be empty. This is a bad assumption.
1132 double barWidth(0.0);
1134 // This is the width of the largest series (no inter series space).
1135 double maxSeriesPlotSize(0.0);
1137 if(!m_bStackedGraph){
1138 int seriessize = GetMaxNonZeroSeriesSize();
1139 barWidth = seriessize ? seriesSpace / seriessize : 0;
1140 if (1 < GetNonZeroSeriesCount()) {
1141 barWidth *= INTERSERIES_PERCENT_USED;
1143 maxSeriesPlotSize = GetMaxNonZeroSeriesSize() * barWidth;
1145 else{
1146 barWidth = seriesSpace * INTERSERIES_PERCENT_USED;
1147 maxSeriesPlotSize = barWidth;
1150 // Iterate the series.
1151 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1152 int nSeries(0);
1154 while (pos) {
1156 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1157 ASSERT_VALID(pSeries);
1159 // Ignore unpopulated series.
1160 if (0 < pSeries->GetNonZeroElementCount()) {
1162 // Draw each bar; empty bars are not drawn.
1163 double runningLeft(m_ptOrigin.x + (nSeries + 1) * seriesSpace -
1164 maxSeriesPlotSize);
1166 double stackAccumulator(0.0);
1168 for (int nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup) {
1170 if (pSeries->GetData(nGroup)) {
1172 int nMaxDataValue(GetMaxDataValue());
1173 nMaxDataValue = max(nMaxDataValue, 1);
1174 double barTop = m_ptOrigin.y - (double)m_nYAxisHeight *
1175 pSeries->GetData(nGroup) / (double)nMaxDataValue - stackAccumulator;
1177 CRect rcBar;
1178 rcBar.left = (int)runningLeft;
1179 rcBar.top = (int)barTop;
1180 // Make adjacent bar borders overlap, so there's only one pixel border line between them.
1181 rcBar.right = (int)(runningLeft + barWidth) + 1;
1182 rcBar.bottom = (int)((double)m_ptOrigin.y - stackAccumulator) + 1;
1184 if(m_bStackedGraph){
1185 stackAccumulator = (double)m_ptOrigin.y - barTop;
1188 pSeries->SetTipRegion(nGroup, rcBar);
1190 COLORREF crBar(m_dwaColors.GetAt(nGroup));
1191 CBrush br(crBar);
1192 CBrush* pBrushOld = dc.SelectObject(&br);
1193 ASSERT_VALID(pBrushOld);
1195 VERIFY(dc.Rectangle(rcBar));
1196 dc.SelectObject(pBrushOld);
1197 br.DeleteObject();
1199 if(!m_bStackedGraph){
1200 runningLeft += barWidth;
1205 ++nSeries;
1209 if (!m_bStackedGraph) {
1210 int nMaxDataValue = max(GetMaxDataValue(), 1);
1211 double barTop = m_ptOrigin.y - (double)m_nYAxisHeight *
1212 (GetAverageDataValue() / (double)nMaxDataValue);
1213 dc.MoveTo(m_ptOrigin.x, barTop);
1214 VERIFY(dc.LineTo(m_ptOrigin.x + (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)), barTop));
1219 void MyGraph::DrawSeriesLine(CDC& dc) const
1221 VALIDATE;
1222 ASSERT_VALID(&dc);
1223 _ASSERTE(!m_bStackedGraph);
1225 // Iterate the groups.
1226 CPoint ptLastLoc(0,0);
1227 int dataLastLoc(0);
1229 for (int nGroup = 0; nGroup < GetMaxSeriesSize(); nGroup++) {
1231 // How much space does each series get (includes inter series space)?
1232 int nSeriesSpace(0);
1234 if (m_saLegendLabels.GetSize()) {
1236 nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
1237 (int)m_olMyGraphSeries.GetCount();
1239 else {
1240 nSeriesSpace = m_nXAxisWidth / (int)m_olMyGraphSeries.GetCount();
1243 // Determine width of bars.
1244 int nMaxSeriesSize(GetMaxSeriesSize());
1245 nMaxSeriesSize = max(nMaxSeriesSize, 1);
1246 int nBarWidth(nSeriesSpace / nMaxSeriesSize);
1248 if (1 < m_olMyGraphSeries.GetCount()) {
1249 nBarWidth = (int) ((double) nBarWidth * INTERSERIES_PERCENT_USED);
1252 // This is the width of the largest series (no inter series space).
1253 //int nMaxSeriesPlotSize(GetMaxSeriesSize() * nBarWidth);
1255 // Iterate the series.
1256 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1258 // Build objects.
1259 COLORREF crLine(m_dwaColors.GetAt(nGroup));
1260 CBrush br(crLine);
1261 CBrush* pBrushOld = dc.SelectObject(&br);
1262 ASSERT_VALID(pBrushOld);
1263 CPen penLine(PS_SOLID, 1, crLine);
1264 CPen* pPenOld = dc.SelectObject(&penLine);
1265 ASSERT_VALID(pPenOld);
1267 for (int nSeries = 0; nSeries < m_olMyGraphSeries.GetCount(); ++nSeries) {
1269 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1270 ASSERT_VALID(pSeries);
1272 // Get x and y location of center of ellipse.
1273 CPoint ptLoc(0,0);
1275 ptLoc.x = m_ptOrigin.x + (((nSeries + 1) * nSeriesSpace) -
1276 (nSeriesSpace / 2));
1278 int nMaxDataValue(GetMaxDataValue());
1279 nMaxDataValue = max(nMaxDataValue, 1);
1280 double dLineHeight(pSeries->GetData(nGroup) * m_nYAxisHeight /
1281 double(nMaxDataValue));
1283 ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);
1286 // Draw line back to last data member.
1287 if (nSeries > 0 && (pSeries->GetData(nGroup)!=0 || dataLastLoc != 0)) {
1289 dc.MoveTo(ptLastLoc.x, ptLastLoc.y - 1);
1290 VERIFY(dc.LineTo(ptLoc.x - 1, ptLoc.y - 1));
1293 // Now draw ellipse.
1294 CRect rcEllipse(ptLoc.x - 3, ptLoc.y - 3, ptLoc.x + 3, ptLoc.y + 3);
1295 if(pSeries->GetData(nGroup)!=0){
1296 VERIFY(dc.Ellipse(rcEllipse));
1298 if (m_olMyGraphSeries.GetCount() < 40)
1300 pSeries->SetTipRegion(nGroup, rcEllipse);
1303 // Save last pt and data
1304 ptLastLoc = ptLoc;
1305 dataLastLoc = pSeries->GetData(nGroup);
1307 VERIFY(dc.SelectObject(pPenOld));
1308 penLine.DeleteObject();
1309 VERIFY(dc.SelectObject(pBrushOld));
1310 br.DeleteObject();
1313 int nMaxDataValue = max(GetMaxDataValue(), 1);
1314 double barTop = m_ptOrigin.y - (double)m_nYAxisHeight *
1315 (GetAverageDataValue() / (double)nMaxDataValue);
1316 dc.MoveTo(m_ptOrigin.x, barTop);
1317 VERIFY(dc.LineTo(m_ptOrigin.x + (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)), barTop));
1321 void MyGraph::DrawSeriesLineStacked(CDC& dc) const
1323 VALIDATE;
1324 ASSERT_VALID(&dc);
1325 _ASSERTE(m_bStackedGraph);
1327 int nSeriesCount = (int)m_olMyGraphSeries.GetCount();
1329 CArray<int> stackAccumulator;
1330 stackAccumulator.SetSize(nSeriesCount);
1332 CArray<CPoint> polygon;
1333 // Special case: if we only have single series, make polygon
1334 // a bar instead of one pixel line.
1335 polygon.SetSize(nSeriesCount==1 ? 4 : nSeriesCount * 2);
1337 // How much space does each series get?
1338 int nSeriesSpace(0);
1339 if (m_saLegendLabels.GetSize()) {
1340 nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
1341 nSeriesCount;
1343 else {
1344 nSeriesSpace = m_nXAxisWidth / nSeriesCount;
1347 int nMaxDataValue(GetMaxDataValue());
1348 nMaxDataValue = max(nMaxDataValue, 1);
1349 double dYScaling = double(m_nYAxisHeight) / nMaxDataValue;
1351 // Iterate the groups.
1352 for (int nGroup = 0; nGroup < GetMaxSeriesSize(); nGroup++) {
1354 // Build objects.
1355 COLORREF crGroup(m_dwaColors.GetAt(nGroup));
1356 CBrush br(crGroup);
1357 CBrush* pBrushOld = dc.SelectObject(&br);
1358 ASSERT_VALID(pBrushOld);
1359 // For polygon outline, use average of this and previous color, and darken it.
1360 COLORREF crPrevGroup(nGroup > 0 ? m_dwaColors.GetAt(nGroup-1) : crGroup);
1361 COLORREF crOutline = RGB(
1362 (GetRValue(crGroup)+GetRValue(crPrevGroup))/3,
1363 (GetGValue(crGroup)+GetGValue(crPrevGroup))/3,
1364 (GetBValue(crGroup)+GetBValue(crPrevGroup))/3);
1365 CPen penLine(PS_SOLID, 1, crOutline);
1366 CPen* pPenOld = dc.SelectObject(&penLine);
1367 ASSERT_VALID(pPenOld);
1369 // Construct bottom part of polygon from current stack accumulator
1370 for (int nPolyBottom = 0; nPolyBottom < nSeriesCount; ++nPolyBottom) {
1371 CPoint ptLoc;
1372 ptLoc.x = m_ptOrigin.x + (((nPolyBottom + 1) * nSeriesSpace) - (nSeriesSpace / 2));
1373 double dLineHeight((stackAccumulator[nPolyBottom]) * dYScaling);
1374 ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);
1376 if (nSeriesCount > 1) {
1377 polygon[nSeriesCount-nPolyBottom-1] = ptLoc;
1378 } else {
1379 // special case: when there's one series, make polygon a bar
1380 polygon[0] = CPoint(ptLoc.x-GAP_PIXELS/2, ptLoc.y);
1381 polygon[1] = CPoint(ptLoc.x+GAP_PIXELS/2, ptLoc.y);
1385 // Iterate the series, construct upper part of polygon and upadte stack accumulator
1386 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1387 for (int nSeries = 0; nSeries < nSeriesCount; ++nSeries) {
1389 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1390 ASSERT_VALID(pSeries);
1392 CPoint ptLoc;
1393 ptLoc.x = m_ptOrigin.x + (((nSeries + 1) * nSeriesSpace) -
1394 (nSeriesSpace / 2));
1395 double dLineHeight((pSeries->GetData(nGroup) + stackAccumulator[nSeries]) * dYScaling);
1396 ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);
1397 if (nSeriesCount > 1) {
1398 polygon[nSeriesCount+nSeries] = ptLoc;
1399 } else {
1400 // special case: when there's one series, make polygon a bar
1401 polygon[2] = CPoint(ptLoc.x+GAP_PIXELS/2, ptLoc.y);
1402 polygon[3] = CPoint(ptLoc.x-GAP_PIXELS/2, ptLoc.y);
1405 stackAccumulator[nSeries] += pSeries->GetData(nGroup);
1408 // Draw polygon
1409 VERIFY(dc.Polygon(polygon.GetData(), (int)polygon.GetSize()));
1411 VERIFY(dc.SelectObject(pPenOld));
1412 penLine.DeleteObject();
1413 VERIFY(dc.SelectObject(pBrushOld));
1414 br.DeleteObject();
1419 void MyGraph::DrawSeriesPie(CDC& dc) const
1421 VALIDATE;
1422 ASSERT_VALID(&dc);
1424 // Determine width of pie display area (pie and space).
1425 int nSeriesSpace(0);
1427 int seriesCount = GetNonZeroSeriesCount();
1428 int horizontalSpace(0);
1430 if (m_saLegendLabels.GetSize()) {
1431 // With legend box.
1433 horizontalSpace = m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2);
1434 int nPieAndSpaceWidth(horizontalSpace / (seriesCount ? seriesCount : 1));
1436 // Height is limiting factor.
1437 if (nPieAndSpaceWidth > m_nYAxisHeight - (GAP_PIXELS * 2)) {
1438 nSeriesSpace = (m_nYAxisHeight - (GAP_PIXELS * 2));
1440 else {
1441 // Width is limiting factor.
1442 nSeriesSpace = nPieAndSpaceWidth;
1445 else {
1446 // No legend box.
1448 horizontalSpace = m_nXAxisWidth;
1450 // Height is limiting factor.
1451 if (m_nXAxisWidth > m_nYAxisHeight * (seriesCount ? seriesCount : 1)) {
1452 nSeriesSpace = m_nYAxisHeight;
1454 else {
1455 // Width is limiting factor.
1456 nSeriesSpace = m_nXAxisWidth / (seriesCount ? seriesCount : 1);
1460 // Make pies be centered horizontally
1461 int xOrigin = m_ptOrigin.x + GAP_PIXELS + (horizontalSpace - nSeriesSpace * seriesCount) / 2;
1463 // Create font for labels.
1464 CFont fontLabels;
1465 int pointFontHeight = max(m_rcGraph.Height() / Y_AXIS_LABEL_DIVISOR, MIN_FONT_SIZE);
1466 VERIFY(fontLabels.CreatePointFont(pointFontHeight, _T("Arial"), &dc));
1467 CFont* pFontOld = dc.SelectObject(&fontLabels);
1468 ASSERT_VALID(pFontOld);
1470 // Draw each pie.
1471 int nPie(0);
1472 int nRadius((int) (nSeriesSpace * INTERSERIES_PERCENT_USED / 2.0));
1473 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1475 while (pos) {
1477 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1478 ASSERT_VALID(pSeries);
1480 // Don't leave a space for empty pies.
1481 if (0 < pSeries->GetNonZeroElementCount()) {
1483 // Locate this pie.
1484 CPoint ptCenter;
1485 ptCenter.x = xOrigin + (nSeriesSpace * nPie) + nSeriesSpace / 2;
1486 ptCenter.y = m_ptOrigin.y - m_nYAxisHeight / 2;
1488 CRect rcPie;
1489 rcPie.left = ptCenter.x - nRadius;
1490 rcPie.right = ptCenter.x + nRadius;
1491 rcPie.top = ptCenter.y - nRadius;
1492 rcPie.bottom = ptCenter.y + nRadius;
1494 // Draw series label.
1495 CSize sizPieLabel(dc.GetTextExtent(pSeries->GetLabel()));
1497 VERIFY(dc.TextOut(ptCenter.x - (sizPieLabel.cx / 2),
1498 ptCenter.y + nRadius + GAP_PIXELS, pSeries->GetLabel()));
1500 // How much do the wedges total to?
1501 double dPieTotal(pSeries->GetDataTotal());
1503 // Draw each wedge in this pie.
1504 CPoint ptStart(rcPie.left, ptCenter.y);
1505 double dRunningWedgeTotal(0.0);
1507 for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {
1509 // Ignore empty wedges.
1510 if (0 < pSeries->GetData(nGroup)) {
1512 // Get the degrees of this wedge.
1513 dRunningWedgeTotal += pSeries->GetData(nGroup);
1514 double dPercent(dRunningWedgeTotal * 100.0 / dPieTotal);
1515 double degrees(360.0 * dPercent / 100.0);
1517 // Find the location of the wedge's endpoint.
1518 CPoint ptEnd(WedgeEndFromDegrees(degrees, ptCenter, nRadius));
1520 // Special case: a wedge that takes up the whole pie would
1521 // otherwise be confused with an empty wedge.
1522 bool drawEmptyWedges = false;
1523 if (1 == pSeries->GetNonZeroElementCount()) {
1524 _ASSERTE(360 == (int)degrees && ptStart == ptEnd && "This is the problem we're correcting");
1525 --ptEnd.y;
1526 drawEmptyWedges = true;
1529 // If the wedge is zero size or very narrow, don't paint it.
1530 // If pie is small, and wedge data is small, we might get a wedges
1531 // where center and both endpoints lie on the same coordinate,
1532 // and endpoints differ only in one pixel. GDI draws such pie as whole pie,
1533 // so we just skip them instead.
1534 int distance = abs(ptStart.x-ptEnd.x) + abs(ptStart.y-ptEnd.y);
1535 if (drawEmptyWedges || distance > 1) {
1537 // Draw wedge.
1538 COLORREF crWedge(m_dwaColors.GetAt(nGroup));
1539 CBrush br(crWedge);
1540 CBrush* pBrushOld = dc.SelectObject(&br);
1541 ASSERT_VALID(pBrushOld);
1542 VERIFY(dc.Pie(rcPie, ptStart, ptEnd));
1544 // Create a region from the path we create.
1545 VERIFY(dc.BeginPath());
1546 VERIFY(dc.Pie(rcPie, ptStart, ptEnd));
1547 VERIFY(dc.EndPath());
1548 std::unique_ptr<CRgn> prgnWedge (new CRgn);
1549 VERIFY(prgnWedge->CreateFromPath(&dc));
1550 pSeries->SetTipRegion(nGroup, prgnWedge.release());
1552 // Cleanup.
1553 dc.SelectObject(pBrushOld);
1554 br.DeleteObject();
1555 ptStart = ptEnd;
1560 ++nPie;
1564 // Draw X axis title
1565 CSize sizXLabel(dc.GetTextExtent(m_sXAxisLabel));
1566 VERIFY(dc.TextOut(xOrigin + (nSeriesSpace * nPie - sizXLabel.cx)/2,
1567 m_ptOrigin.y - m_nYAxisHeight/2 + nRadius + GAP_PIXELS*2 + sizXLabel.cy, m_sXAxisLabel));
1569 VERIFY(dc.SelectObject(pFontOld));
1570 fontLabels.DeleteObject();
1573 // Convert degrees to x and y coords.
1574 CPoint MyGraph::WedgeEndFromDegrees(double degrees, const CPoint& ptCenter,
1575 double radius) const
1577 VALIDATE;
1579 CPoint pt;
1581 double radians = degrees / 360.0 * PI * 2.0;
1583 pt.x = (int) (radius * cos(radians));
1584 pt.x = ptCenter.x - pt.x;
1586 pt.y = (int) (radius * sin(radians));
1587 pt.y = ptCenter.y + pt.y;
1589 return pt;
1592 // Spin The Message Loop: C++ version. See "Advanced Windows Programming",
1593 // M. Heller, p. 153, and the MS TechNet CD, PSS ID Number: Q99999.
1594 /* static */ UINT MyGraph::SpinTheMessageLoop(bool bNoDrawing /* = false */ ,
1595 bool bOnlyDrawing /* = false */ ,
1596 UINT uiMsgAllowed /* = WM_NULL */ )
1598 MSG msg;
1599 ::SecureZeroMemory(&msg, sizeof(msg));
1601 while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
1603 // Do painting only.
1604 if (bOnlyDrawing && WM_PAINT == msg.message) {
1605 ::TranslateMessage(&msg);
1606 ::DispatchMessage(&msg);
1608 // Update user interface.
1609 AfxGetApp()->OnIdle(0);
1611 // Do everything *but* painting.
1612 else if (bNoDrawing && WM_PAINT == msg.message) {
1613 break;
1615 // Special handling for this message.
1616 else if (WM_QUIT == msg.message) {
1617 ::PostQuitMessage(static_cast<int>(msg.wParam));
1618 break;
1620 // Allow one message (like WM_LBUTTONDOWN).
1621 else if (uiMsgAllowed == msg.message
1622 && ! AfxGetApp()->PreTranslateMessage(&msg)) {
1623 ::TranslateMessage(&msg);
1624 ::DispatchMessage(&msg);
1625 break;
1627 // This is the general case.
1628 else if (! bOnlyDrawing && ! AfxGetApp()->PreTranslateMessage(&msg)) {
1629 ::TranslateMessage(&msg);
1630 ::DispatchMessage(&msg);
1632 // Update user interface, then free temporary objects.
1633 AfxGetApp()->OnIdle(0);
1634 AfxGetApp()->OnIdle(1);
1638 return msg.message;
1642 /////////////////////////////////////////////////////////////////////////////
1643 // Conversion routines: RGB to HLS (Red-Green-Blue to Hue-Luminosity-Saturation).
1644 // See Microsoft KnowledgeBase article Q29240.
1646 #define HLSMAX 240 // H,L, and S vary over 0-HLSMAX
1647 #define RGBMAX 255 // R,G, and B vary over 0-RGBMAX
1648 // HLSMAX BEST IF DIVISIBLE BY 6
1649 // RGBMAX, HLSMAX must each fit in a byte (255).
1651 #define UNDEFINED (HLSMAX * 2 / 3) // Hue is undefined if Saturation is 0
1652 // (grey-scale). This value determines
1653 // where the Hue scrollbar is initially
1654 // set for achromatic colors.
1657 // Convert HLS to RGB.
1658 /* static */ COLORREF MyGraph::HLStoRGB(WORD wH, WORD wL, WORD wS)
1660 _ASSERTE(0 == wH && 240 >= wH && "Illegal hue value");
1661 _ASSERTE(0 == wL && 240 >= wL && "Illegal lum value");
1662 _ASSERTE(0 == wS && 240 >= wS && "Illegal sat value");
1664 WORD wR(0);
1665 WORD wG(0);
1666 WORD wB(0);
1668 // Achromatic case.
1669 if (0 == wS) {
1670 wR = wG = wB = (wL * RGBMAX) / HLSMAX;
1672 if (UNDEFINED != wH) {
1673 _ASSERTE(! "ERROR");
1676 else {
1677 // Chromatic case.
1678 WORD Magic1(0);
1679 WORD Magic2(0);
1681 // Set up magic numbers.
1682 if (wL <= HLSMAX / 2) {
1683 Magic2 = (wL * (HLSMAX + wS) + (HLSMAX / 2)) / HLSMAX;
1685 else {
1686 Magic2 = wL + wS - ((wL * wS) + (HLSMAX / 2)) / HLSMAX;
1689 Magic1 = 2 * wL - Magic2;
1691 // Get RGB, change units from HLSMAX to RGBMAX.
1692 wR = (HueToRGB(Magic1, Magic2, wH + (HLSMAX / 3)) * RGBMAX + (HLSMAX / 2)) / HLSMAX;
1693 wG = (HueToRGB(Magic1, Magic2, wH) * RGBMAX + (HLSMAX / 2)) / HLSMAX;
1694 wB = (HueToRGB(Magic1, Magic2, wH - (HLSMAX / 3)) * RGBMAX + (HLSMAX / 2)) / HLSMAX;
1697 return RGB(wR,wG,wB);
1700 // Utility routine for HLStoRGB.
1701 /* static */ WORD MyGraph::HueToRGB(WORD w1, WORD w2, WORD wH)
1703 // Range check: note values passed add/subtract thirds of range.
1704 if (wH > HLSMAX) {
1705 wH -= HLSMAX;
1708 // Return r, g, or b value from this tridrant.
1709 if (wH < HLSMAX / 6) {
1710 return w1 + (((w2 - w1) * wH + (HLSMAX / 12)) / (HLSMAX / 6));
1713 if (wH < HLSMAX / 2) {
1714 return w2;
1717 if (wH < (HLSMAX * 2) / 3) {
1718 return w1 + (((w2 - w1) * (((HLSMAX * 2) / 3) - wH) + (HLSMAX / 12)) / (HLSMAX / 6));
1720 else {
1721 return w1;