Prevent possible division by zero
[TortoiseGit.git] / src / Utils / MiscUI / MyGraph.cpp
blobfd35c56f13c71464c2540c0c1ded44a88bacd67b
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 CRgn* prgnNew = new CRgn;
94 ASSERT_VALID(prgnNew);
96 VERIFY(prgnNew->CreateRectRgnIndirect(rc));
97 SetTipRegion(nGroup, prgnNew);
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 if (prgnOld) {
117 delete prgnOld;
118 prgnOld = NULL;
121 // Add the new region.
122 m_oaRegions.SetAtGrow(nGroup, prgn);
124 _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());
128 CString MyGraphSeries::GetLabel() const
130 VALIDATE;
132 return m_sLabel;
136 int MyGraphSeries::GetData(int nGroup) const
138 VALIDATE;
139 _ASSERTE(0 <= nGroup);
140 _ASSERTE(m_dwaValues.GetSize() > nGroup);
142 return m_dwaValues[nGroup];
145 // Returns the largest data value in this series.
146 int MyGraphSeries::GetMaxDataValue(bool bStackedGraph) const
148 VALIDATE;
150 int nMax(0);
152 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
153 if(!bStackedGraph){
154 nMax = max(nMax, static_cast<int> (m_dwaValues.GetAt(nGroup)));
156 else{
157 nMax += static_cast<int> (m_dwaValues.GetAt(nGroup));
161 return nMax;
164 // Returns the average data value in this series.
165 int MyGraphSeries::GetAverageDataValue() const
167 VALIDATE;
169 int nTotal = 0;
171 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
172 nTotal += static_cast<int> (m_dwaValues.GetAt(nGroup));
175 if (m_dwaValues.GetSize() == 0)
176 return 0;
178 return nTotal / m_dwaValues.GetSize();
181 // Returns the number of data points that are not zero.
182 int MyGraphSeries::GetNonZeroElementCount() const
184 VALIDATE;
186 int nCount(0);
188 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
190 if (m_dwaValues.GetAt(nGroup)) {
191 ++nCount;
195 return nCount;
198 // Returns the sum of the data points for this series.
199 int MyGraphSeries::GetDataTotal() const
201 VALIDATE;
203 int nTotal(0);
205 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
206 nTotal += m_dwaValues.GetAt(nGroup);
209 return nTotal;
212 // Returns which group (if any) the sent point lies within in this series.
213 int MyGraphSeries::HitTest(const CPoint& pt, int searchStart = 0) const
215 VALIDATE;
217 for (int nGroup = searchStart; nGroup < m_oaRegions.GetSize(); ++nGroup) {
218 CRgn* prgnData = m_oaRegions.GetAt(nGroup);
219 ASSERT_NULL_OR_POINTER(prgnData, CRgn);
221 if (prgnData && prgnData->PtInRegion(pt)) {
222 return nGroup;
226 return -1;
229 // Get the series portion of the tip for this group in this series.
230 CString MyGraphSeries::GetTipText(int nGroup, const CString &unitString) const
232 VALIDATE;
233 _ASSERTE(0 <= nGroup);
234 _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());
236 CString sTip;
238 sTip.Format(_T("%d %s (%d%%)"), m_dwaValues.GetAt(nGroup),
239 (LPCTSTR)unitString,
240 GetDataTotal() ? (int) (100.0 * (double) m_dwaValues.GetAt(nGroup) /
241 (double) GetDataTotal()) : 0);
243 return sTip;
247 /////////////////////////////////////////////////////////////////////////////
248 // MyGraph
250 // Constructor.
251 MyGraph::MyGraph(GraphType eGraphType /* = MyGraph::Pie */ , bool bStackedGraph /* = false */)
252 : m_nXAxisWidth(0)
253 , m_nYAxisHeight(0)
254 , m_nAxisLabelHeight(0)
255 , m_nAxisTickLabelHeight(0)
256 , m_eGraphType(eGraphType)
257 , m_bStackedGraph(bStackedGraph)
259 m_ptOrigin.x = m_ptOrigin.y = 0;
260 m_rcGraph.SetRectEmpty();
261 m_rcLegend.SetRectEmpty();
262 m_rcTitle.SetRectEmpty();
265 // Destructor.
266 /* virtual */ MyGraph::~MyGraph()
270 BEGIN_MESSAGE_MAP(MyGraph, CStatic)
271 //{{AFX_MSG_MAP(MyGraph)
272 ON_WM_PAINT()
273 ON_WM_SIZE()
274 //}}AFX_MSG_MAP
275 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnNeedText)
276 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnNeedText)
277 END_MESSAGE_MAP()
279 // Called by the framework to allow other necessary sub classing to occur
280 // before the window is sub classed.
281 void MyGraph::PreSubclassWindow()
283 VALIDATE;
285 CStatic::PreSubclassWindow();
287 VERIFY(EnableToolTips(true));
291 /////////////////////////////////////////////////////////////////////////////
292 // MyGraph message handlers
294 // Handle the tooltip messages. Returns true to mean message was handled.
295 BOOL MyGraph::OnNeedText(UINT /*uiId*/, NMHDR* pNMHDR, LRESULT* pResult)
297 _ASSERTE(pNMHDR && "Bad parameter passed");
298 _ASSERTE(pResult && "Bad parameter passed");
300 bool bReturn(false);
301 UINT_PTR uiID(pNMHDR->idFrom);
303 // Notification in NT from automatically created tooltip.
304 if (0U != uiID) {
305 bReturn = true;
307 // Need to handle both ANSI and UNICODE versions of the message.
308 TOOLTIPTEXTA* pTTTA = reinterpret_cast<TOOLTIPTEXTA*> (pNMHDR);
309 ASSERT_POINTER(pTTTA, TOOLTIPTEXTA);
311 TOOLTIPTEXTW* pTTTW = reinterpret_cast<TOOLTIPTEXTW*> (pNMHDR);
312 ASSERT_POINTER(pTTTW, TOOLTIPTEXTW);
314 CString sTipText(GetTipText());
316 #ifndef _UNICODE
317 if (TTN_NEEDTEXTA == pNMHDR->code) {
318 lstrcpyn(pTTTA->szText, sTipText, _countof(pTTTA->szText));
320 else {
321 _mbstowcsz(pTTTW->szText, sTipText, _countof(pTTTA->szText));
323 #else
324 if (pNMHDR->code == TTN_NEEDTEXTA) {
325 _wcstombsz(pTTTA->szText, sTipText, _countof(pTTTA->szText));
327 else {
328 lstrcpyn(pTTTW->szText, sTipText, _countof(pTTTA->szText));
330 #endif
332 *pResult = 0;
335 return bReturn;
338 // The framework calls this member function to determine whether a point is in
339 // the bounding rectangle of the specified tool.
340 INT_PTR MyGraph::OnToolHitTest(CPoint point, TOOLINFO* pTI) const
342 _ASSERTE(pTI && "Bad parameter passed");
344 // This works around the problem of the tip remaining visible when you move
345 // the mouse to various positions over this control.
346 INT_PTR nReturn(0);
347 static bool bTipPopped(false);
348 static CPoint ptPrev(-1,-1);
350 if (point != ptPrev) {
351 ptPrev = point;
353 if (bTipPopped) {
354 bTipPopped = false;
355 nReturn = -1;
357 else {
358 ::Sleep(50);
359 bTipPopped = true;
361 pTI->hwnd = m_hWnd;
362 pTI->uId = (UINT_PTR) m_hWnd;
363 pTI->lpszText = LPSTR_TEXTCALLBACK;
365 CRect rcWnd;
366 GetClientRect(&rcWnd);
367 pTI->rect = rcWnd;
368 nReturn = 1;
371 else {
372 nReturn = 1;
375 MyGraph::SpinTheMessageLoop();
377 return nReturn;
380 // Build the tip text for the part of the graph that the mouse is currently
381 // over.
382 CString MyGraph::GetTipText() const
384 VALIDATE;
386 CString sTip("");
388 // Get the position of the mouse.
389 CPoint pt;
390 VERIFY(::GetCursorPos(&pt));
391 ScreenToClient(&pt);
393 // Ask each part of the graph to check and see if the mouse is over it.
394 if (m_rcLegend.PtInRect(pt)) {
395 sTip = "Legend";
397 else if (m_rcTitle.PtInRect(pt)) {
398 sTip = "Title";
400 else {
401 int maxXAxis = m_ptOrigin.x + (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2));
402 if (pt.x >= m_ptOrigin.x && pt.x <= maxXAxis) {
403 int average = GetAverageDataValue();
404 int nMaxDataValue = max(GetMaxDataValue(), 1);
405 double barTop = m_ptOrigin.y - (double)m_nYAxisHeight *
406 (average / (double)nMaxDataValue);
407 if (pt.y >= barTop - 2 && pt.y <= barTop + 2) {
408 sTip.Format(_T("Average: %d %s (%d%%)"), average, 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){
425 sTip += _T(", ");
427 sTip += m_saLegendLabels.GetAt(nGroup) + _T(": ");
428 sTip += pSeries->GetTipText(nGroup, m_sYAxisLabel);
429 nGroup++;
431 }while(-1 != nGroup);
435 return sTip;
438 // Handle WM_PAINT.
439 void MyGraph::OnPaint()
441 VALIDATE;
443 CBufferDC dc(this);
444 DrawGraph(dc);
447 // Handle WM_SIZE.
448 void MyGraph::OnSize(UINT nType, int cx, int cy)
450 VALIDATE;
452 CStatic::OnSize(nType, cx, cy);
454 Invalidate();
457 // Change the type of the graph; the caller should call Invalidate() on this
458 // window to make the effect of this change visible.
459 void MyGraph::SetGraphType(GraphType e, bool bStackedGraph)
461 VALIDATE;
463 m_eGraphType = e;
464 m_bStackedGraph = bStackedGraph;
467 // Calculate the current max legend label length in pixels.
468 int MyGraph::GetMaxLegendLabelLength(CDC& dc) const
470 VALIDATE;
471 ASSERT_VALID(&dc);
473 CString sMax;
474 int nMaxChars(-1);
475 CSize siz(-1,-1);
477 // First get max number of characters.
478 for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {
479 int nLabelLength(m_saLegendLabels.GetAt(nGroup).GetLength());
481 if (nMaxChars < nLabelLength) {
482 nMaxChars = nLabelLength;
483 sMax = m_saLegendLabels.GetAt(nGroup);
487 // Now calculate the pixels.
488 siz = dc.GetTextExtent(sMax);
490 _ASSERTE(-1 < siz.cx);
492 return siz.cx;
495 // Returns the largest number of data points in any series.
496 int MyGraph::GetMaxSeriesSize() const
498 VALIDATE;
500 int nMax(0);
501 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
503 while (pos) {
504 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
505 ASSERT_VALID(pSeries);
507 nMax = max(nMax, (int)pSeries->m_dwaValues.GetSize());
510 return nMax;
513 // Returns the largest number of non-zero data points in any series.
514 int MyGraph::GetMaxNonZeroSeriesSize() const
516 VALIDATE;
518 int nMax(0);
519 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
521 while (pos) {
522 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
523 ASSERT_VALID(pSeries);
525 nMax = max(nMax, pSeries->GetNonZeroElementCount());
528 return nMax;
531 // Get the largest data value in all series.
532 int MyGraph::GetMaxDataValue() const
534 VALIDATE;
536 int nMax(0);
537 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
539 while (pos) {
540 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
541 ASSERT_VALID(pSeries);
543 nMax = max(nMax, pSeries->GetMaxDataValue(m_bStackedGraph));
546 return nMax;
549 // Get the average data value in all series.
550 int MyGraph::GetAverageDataValue() const
552 VALIDATE;
554 int nTotal = 0, nCount = 0;
555 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
557 while (pos) {
558 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
559 ASSERT_VALID(pSeries);
561 nTotal += pSeries->GetAverageDataValue();
562 ++nCount;
565 if (nCount == 0)
566 return 0;
568 return nTotal / nCount;
571 // How many series are populated?
572 int MyGraph::GetNonZeroSeriesCount() const
574 VALIDATE;
576 int nCount(0);
577 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
579 while (pos) {
580 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
581 ASSERT_VALID(pSeries);
583 if (0 < pSeries->GetNonZeroElementCount()) {
584 ++nCount;
588 return nCount ? nCount : 1;
591 // Returns the group number for the sent label; -1 if not found.
592 int MyGraph::LookupLabel(const CString& sLabel) const
594 VALIDATE;
595 _ASSERTE(! sLabel.IsEmpty());
597 for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {
599 if (0 == sLabel.CompareNoCase(m_saLegendLabels.GetAt(nGroup))) {
600 return nGroup;
604 return -1;
607 void MyGraph::Clear()
609 m_dwaColors.RemoveAll();
610 m_saLegendLabels.RemoveAll();
611 m_olMyGraphSeries.RemoveAll();
615 void MyGraph::AddSeries(MyGraphSeries& rMyGraphSeries)
617 VALIDATE;
618 ASSERT_VALID(&rMyGraphSeries);
619 _ASSERTE(m_saLegendLabels.GetSize() == rMyGraphSeries.m_dwaValues.GetSize());
621 m_olMyGraphSeries.AddTail(&rMyGraphSeries);
625 void MyGraph::SetXAxisLabel(const CString& sLabel)
627 VALIDATE;
628 _ASSERTE(! sLabel.IsEmpty());
630 m_sXAxisLabel = sLabel;
634 void MyGraph::SetYAxisLabel(const CString& sLabel)
636 VALIDATE;
637 _ASSERTE(! sLabel.IsEmpty());
639 m_sYAxisLabel = sLabel;
642 // Returns the group number added. Also, makes sure that all the series have
643 // this many elements.
644 int MyGraph::AppendGroup(const CString& sLabel)
646 VALIDATE;
648 // Add the group.
649 int nGroup((int)m_saLegendLabels.GetSize());
650 SetLegend(nGroup, sLabel);
652 // Make sure that all series have this element.
653 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
655 while (pos) {
657 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
658 ASSERT_VALID(pSeries);
660 if (nGroup >= pSeries->m_dwaValues.GetSize()) {
661 pSeries->m_dwaValues.SetAtGrow(nGroup, 0);
665 return nGroup;
668 // Set this value to the legend.
669 void MyGraph::SetLegend(int nGroup, const CString& sLabel)
671 VALIDATE;
672 _ASSERTE(0 <= nGroup);
674 m_saLegendLabels.SetAtGrow(nGroup, sLabel);
678 void MyGraph::SetGraphTitle(const CString& sTitle)
680 VALIDATE;
681 _ASSERTE(! sTitle.IsEmpty());
683 m_sTitle = sTitle;
687 void MyGraph::DrawGraph(CDC& dc)
689 VALIDATE;
690 ASSERT_VALID(&dc);
692 if (GetMaxSeriesSize()) {
693 dc.SetBkMode(TRANSPARENT);
695 // Populate the colors as a group of evenly spaced colors of maximum
696 // saturation.
697 int nColorsDelta(240 / GetMaxSeriesSize());
699 int baseColorL = 120;
700 int diffColorL = 60;
701 DWORD backgroundColor = ::GetSysColor(COLOR_WINDOW);
702 // If graph is a non-stacked line graph, use darker colors if system window color is light.
703 #if 0
704 if (m_eGraphType == MyGraph::Line && !m_bStackedGraph) {
705 int backgroundLuma = (GetRValue(backgroundColor) + GetGValue(backgroundColor) + GetBValue(backgroundColor)) / 3;
706 if (backgroundLuma > 128) {
707 baseColorL = 70;
708 diffColorL = 50;
711 #endif
712 for (WORD nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup) {
713 WORD colorH = (WORD)(nColorsDelta * nGroup);
714 WORD colorL = (WORD)(baseColorL+(diffColorL*(nGroup%2)));
715 WORD colorS = (WORD)(180)+(30*((1-nGroup%2)*(nGroup%3)));
716 COLORREF cr(MyGraph::HLStoRGB(colorH, colorL, colorS)); // Populate colors cleverly
717 m_dwaColors.SetAtGrow(nGroup, cr);
720 // Reduce the graphable area by the frame window and status bar. We will
721 // leave GAP_PIXELS pixels blank on all sides of the graph. So top-left
722 // side of graph is at GAP_PIXELS,GAP_PIXELS and the bottom-right side
723 // of graph is at (m_rcGraph.Height() - GAP_PIXELS), (m_rcGraph.Width() -
724 // GAP_PIXELS). These settings are altered by axis labels and legends.
725 CRect rcWnd;
726 GetClientRect(&rcWnd);
727 m_rcGraph.left = GAP_PIXELS;
728 m_rcGraph.top = GAP_PIXELS;
729 m_rcGraph.right = rcWnd.Width() - GAP_PIXELS;
730 m_rcGraph.bottom = rcWnd.Height() - GAP_PIXELS;
732 CBrush br;
733 VERIFY(br.CreateSolidBrush(backgroundColor));
734 dc.FillRect(rcWnd, &br);
735 br.DeleteObject();
737 // Draw graph title.
738 DrawTitle(dc);
740 // Set the axes and origin values.
741 SetupAxes(dc);
743 // Draw legend if there is one and there's enough space.
744 if (m_saLegendLabels.GetSize() && m_rcGraph.right-m_rcGraph.left > LEGEND_VISIBILITY_THRESHOLD) {
745 DrawLegend(dc);
747 else{
748 m_rcLegend.SetRectEmpty();
751 // Draw axes unless it's a pie.
752 if (m_eGraphType != MyGraph::PieChart) {
753 DrawAxes(dc);
756 // Draw series data and labels.
757 switch (m_eGraphType) {
758 case MyGraph::Bar: DrawSeriesBar(dc); break;
759 case MyGraph::Line: if (m_bStackedGraph) DrawSeriesLineStacked(dc); else DrawSeriesLine(dc); break;
760 case MyGraph::PieChart: DrawSeriesPie(dc); break;
761 default: _ASSERTE(! "Bad default case"); break;
766 // Draw graph title; size is proportionate to width.
767 void MyGraph::DrawTitle(CDC& dc)
769 VALIDATE;
770 ASSERT_VALID(&dc);
772 // Create the title font.
773 CFont fontTitle;
774 VERIFY(fontTitle.CreatePointFont(max(m_rcGraph.Width() / TITLE_DIVISOR, MIN_FONT_SIZE),
775 _T("Arial"), &dc));
776 CFont* pFontOld = dc.SelectObject(&fontTitle);
777 ASSERT_VALID(pFontOld);
779 // Draw the title.
780 m_rcTitle.SetRect(GAP_PIXELS, GAP_PIXELS, m_rcGraph.Width() + GAP_PIXELS,
781 m_rcGraph.Height() + GAP_PIXELS);
783 dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE |
784 DT_TOP | DT_CALCRECT);
786 m_rcTitle.right = m_rcGraph.Width() + GAP_PIXELS;
788 dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE |
789 DT_TOP);
791 VERIFY(dc.SelectObject(pFontOld));
792 fontTitle.DeleteObject();
795 // Set the axes and origin values.
796 void MyGraph::SetupAxes(CDC& dc)
798 VALIDATE;
799 ASSERT_VALID(&dc);
801 // Since pie has no axis lines, set to full size minus GAP_PIXELS on each
802 // side. These are needed for legend to plot itself.
803 if (MyGraph::PieChart == m_eGraphType) {
804 m_nXAxisWidth = m_rcGraph.Width() - (GAP_PIXELS * 2);
805 m_nYAxisHeight = m_rcGraph.Height() - m_rcTitle.bottom;
806 m_ptOrigin.x = GAP_PIXELS;
807 m_ptOrigin.y = m_rcGraph.Height() - GAP_PIXELS;
809 else {
810 // Bar and Line graphs.
812 // Need to find out how wide the biggest Y-axis tick label is
814 // Get and store height of axis label font.
815 m_nAxisLabelHeight = max(m_rcGraph.Height() / Y_AXIS_LABEL_DIVISOR, MIN_FONT_SIZE);
816 // Get and store height of tick label font.
817 m_nAxisTickLabelHeight = max(int(m_nAxisLabelHeight*0.8), MIN_FONT_SIZE);
819 CFont fontTickLabels;
820 VERIFY(fontTickLabels.CreatePointFont(m_nAxisTickLabelHeight, _T("Arial"), &dc));
821 // Select font and store the old.
822 CFont* pFontOld = dc.SelectObject(&fontTickLabels);
823 ASSERT_VALID(pFontOld);
825 // Obtain tick label dimensions.
826 CString sTickLabel;
827 sTickLabel.Format(_T("%d"), GetMaxDataValue());
828 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));
830 // Set old font object again and delete temporary font object.
831 VERIFY(dc.SelectObject(pFontOld));
832 fontTickLabels.DeleteObject();
834 // Determine axis specifications.
835 m_ptOrigin.x = m_rcGraph.left + m_nAxisLabelHeight/10 + 2*GAP_PIXELS
836 + sizTickLabel.cx + GAP_PIXELS + TICK_PIXELS;
837 m_ptOrigin.y = m_rcGraph.bottom - m_nAxisLabelHeight/10 - 2*GAP_PIXELS -
838 sizTickLabel.cy - GAP_PIXELS - TICK_PIXELS;
839 m_nYAxisHeight = m_ptOrigin.y - m_rcTitle.bottom - (2 * GAP_PIXELS);
840 m_nXAxisWidth = (m_rcGraph.Width() - GAP_PIXELS) - m_ptOrigin.x;
845 void MyGraph::DrawLegend(CDC& dc)
847 VALIDATE;
848 ASSERT_VALID(&dc);
850 // Create the legend font.
851 CFont fontLegend;
852 int pointFontHeight = max(m_rcGraph.Height() / LEGEND_DIVISOR, MIN_FONT_SIZE);
853 VERIFY(fontLegend.CreatePointFont(pointFontHeight, _T("Arial"), &dc));
855 // Get the height of each label.
856 LOGFONT lf;
857 ::SecureZeroMemory(&lf, sizeof(lf));
858 VERIFY(fontLegend.GetLogFont(&lf));
859 int nLabelHeight(abs(lf.lfHeight));
861 // Get number of legend entries
862 int nLegendEntries = max(1, GetMaxSeriesSize());
864 // Calculate optimal label height = AvailableLegendHeight/AllAuthors
865 // Use a buffer of (GAP_PIXELS / 2) on each side inside the legend, and in addition the same
866 // gab above and below the legend frame, so in total 2*GAP_PIXELS
867 double optimalLabelHeight = double(m_rcGraph.Height() - 2*GAP_PIXELS)/nLegendEntries;
869 // Now relate the LabelHeight to the PointFontHeight
870 int optimalPointFontHeight = int(pointFontHeight*optimalLabelHeight/nLabelHeight);
872 // Limit the optimal PointFontHeight to the available range
873 optimalPointFontHeight = min( max(optimalPointFontHeight, MIN_FONT_SIZE), pointFontHeight);
875 // If the optimalPointFontHeight is different from the initial one, create a new legend font
876 if (optimalPointFontHeight != pointFontHeight) {
877 fontLegend.DeleteObject();
878 VERIFY(fontLegend.CreatePointFont(optimalPointFontHeight, _T("Arial"), &dc));
879 VERIFY(fontLegend.GetLogFont(&lf));
880 nLabelHeight = abs(lf.lfHeight);
883 // Calculate maximum number of authors that can be shown with the current label height
884 int nShownAuthors = (m_rcGraph.Height() - 2*GAP_PIXELS)/nLabelHeight - 1;
885 // Fix rounding errors.
886 if (nShownAuthors+1 == GetMaxSeriesSize())
887 ++nShownAuthors;
889 // Get number of authors to be shown.
890 nShownAuthors = min(nShownAuthors, GetMaxSeriesSize());
891 // nShownAuthors contains now the number of authors
893 CFont* pFontOld = dc.SelectObject(&fontLegend);
894 ASSERT_VALID(pFontOld);
896 // Determine actual size of legend. A buffer of (GAP_PIXELS / 2) on each side,
897 // plus the height of each label based on the pint size of the font.
898 int nLegendHeight = (GAP_PIXELS / 2) + (nShownAuthors * nLabelHeight) + (GAP_PIXELS / 2);
899 // Draw the legend border. Allow LEGEND_COLOR_BAR_PIXELS pixels for
900 // display of label bars.
901 m_rcLegend.top = (m_rcGraph.Height() - nLegendHeight) / 2;
902 m_rcLegend.bottom = m_rcLegend.top + nLegendHeight;
903 m_rcLegend.right = m_rcGraph.Width() - GAP_PIXELS;
904 m_rcLegend.left = m_rcLegend.right - GetMaxLegendLabelLength(dc) -
905 LEGEND_COLOR_BAR_WIDTH_PIXELS;
906 VERIFY(dc.Rectangle(m_rcLegend));
908 int skipped_row = -1; // if != -1, this is the row that we show the ... in
909 if (nShownAuthors < GetMaxSeriesSize())
910 skipped_row = nShownAuthors-2;
911 // Draw each group's label and bar.
912 for (int nGroup = 0; nGroup < nShownAuthors; ++nGroup) {
914 int nLabelTop(m_rcLegend.top + (nGroup * nLabelHeight) +
915 (GAP_PIXELS / 2));
917 int nShownGroup = nGroup; // introduce helper variable to avoid code duplication
919 // Do we have a skipped row?
920 if (skipped_row != -1)
922 if (nGroup == skipped_row) {
923 // draw the dots
924 VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop, _T("...") ));
925 continue;
927 if (nGroup == nShownAuthors-1) {
928 // we show the last group instead of the scheduled group
929 nShownGroup = GetMaxSeriesSize()-1;
932 // Draw the label.
933 VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop,
934 m_saLegendLabels.GetAt(nShownGroup)));
936 // Determine the bar.
937 CRect rcBar;
938 rcBar.left = m_rcLegend.left + GAP_PIXELS + GetMaxLegendLabelLength(dc) + GAP_PIXELS;
939 rcBar.top = nLabelTop + LEGEND_COLOR_BAR_GAP_PIXELS;
940 rcBar.right = m_rcLegend.right - GAP_PIXELS;
941 rcBar.bottom = rcBar.top + nLabelHeight - LEGEND_COLOR_BAR_GAP_PIXELS;
942 VERIFY(dc.Rectangle(rcBar));
944 // Draw bar for group.
945 COLORREF crBar(m_dwaColors.GetAt(nShownGroup));
946 CBrush br(crBar);
948 CBrush* pBrushOld = dc.SelectObject(&br);
949 ASSERT_VALID(pBrushOld);
951 rcBar.DeflateRect(LEGEND_COLOR_BAR_GAP_PIXELS, LEGEND_COLOR_BAR_GAP_PIXELS);
952 dc.FillRect(rcBar, &br);
954 dc.SelectObject(pBrushOld);
955 br.DeleteObject();
958 VERIFY(dc.SelectObject(pFontOld));
959 fontLegend.DeleteObject();
963 void MyGraph::DrawAxes(CDC& dc) const
965 VALIDATE;
966 ASSERT_VALID(&dc);
967 _ASSERTE(MyGraph::PieChart != m_eGraphType);
969 dc.SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));
971 // Draw y axis.
972 dc.MoveTo(m_ptOrigin);
973 VERIFY(dc.LineTo(m_ptOrigin.x, m_ptOrigin.y - m_nYAxisHeight));
975 // Draw x axis.
976 dc.MoveTo(m_ptOrigin);
978 if (m_saLegendLabels.GetSize()) {
980 VERIFY(dc.LineTo(m_ptOrigin.x +
981 (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)),
982 m_ptOrigin.y));
984 else {
985 VERIFY(dc.LineTo(m_ptOrigin.x + m_nXAxisWidth, m_ptOrigin.y));
988 // Note: m_nAxisLabelHeight and m_nAxisTickLabelHeight have been calculated in SetupAxis()
990 // Create the x-axis label font.
991 CFont fontXAxis;
992 VERIFY(fontXAxis.CreatePointFont(m_nAxisLabelHeight, _T("Arial"), &dc));
994 // Obtain the height of the font in device coordinates.
995 LOGFONT pLF;
996 VERIFY(fontXAxis.GetLogFont(&pLF));
997 int fontHeightDC = pLF.lfHeight;
999 // Create the y-axis label font.
1000 CFont fontYAxis;
1001 VERIFY(fontYAxis.CreateFont(
1002 /* nHeight */ fontHeightDC,
1003 /* nWidth */ 0,
1004 /* nEscapement */ 90 * 10,
1005 /* nOrientation */ 0,
1006 /* nWeight */ FW_DONTCARE,
1007 /* bItalic */ false,
1008 /* bUnderline */ false,
1009 /* cStrikeOut */ 0,
1010 ANSI_CHARSET,
1011 OUT_DEFAULT_PRECIS,
1012 CLIP_DEFAULT_PRECIS,
1013 PROOF_QUALITY,
1014 VARIABLE_PITCH | FF_DONTCARE,
1015 _T("Arial"))
1018 // Set the y-axis label font and draw the label.
1019 CFont* pFontOld = dc.SelectObject(&fontYAxis);
1020 ASSERT_VALID(pFontOld);
1021 CSize sizYLabel(dc.GetTextExtent(m_sYAxisLabel));
1022 VERIFY(dc.TextOut(GAP_PIXELS, (m_rcGraph.Height() + sizYLabel.cx) / 2,
1023 m_sYAxisLabel));
1025 // Set the x-axis label font and draw the label.
1026 VERIFY(dc.SelectObject(&fontXAxis));
1027 CSize sizXLabel(dc.GetTextExtent(m_sXAxisLabel));
1028 VERIFY(dc.TextOut(m_ptOrigin.x + (m_nXAxisWidth - sizXLabel.cx) / 2,
1029 m_rcGraph.bottom - GAP_PIXELS - sizXLabel.cy, m_sXAxisLabel));
1031 // chose suitable tick step (1, 2, 5, 10, 20, 50, etc.)
1032 int nMaxDataValue(GetMaxDataValue());
1033 nMaxDataValue = max(nMaxDataValue, 1);
1034 int nTickStep = 1;
1035 while (10 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
1036 nTickStep *= 10;
1038 if (5 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
1039 nTickStep *= 5;
1040 if (2 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
1041 nTickStep *= 2;
1043 // We hardwire TITLE_DIVISOR y-axis ticks here for simplicity.
1044 int nTickCount(nMaxDataValue / nTickStep);
1045 double tickSpace = (double)m_nYAxisHeight * nTickStep / (double)nMaxDataValue;
1047 // create tick label font and set it in the device context
1048 CFont fontTickLabels;
1049 VERIFY(fontTickLabels.CreatePointFont(m_nAxisTickLabelHeight, _T("Arial"), &dc));
1050 VERIFY(dc.SelectObject(&fontTickLabels));
1052 for (int nTick = 0; nTick < nTickCount; ++nTick)
1054 int nTickYLocation = static_cast<int>(m_ptOrigin.y - tickSpace * (nTick + 1) + 0.5);
1055 dc.MoveTo(m_ptOrigin.x - TICK_PIXELS, nTickYLocation);
1056 VERIFY(dc.LineTo(m_ptOrigin.x + TICK_PIXELS, nTickYLocation));
1058 // Draw tick label.
1059 CString sTickLabel;
1060 sTickLabel.Format(_T("%d"), nTickStep * (nTick+1));
1061 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));
1063 VERIFY(dc.TextOut(m_ptOrigin.x - GAP_PIXELS - sizTickLabel.cx - TICK_PIXELS,
1064 nTickYLocation - sizTickLabel.cy/2, sTickLabel));
1067 // Draw X axis tick marks.
1068 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1069 int nSeries(0);
1071 while (pos) {
1073 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1074 ASSERT_VALID(pSeries);
1076 // Ignore unpopulated series if bar chart.
1077 if (m_eGraphType != MyGraph::Bar ||
1078 0 < pSeries->GetNonZeroElementCount()) {
1080 // Get the spacing of the series.
1081 int nSeriesSpace(0);
1083 if (m_saLegendLabels.GetSize()) {
1085 nSeriesSpace =
1086 (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
1087 (m_eGraphType == MyGraph::Bar ?
1088 GetNonZeroSeriesCount() : (int)m_olMyGraphSeries.GetCount());
1090 else {
1091 nSeriesSpace = m_nXAxisWidth / (m_eGraphType == MyGraph::Bar ?
1092 GetNonZeroSeriesCount() : (int)m_olMyGraphSeries.GetCount());
1095 int nTickXLocation(m_ptOrigin.x + ((nSeries + 1) * nSeriesSpace) -
1096 (nSeriesSpace / 2));
1098 dc.MoveTo(nTickXLocation, m_ptOrigin.y - TICK_PIXELS);
1099 VERIFY(dc.LineTo(nTickXLocation, m_ptOrigin.y + TICK_PIXELS));
1101 // Draw x-axis tick label.
1102 CString sTickLabel(pSeries->GetLabel());
1103 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));
1105 VERIFY(dc.TextOut(nTickXLocation - (sizTickLabel.cx / 2),
1106 m_ptOrigin.y + TICK_PIXELS + GAP_PIXELS, sTickLabel));
1108 ++nSeries;
1112 VERIFY(dc.SelectObject(pFontOld));
1113 fontXAxis.DeleteObject();
1114 fontYAxis.DeleteObject();
1115 fontTickLabels.DeleteObject();
1119 void MyGraph::DrawSeriesBar(CDC& dc) const
1121 VALIDATE;
1122 ASSERT_VALID(&dc);
1124 // How much space does each series get (includes inter series space)?
1125 // We ignore series whose members are all zero.
1126 double availableSpace = m_saLegendLabels.GetSize()
1127 ? m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)
1128 : m_nXAxisWidth;
1130 double seriesSpace = availableSpace / (double)GetNonZeroSeriesCount();
1132 // Determine width of bars. Data points with a value of zero are assumed
1133 // to be empty. This is a bad assumption.
1134 double barWidth(0.0);
1136 // This is the width of the largest series (no inter series space).
1137 double maxSeriesPlotSize(0.0);
1139 if(!m_bStackedGraph){
1140 int seriessize = GetMaxNonZeroSeriesSize();
1141 barWidth = seriessize ? seriesSpace / seriessize : 0;
1142 if (1 < GetNonZeroSeriesCount()) {
1143 barWidth *= INTERSERIES_PERCENT_USED;
1145 maxSeriesPlotSize = GetMaxNonZeroSeriesSize() * barWidth;
1147 else{
1148 barWidth = seriesSpace * INTERSERIES_PERCENT_USED;
1149 maxSeriesPlotSize = barWidth;
1152 // Iterate the series.
1153 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1154 int nSeries(0);
1156 while (pos) {
1158 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1159 ASSERT_VALID(pSeries);
1161 // Ignore unpopulated series.
1162 if (0 < pSeries->GetNonZeroElementCount()) {
1164 // Draw each bar; empty bars are not drawn.
1165 double runningLeft(m_ptOrigin.x + (nSeries + 1) * seriesSpace -
1166 maxSeriesPlotSize);
1168 double stackAccumulator(0.0);
1170 for (int nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup) {
1172 if (pSeries->GetData(nGroup)) {
1174 int nMaxDataValue(GetMaxDataValue());
1175 nMaxDataValue = max(nMaxDataValue, 1);
1176 double barTop = m_ptOrigin.y - (double)m_nYAxisHeight *
1177 pSeries->GetData(nGroup) / (double)nMaxDataValue - stackAccumulator;
1179 CRect rcBar;
1180 rcBar.left = (int)runningLeft;
1181 rcBar.top = (int)barTop;
1182 // Make adjacent bar borders overlap, so there's only one pixel border line between them.
1183 rcBar.right = (int)(runningLeft + barWidth) + 1;
1184 rcBar.bottom = (int)((double)m_ptOrigin.y - stackAccumulator) + 1;
1186 if(m_bStackedGraph){
1187 stackAccumulator = (double)m_ptOrigin.y - barTop;
1190 pSeries->SetTipRegion(nGroup, rcBar);
1192 COLORREF crBar(m_dwaColors.GetAt(nGroup));
1193 CBrush br(crBar);
1194 CBrush* pBrushOld = dc.SelectObject(&br);
1195 ASSERT_VALID(pBrushOld);
1197 VERIFY(dc.Rectangle(rcBar));
1198 dc.SelectObject(pBrushOld);
1199 br.DeleteObject();
1201 if(!m_bStackedGraph){
1202 runningLeft += barWidth;
1207 ++nSeries;
1211 if (!m_bStackedGraph) {
1212 int nMaxDataValue = max(GetMaxDataValue(), 1);
1213 double barTop = m_ptOrigin.y - (double)m_nYAxisHeight *
1214 (GetAverageDataValue() / (double)nMaxDataValue);
1215 dc.MoveTo(m_ptOrigin.x, barTop);
1216 VERIFY(dc.LineTo(m_ptOrigin.x + (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)), 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++) {
1233 // How much space does each series get (includes inter series space)?
1234 int nSeriesSpace(0);
1236 if (m_saLegendLabels.GetSize()) {
1238 nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
1239 (int)m_olMyGraphSeries.GetCount();
1241 else {
1242 nSeriesSpace = m_nXAxisWidth / (int)m_olMyGraphSeries.GetCount();
1245 // Determine width of bars.
1246 int nMaxSeriesSize(GetMaxSeriesSize());
1247 nMaxSeriesSize = max(nMaxSeriesSize, 1);
1248 int nBarWidth(nSeriesSpace / nMaxSeriesSize);
1250 if (1 < m_olMyGraphSeries.GetCount()) {
1251 nBarWidth = (int) ((double) nBarWidth * INTERSERIES_PERCENT_USED);
1254 // This is the width of the largest series (no inter series space).
1255 //int nMaxSeriesPlotSize(GetMaxSeriesSize() * nBarWidth);
1257 // Iterate the series.
1258 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1260 // Build objects.
1261 COLORREF crLine(m_dwaColors.GetAt(nGroup));
1262 CBrush br(crLine);
1263 CBrush* pBrushOld = dc.SelectObject(&br);
1264 ASSERT_VALID(pBrushOld);
1265 CPen penLine(PS_SOLID, 1, crLine);
1266 CPen* pPenOld = dc.SelectObject(&penLine);
1267 ASSERT_VALID(pPenOld);
1269 for (int nSeries = 0; nSeries < m_olMyGraphSeries.GetCount(); ++nSeries) {
1271 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1272 ASSERT_VALID(pSeries);
1274 // Get x and y location of center of ellipse.
1275 CPoint ptLoc(0,0);
1277 ptLoc.x = m_ptOrigin.x + (((nSeries + 1) * nSeriesSpace) -
1278 (nSeriesSpace / 2));
1280 int nMaxDataValue(GetMaxDataValue());
1281 nMaxDataValue = max(nMaxDataValue, 1);
1282 double dLineHeight(pSeries->GetData(nGroup) * m_nYAxisHeight /
1283 double(nMaxDataValue));
1285 ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);
1288 // Draw line back to last data member.
1289 if (nSeries > 0 && (pSeries->GetData(nGroup)!=0 || dataLastLoc != 0)) {
1291 dc.MoveTo(ptLastLoc.x, ptLastLoc.y - 1);
1292 VERIFY(dc.LineTo(ptLoc.x - 1, ptLoc.y - 1));
1295 // Now draw ellipse.
1296 CRect rcEllipse(ptLoc.x - 3, ptLoc.y - 3, ptLoc.x + 3, ptLoc.y + 3);
1297 if(pSeries->GetData(nGroup)!=0){
1298 VERIFY(dc.Ellipse(rcEllipse));
1300 if (m_olMyGraphSeries.GetCount() < 40)
1302 pSeries->SetTipRegion(nGroup, rcEllipse);
1305 // Save last pt and data
1306 ptLastLoc = ptLoc;
1307 dataLastLoc = pSeries->GetData(nGroup);
1309 VERIFY(dc.SelectObject(pPenOld));
1310 penLine.DeleteObject();
1311 VERIFY(dc.SelectObject(pBrushOld));
1312 br.DeleteObject();
1315 int nMaxDataValue = max(GetMaxDataValue(), 1);
1316 double barTop = m_ptOrigin.y - (double)m_nYAxisHeight *
1317 (GetAverageDataValue() / (double)nMaxDataValue);
1318 dc.MoveTo(m_ptOrigin.x, barTop);
1319 VERIFY(dc.LineTo(m_ptOrigin.x + (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)), barTop));
1323 void MyGraph::DrawSeriesLineStacked(CDC& dc) const
1325 VALIDATE;
1326 ASSERT_VALID(&dc);
1327 _ASSERTE(m_bStackedGraph);
1329 int nSeriesCount = (int)m_olMyGraphSeries.GetCount();
1331 CArray<int> stackAccumulator;
1332 stackAccumulator.SetSize(nSeriesCount);
1334 CArray<CPoint> polygon;
1335 // Special case: if we only have single series, make polygon
1336 // a bar instead of one pixel line.
1337 polygon.SetSize(nSeriesCount==1 ? 4 : nSeriesCount * 2);
1339 // How much space does each series get?
1340 int nSeriesSpace(0);
1341 if (m_saLegendLabels.GetSize()) {
1342 nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
1343 nSeriesCount;
1345 else {
1346 nSeriesSpace = m_nXAxisWidth / nSeriesCount;
1349 int nMaxDataValue(GetMaxDataValue());
1350 nMaxDataValue = max(nMaxDataValue, 1);
1351 double dYScaling = double(m_nYAxisHeight) / nMaxDataValue;
1353 // Iterate the groups.
1354 for (int nGroup = 0; nGroup < GetMaxSeriesSize(); nGroup++) {
1356 // Build objects.
1357 COLORREF crGroup(m_dwaColors.GetAt(nGroup));
1358 CBrush br(crGroup);
1359 CBrush* pBrushOld = dc.SelectObject(&br);
1360 ASSERT_VALID(pBrushOld);
1361 // For polygon outline, use average of this and previous color, and darken it.
1362 COLORREF crPrevGroup(nGroup > 0 ? m_dwaColors.GetAt(nGroup-1) : crGroup);
1363 COLORREF crOutline = RGB(
1364 (GetRValue(crGroup)+GetRValue(crPrevGroup))/3,
1365 (GetGValue(crGroup)+GetGValue(crPrevGroup))/3,
1366 (GetBValue(crGroup)+GetBValue(crPrevGroup))/3);
1367 CPen penLine(PS_SOLID, 1, crOutline);
1368 CPen* pPenOld = dc.SelectObject(&penLine);
1369 ASSERT_VALID(pPenOld);
1371 // Construct bottom part of polygon from current stack accumulator
1372 for (int nPolyBottom = 0; nPolyBottom < nSeriesCount; ++nPolyBottom) {
1373 CPoint ptLoc;
1374 ptLoc.x = m_ptOrigin.x + (((nPolyBottom + 1) * nSeriesSpace) - (nSeriesSpace / 2));
1375 double dLineHeight((stackAccumulator[nPolyBottom]) * dYScaling);
1376 ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);
1378 if (nSeriesCount > 1) {
1379 polygon[nSeriesCount-nPolyBottom-1] = ptLoc;
1380 } else {
1381 // special case: when there's one series, make polygon a bar
1382 polygon[0] = CPoint(ptLoc.x-GAP_PIXELS/2, ptLoc.y);
1383 polygon[1] = CPoint(ptLoc.x+GAP_PIXELS/2, ptLoc.y);
1387 // Iterate the series, construct upper part of polygon and upadte stack accumulator
1388 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1389 for (int nSeries = 0; nSeries < nSeriesCount; ++nSeries) {
1391 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1392 ASSERT_VALID(pSeries);
1394 CPoint ptLoc;
1395 ptLoc.x = m_ptOrigin.x + (((nSeries + 1) * nSeriesSpace) -
1396 (nSeriesSpace / 2));
1397 double dLineHeight((pSeries->GetData(nGroup) + stackAccumulator[nSeries]) * dYScaling);
1398 ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);
1399 if (nSeriesCount > 1) {
1400 polygon[nSeriesCount+nSeries] = ptLoc;
1401 } else {
1402 // special case: when there's one series, make polygon a bar
1403 polygon[2] = CPoint(ptLoc.x+GAP_PIXELS/2, ptLoc.y);
1404 polygon[3] = CPoint(ptLoc.x-GAP_PIXELS/2, ptLoc.y);
1407 stackAccumulator[nSeries] += pSeries->GetData(nGroup);
1410 // Draw polygon
1411 VERIFY(dc.Polygon(polygon.GetData(), (int)polygon.GetSize()));
1413 VERIFY(dc.SelectObject(pPenOld));
1414 penLine.DeleteObject();
1415 VERIFY(dc.SelectObject(pBrushOld));
1416 br.DeleteObject();
1421 void MyGraph::DrawSeriesPie(CDC& dc) const
1423 VALIDATE;
1424 ASSERT_VALID(&dc);
1426 // Determine width of pie display area (pie and space).
1427 int nSeriesSpace(0);
1429 int seriesCount = GetNonZeroSeriesCount();
1430 int horizontalSpace(0);
1432 if (m_saLegendLabels.GetSize()) {
1433 // With legend box.
1435 horizontalSpace = m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2);
1436 int nPieAndSpaceWidth(horizontalSpace / (seriesCount ? seriesCount : 1));
1438 // Height is limiting factor.
1439 if (nPieAndSpaceWidth > m_nYAxisHeight - (GAP_PIXELS * 2)) {
1440 nSeriesSpace = (m_nYAxisHeight - (GAP_PIXELS * 2));
1442 else {
1443 // Width is limiting factor.
1444 nSeriesSpace = nPieAndSpaceWidth;
1447 else {
1448 // No legend box.
1450 horizontalSpace = m_nXAxisWidth;
1452 // Height is limiting factor.
1453 if (m_nXAxisWidth > m_nYAxisHeight * (seriesCount ? seriesCount : 1)) {
1454 nSeriesSpace = m_nYAxisHeight;
1456 else {
1457 // Width is limiting factor.
1458 nSeriesSpace = m_nXAxisWidth / (seriesCount ? seriesCount : 1);
1462 // Make pies be centered horizontally
1463 int xOrigin = m_ptOrigin.x + GAP_PIXELS + (horizontalSpace - nSeriesSpace * seriesCount) / 2;
1465 // Create font for labels.
1466 CFont fontLabels;
1467 int pointFontHeight = max(m_rcGraph.Height() / Y_AXIS_LABEL_DIVISOR, MIN_FONT_SIZE);
1468 VERIFY(fontLabels.CreatePointFont(pointFontHeight, _T("Arial"), &dc));
1469 CFont* pFontOld = dc.SelectObject(&fontLabels);
1470 ASSERT_VALID(pFontOld);
1472 // Draw each pie.
1473 int nPie(0);
1474 int nRadius((int) (nSeriesSpace * INTERSERIES_PERCENT_USED / 2.0));
1475 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1477 while (pos) {
1479 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1480 ASSERT_VALID(pSeries);
1482 // Don't leave a space for empty pies.
1483 if (0 < pSeries->GetNonZeroElementCount()) {
1485 // Locate this pie.
1486 CPoint ptCenter;
1487 ptCenter.x = xOrigin + (nSeriesSpace * nPie) + nSeriesSpace / 2;
1488 ptCenter.y = m_ptOrigin.y - m_nYAxisHeight / 2;
1490 CRect rcPie;
1491 rcPie.left = ptCenter.x - nRadius;
1492 rcPie.right = ptCenter.x + nRadius;
1493 rcPie.top = ptCenter.y - nRadius;
1494 rcPie.bottom = ptCenter.y + nRadius;
1496 // Draw series label.
1497 CSize sizPieLabel(dc.GetTextExtent(pSeries->GetLabel()));
1499 VERIFY(dc.TextOut(ptCenter.x - (sizPieLabel.cx / 2),
1500 ptCenter.y + nRadius + GAP_PIXELS, pSeries->GetLabel()));
1502 // How much do the wedges total to?
1503 double dPieTotal(pSeries->GetDataTotal());
1505 // Draw each wedge in this pie.
1506 CPoint ptStart(rcPie.left, ptCenter.y);
1507 double dRunningWedgeTotal(0.0);
1509 for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {
1511 // Ignore empty wedges.
1512 if (0 < pSeries->GetData(nGroup)) {
1514 // Get the degrees of this wedge.
1515 dRunningWedgeTotal += pSeries->GetData(nGroup);
1516 double dPercent(dRunningWedgeTotal * 100.0 / dPieTotal);
1517 double degrees(360.0 * dPercent / 100.0);
1519 // Find the location of the wedge's endpoint.
1520 CPoint ptEnd(WedgeEndFromDegrees(degrees, ptCenter, nRadius));
1522 // Special case: a wedge that takes up the whole pie would
1523 // otherwise be confused with an empty wedge.
1524 bool drawEmptyWedges = false;
1525 if (1 == pSeries->GetNonZeroElementCount()) {
1526 _ASSERTE(360 == (int)degrees && ptStart == ptEnd && "This is the problem we're correcting");
1527 --ptEnd.y;
1528 drawEmptyWedges = true;
1531 // If the wedge is zero size or very narrow, don't paint it.
1532 // If pie is small, and wedge data is small, we might get a wedges
1533 // where center and both endpoints lie on the same coordinate,
1534 // and endpoints differ only in one pixel. GDI draws such pie as whole pie,
1535 // so we just skip them instead.
1536 int distance = abs(ptStart.x-ptEnd.x) + abs(ptStart.y-ptEnd.y);
1537 if (drawEmptyWedges || distance > 1) {
1539 // Draw wedge.
1540 COLORREF crWedge(m_dwaColors.GetAt(nGroup));
1541 CBrush br(crWedge);
1542 CBrush* pBrushOld = dc.SelectObject(&br);
1543 ASSERT_VALID(pBrushOld);
1544 VERIFY(dc.Pie(rcPie, ptStart, ptEnd));
1546 // Create a region from the path we create.
1547 VERIFY(dc.BeginPath());
1548 VERIFY(dc.Pie(rcPie, ptStart, ptEnd));
1549 VERIFY(dc.EndPath());
1550 CRgn * prgnWedge = new CRgn;
1551 VERIFY(prgnWedge->CreateFromPath(&dc));
1552 pSeries->SetTipRegion(nGroup, prgnWedge);
1554 // Cleanup.
1555 dc.SelectObject(pBrushOld);
1556 br.DeleteObject();
1557 ptStart = ptEnd;
1562 ++nPie;
1566 // Draw X axis title
1567 CSize sizXLabel(dc.GetTextExtent(m_sXAxisLabel));
1568 VERIFY(dc.TextOut(xOrigin + (nSeriesSpace * nPie - sizXLabel.cx)/2,
1569 m_ptOrigin.y - m_nYAxisHeight/2 + nRadius + GAP_PIXELS*2 + sizXLabel.cy, m_sXAxisLabel));
1571 VERIFY(dc.SelectObject(pFontOld));
1572 fontLabels.DeleteObject();
1575 // Convert degrees to x and y coords.
1576 CPoint MyGraph::WedgeEndFromDegrees(double degrees, const CPoint& ptCenter,
1577 double radius) const
1579 VALIDATE;
1581 CPoint pt;
1583 double radians = degrees / 360.0 * PI * 2.0;
1585 pt.x = (int) (radius * cos(radians));
1586 pt.x = ptCenter.x - pt.x;
1588 pt.y = (int) (radius * sin(radians));
1589 pt.y = ptCenter.y + pt.y;
1591 return pt;
1594 // Spin The Message Loop: C++ version. See "Advanced Windows Programming",
1595 // M. Heller, p. 153, and the MS TechNet CD, PSS ID Number: Q99999.
1596 /* static */ UINT MyGraph::SpinTheMessageLoop(bool bNoDrawing /* = false */ ,
1597 bool bOnlyDrawing /* = false */ ,
1598 UINT uiMsgAllowed /* = WM_NULL */ )
1600 MSG msg;
1601 ::SecureZeroMemory(&msg, sizeof(msg));
1603 while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
1605 // Do painting only.
1606 if (bOnlyDrawing && WM_PAINT == msg.message) {
1607 ::TranslateMessage(&msg);
1608 ::DispatchMessage(&msg);
1610 // Update user interface.
1611 AfxGetApp()->OnIdle(0);
1613 // Do everything *but* painting.
1614 else if (bNoDrawing && WM_PAINT == msg.message) {
1615 break;
1617 // Special handling for this message.
1618 else if (WM_QUIT == msg.message) {
1619 ::PostQuitMessage(static_cast<int>(msg.wParam));
1620 break;
1622 // Allow one message (like WM_LBUTTONDOWN).
1623 else if (uiMsgAllowed == msg.message
1624 && ! AfxGetApp()->PreTranslateMessage(&msg)) {
1625 ::TranslateMessage(&msg);
1626 ::DispatchMessage(&msg);
1627 break;
1629 // This is the general case.
1630 else if (! bOnlyDrawing && ! AfxGetApp()->PreTranslateMessage(&msg)) {
1631 ::TranslateMessage(&msg);
1632 ::DispatchMessage(&msg);
1634 // Update user interface, then free temporary objects.
1635 AfxGetApp()->OnIdle(0);
1636 AfxGetApp()->OnIdle(1);
1640 return msg.message;
1644 /////////////////////////////////////////////////////////////////////////////
1645 // Conversion routines: RGB to HLS (Red-Green-Blue to Hue-Luminosity-Saturation).
1646 // See Microsoft KnowledgeBase article Q29240.
1648 #define HLSMAX 240 // H,L, and S vary over 0-HLSMAX
1649 #define RGBMAX 255 // R,G, and B vary over 0-RGBMAX
1650 // HLSMAX BEST IF DIVISIBLE BY 6
1651 // RGBMAX, HLSMAX must each fit in a byte (255).
1653 #define UNDEFINED (HLSMAX * 2 / 3) // Hue is undefined if Saturation is 0
1654 // (grey-scale). This value determines
1655 // where the Hue scrollbar is initially
1656 // set for achromatic colors.
1659 // Convert HLS to RGB.
1660 /* static */ COLORREF MyGraph::HLStoRGB(WORD wH, WORD wL, WORD wS)
1662 _ASSERTE(0 <= wH && 240 >= wH && "Illegal hue value");
1663 _ASSERTE(0 <= wL && 240 >= wL && "Illegal lum value");
1664 _ASSERTE(0 <= wS && 240 >= wS && "Illegal sat value");
1666 WORD wR(0);
1667 WORD wG(0);
1668 WORD wB(0);
1670 // Achromatic case.
1671 if (0 == wS) {
1672 wR = wG = wB = (wL * RGBMAX) / HLSMAX;
1674 if (UNDEFINED != wH) {
1675 _ASSERTE(! "ERROR");
1678 else {
1679 // Chromatic case.
1680 WORD Magic1(0);
1681 WORD Magic2(0);
1683 // Set up magic numbers.
1684 if (wL <= HLSMAX / 2) {
1685 Magic2 = (wL * (HLSMAX + wS) + (HLSMAX / 2)) / HLSMAX;
1687 else {
1688 Magic2 = wL + wS - ((wL * wS) + (HLSMAX / 2)) / HLSMAX;
1691 Magic1 = 2 * wL - Magic2;
1693 // Get RGB, change units from HLSMAX to RGBMAX.
1694 wR = (HueToRGB(Magic1, Magic2, wH + (HLSMAX / 3)) * RGBMAX + (HLSMAX / 2)) / HLSMAX;
1695 wG = (HueToRGB(Magic1, Magic2, wH) * RGBMAX + (HLSMAX / 2)) / HLSMAX;
1696 wB = (HueToRGB(Magic1, Magic2, wH - (HLSMAX / 3)) * RGBMAX + (HLSMAX / 2)) / HLSMAX;
1699 return RGB(wR,wG,wB);
1702 // Utility routine for HLStoRGB.
1703 /* static */ WORD MyGraph::HueToRGB(WORD w1, WORD w2, WORD wH)
1705 // Range check: note values passed add/subtract thirds of range.
1706 if (wH < 0) {
1707 wH += HLSMAX;
1710 if (wH > HLSMAX) {
1711 wH -= HLSMAX;
1714 // Return r, g, or b value from this tridrant.
1715 if (wH < HLSMAX / 6) {
1716 return w1 + (((w2 - w1) * wH + (HLSMAX / 12)) / (HLSMAX / 6));
1719 if (wH < HLSMAX / 2) {
1720 return w2;
1723 if (wH < (HLSMAX * 2) / 3) {
1724 return w1 + (((w2 - w1) * (((HLSMAX * 2) / 3) - wH) + (HLSMAX / 12)) / (HLSMAX / 6));
1726 else {
1727 return w1;