The number of characters to be copied must be one less than the buffer size to accoun...
[TortoiseGit.git] / src / Utils / MiscUI / MyGraph.cpp
blobc39761d053dd8901e5833e154d51b1d07802d324
1 // MyGraph.cpp
3 #include "stdafx.h"
4 #include "MyGraph.h"
5 #include "BufferDC.h"
7 #include <cmath>
8 #define _USE_MATH_DEFINES
9 #include <math.h> // for M_PI
10 #include <memory>
12 #ifdef _DEBUG
13 #define new DEBUG_NEW
14 #undef THIS_FILE
15 static char THIS_FILE[] = __FILE__;
16 #endif
19 /////////////////////////////////////////////////////////////////////////////
20 // This macro can be called at the beginning and ending of every
21 // method. It is identical to saying "ASSERT_VALID(); ASSERT_KINDOF();"
22 // but is written like this so that VALIDATE can be a macro. It is useful
23 // as an "early warning" that something has gone wrong with "this" object.
24 #ifndef VALIDATE
25 #ifdef _DEBUG
26 #define VALIDATE ::AfxAssertValidObject(this, __FILE__ , __LINE__ ); \
27 _ASSERTE(IsKindOf(GetRuntimeClass()));
28 #else
29 #define VALIDATE
30 #endif
31 #endif
34 /////////////////////////////////////////////////////////////////////////////
35 // Constants.
37 #define TICK_PIXELS 4 // Size of tick marks.
38 #define GAP_PIXELS 6 // Better if an even value.
39 #define LEGEND_COLOR_BAR_WIDTH_PIXELS 50 // Width of color bar.
40 #define LEGEND_COLOR_BAR_GAP_PIXELS 1 // Space between color bars.
41 #define Y_AXIS_TICK_COUNT_TARGET 5 // How many ticks should be there on the y axis.
42 #define MIN_FONT_SIZE 70 // The minimum font-size in pt*10.
43 #define LEGEND_VISIBILITY_THRESHOLD 300 // The width of the graph in pixels when the legend gets hidden.
45 #define INTERSERIES_PERCENT_USED 0.85 // How much of the graph is
46 // used for bars/pies (the
47 // rest is for inter-series
48 // spacing).
50 #define TITLE_DIVISOR 5 // Scale font to graph width.
51 #define LEGEND_DIVISOR 8 // Scale font to graph height.
52 #define Y_AXIS_LABEL_DIVISOR 6 // Scale font to graph height.
54 #ifndef M_PI
55 const double M_PI = 3.1415926535897932384626433832795;
56 #endif
58 /////////////////////////////////////////////////////////////////////////////
59 // MyGraphSeries
61 // Constructor.
62 MyGraphSeries::MyGraphSeries(const CString& sLabel /* = "" */ )
63 : m_sLabel(sLabel)
67 // Destructor.
68 /* virtual */ MyGraphSeries::~MyGraphSeries()
70 for (int nGroup = 0; nGroup < m_oaRegions.GetSize(); ++nGroup) {
71 delete m_oaRegions.GetAt(nGroup);
76 void MyGraphSeries::SetLabel(const CString& sLabel)
78 VALIDATE;
80 m_sLabel = sLabel;
84 void MyGraphSeries::SetData(int nGroup, int nValue)
86 VALIDATE;
87 _ASSERTE(0 <= nGroup);
89 m_dwaValues.SetAtGrow(nGroup, nValue);
93 void MyGraphSeries::SetTipRegion(int nGroup, const CRect& rc)
95 VALIDATE;
97 std::unique_ptr<CRgn> prgnNew (new CRgn);
98 ASSERT_VALID(prgnNew.get());
100 VERIFY(prgnNew->CreateRectRgnIndirect(rc));
101 SetTipRegion(nGroup, prgnNew.release());
105 void MyGraphSeries::SetTipRegion(int nGroup, CRgn* prgn)
107 VALIDATE;
108 _ASSERTE(0 <= nGroup);
109 ASSERT_VALID(prgn);
111 // If there is an existing region, delete it.
112 CRgn* prgnOld = NULL;
114 if (nGroup < m_oaRegions.GetSize())
116 prgnOld = m_oaRegions.GetAt(nGroup);
117 ASSERT_NULL_OR_POINTER(prgnOld, CRgn);
120 delete prgnOld;
121 prgnOld = NULL;
123 // Add the new region.
124 m_oaRegions.SetAtGrow(nGroup, prgn);
126 _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());
130 CString MyGraphSeries::GetLabel() const
132 VALIDATE;
134 return m_sLabel;
138 int MyGraphSeries::GetData(int nGroup) const
140 VALIDATE;
141 _ASSERTE(0 <= nGroup);
142 _ASSERTE(m_dwaValues.GetSize() > nGroup);
144 return m_dwaValues[nGroup];
147 // Returns the largest data value in this series.
148 int MyGraphSeries::GetMaxDataValue(bool bStackedGraph) const
150 VALIDATE;
152 int nMax(0);
154 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
155 if(!bStackedGraph){
156 nMax = max(nMax, static_cast<int> (m_dwaValues.GetAt(nGroup)));
158 else{
159 nMax += static_cast<int> (m_dwaValues.GetAt(nGroup));
163 return nMax;
166 // Returns the average data value in this series.
167 int MyGraphSeries::GetAverageDataValue() const
169 VALIDATE;
171 int nTotal = 0;
173 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
174 nTotal += static_cast<int> (m_dwaValues.GetAt(nGroup));
177 if (m_dwaValues.GetSize() == 0)
178 return 0;
180 return nTotal / m_dwaValues.GetSize();
183 // Returns the number of data points that are not zero.
184 int MyGraphSeries::GetNonZeroElementCount() const
186 VALIDATE;
188 int nCount(0);
190 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
192 if (m_dwaValues.GetAt(nGroup)) {
193 ++nCount;
197 return nCount;
200 // Returns the sum of the data points for this series.
201 int MyGraphSeries::GetDataTotal() const
203 VALIDATE;
205 int nTotal(0);
207 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
208 nTotal += m_dwaValues.GetAt(nGroup);
211 return nTotal;
214 // Returns which group (if any) the sent point lies within in this series.
215 int MyGraphSeries::HitTest(const CPoint& pt, int searchStart = 0) const
217 VALIDATE;
219 for (int nGroup = searchStart; nGroup < m_oaRegions.GetSize(); ++nGroup) {
220 CRgn* prgnData = m_oaRegions.GetAt(nGroup);
221 ASSERT_NULL_OR_POINTER(prgnData, CRgn);
223 if (prgnData && prgnData->PtInRegion(pt)) {
224 return nGroup;
228 return -1;
231 // Get the series portion of the tip for this group in this series.
232 CString MyGraphSeries::GetTipText(int nGroup, const CString &unitString) const
234 VALIDATE;
235 _ASSERTE(0 <= nGroup);
236 _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());
238 CString sTip;
240 sTip.Format(_T("%d %s (%d%%)"), m_dwaValues.GetAt(nGroup),
241 (LPCTSTR)unitString,
242 GetDataTotal() ? (int) (100.0 * (double) m_dwaValues.GetAt(nGroup) /
243 (double) GetDataTotal()) : 0);
245 return sTip;
249 /////////////////////////////////////////////////////////////////////////////
250 // MyGraph
252 // Constructor.
253 MyGraph::MyGraph(GraphType eGraphType /* = MyGraph::Pie */ , bool bStackedGraph /* = false */)
254 : m_nXAxisWidth(0)
255 , m_nYAxisHeight(0)
256 , m_nAxisLabelHeight(0)
257 , m_nAxisTickLabelHeight(0)
258 , m_eGraphType(eGraphType)
259 , m_bStackedGraph(bStackedGraph)
261 m_ptOrigin.x = m_ptOrigin.y = 0;
262 m_rcGraph.SetRectEmpty();
263 m_rcLegend.SetRectEmpty();
264 m_rcTitle.SetRectEmpty();
267 // Destructor.
268 /* virtual */ MyGraph::~MyGraph()
272 BEGIN_MESSAGE_MAP(MyGraph, CStatic)
273 //{{AFX_MSG_MAP(MyGraph)
274 ON_WM_PAINT()
275 ON_WM_SIZE()
276 //}}AFX_MSG_MAP
277 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnNeedText)
278 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnNeedText)
279 END_MESSAGE_MAP()
281 // Called by the framework to allow other necessary sub classing to occur
282 // before the window is sub classed.
283 void MyGraph::PreSubclassWindow()
285 VALIDATE;
287 CStatic::PreSubclassWindow();
289 VERIFY(EnableToolTips(true));
293 /////////////////////////////////////////////////////////////////////////////
294 // MyGraph message handlers
296 // Handle the tooltip messages. Returns true to mean message was handled.
297 BOOL MyGraph::OnNeedText(UINT /*uiId*/, NMHDR* pNMHDR, LRESULT* pResult)
299 _ASSERTE(pNMHDR && "Bad parameter passed");
300 _ASSERTE(pResult && "Bad parameter passed");
302 bool bReturn(false);
303 UINT_PTR uiID(pNMHDR->idFrom);
305 // Notification in NT from automatically created tooltip.
306 if (0U != uiID) {
307 bReturn = true;
309 // Need to handle both ANSI and UNICODE versions of the message.
310 TOOLTIPTEXTA* pTTTA = reinterpret_cast<TOOLTIPTEXTA*> (pNMHDR);
311 ASSERT_POINTER(pTTTA, TOOLTIPTEXTA);
313 TOOLTIPTEXTW* pTTTW = reinterpret_cast<TOOLTIPTEXTW*> (pNMHDR);
314 ASSERT_POINTER(pTTTW, TOOLTIPTEXTW);
316 CString sTipText(GetTipText());
318 #ifndef _UNICODE
319 if (TTN_NEEDTEXTA == pNMHDR->code) {
320 lstrcpyn(pTTTA->szText, sTipText, _countof(pTTTA->szText) - 1);
322 else {
323 _mbstowcsz(pTTTW->szText, sTipText, _countof(pTTTA->szText));
325 #else
326 if (pNMHDR->code == TTN_NEEDTEXTA) {
327 _wcstombsz(pTTTA->szText, sTipText, _countof(pTTTA->szText));
329 else {
330 lstrcpyn(pTTTW->szText, sTipText, _countof(pTTTA->szText) - 1);
332 #endif
334 *pResult = 0;
337 return bReturn;
340 // The framework calls this member function to determine whether a point is in
341 // the bounding rectangle of the specified tool.
342 INT_PTR MyGraph::OnToolHitTest(CPoint point, TOOLINFO* pTI) const
344 _ASSERTE(pTI && "Bad parameter passed");
346 // This works around the problem of the tip remaining visible when you move
347 // the mouse to various positions over this control.
348 INT_PTR nReturn(0);
349 static bool bTipPopped(false);
350 static CPoint ptPrev(-1,-1);
352 if (point != ptPrev) {
353 ptPrev = point;
355 if (bTipPopped) {
356 bTipPopped = false;
357 nReturn = -1;
359 else {
360 ::Sleep(50);
361 bTipPopped = true;
363 pTI->hwnd = m_hWnd;
364 pTI->uId = (UINT_PTR) m_hWnd;
365 pTI->lpszText = LPSTR_TEXTCALLBACK;
367 CRect rcWnd;
368 GetClientRect(&rcWnd);
369 pTI->rect = rcWnd;
370 nReturn = 1;
373 else {
374 nReturn = 1;
377 MyGraph::SpinTheMessageLoop();
379 return nReturn;
382 // Build the tip text for the part of the graph that the mouse is currently
383 // over.
384 CString MyGraph::GetTipText() const
386 VALIDATE;
388 CString sTip("");
390 // Get the position of the mouse.
391 CPoint pt;
392 VERIFY(::GetCursorPos(&pt));
393 ScreenToClient(&pt);
395 // Ask each part of the graph to check and see if the mouse is over it.
396 if (m_rcLegend.PtInRect(pt)) {
397 sTip = "Legend";
399 else if (m_rcTitle.PtInRect(pt)) {
400 sTip = "Title";
402 else {
403 int maxXAxis = m_ptOrigin.x + (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2));
404 if (pt.x >= m_ptOrigin.x && pt.x <= maxXAxis) {
405 int average = GetAverageDataValue();
406 int nMaxDataValue = max(GetMaxDataValue(), 1);
407 double barTop = m_ptOrigin.y - (double)m_nYAxisHeight *
408 (average / (double)nMaxDataValue);
409 if (pt.y >= barTop - 2 && pt.y <= barTop + 2) {
410 sTip.Format(_T("Average: %d %s (%d%%)"), average, m_sYAxisLabel, nMaxDataValue ? (100 * average / nMaxDataValue) : 0);
411 return sTip;
415 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
417 while (pos && sTip.IsEmpty()) {
418 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
419 ASSERT_VALID(pSeries);
421 int nGroup(0);
423 nGroup = pSeries->HitTest(pt,nGroup);
425 if (-1 != nGroup) {
426 if("" != sTip){
427 sTip += _T(", ");
429 sTip += m_saLegendLabels.GetAt(nGroup) + _T(": ");
430 sTip += pSeries->GetTipText(nGroup, m_sYAxisLabel);
431 nGroup++;
433 }while(-1 != nGroup);
437 return sTip;
440 // Handle WM_PAINT.
441 void MyGraph::OnPaint()
443 VALIDATE;
445 CBufferDC dc(this);
446 DrawGraph(dc);
449 // Handle WM_SIZE.
450 void MyGraph::OnSize(UINT nType, int cx, int cy)
452 VALIDATE;
454 CStatic::OnSize(nType, cx, cy);
456 Invalidate();
459 // Change the type of the graph; the caller should call Invalidate() on this
460 // window to make the effect of this change visible.
461 void MyGraph::SetGraphType(GraphType e, bool bStackedGraph)
463 VALIDATE;
465 m_eGraphType = e;
466 m_bStackedGraph = bStackedGraph;
469 // Calculate the current max legend label length in pixels.
470 int MyGraph::GetMaxLegendLabelLength(CDC& dc) const
472 VALIDATE;
473 ASSERT_VALID(&dc);
475 CString sMax;
476 int nMaxChars(-1);
477 CSize siz(-1,-1);
479 // First get max number of characters.
480 for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {
481 int nLabelLength(m_saLegendLabels.GetAt(nGroup).GetLength());
483 if (nMaxChars < nLabelLength) {
484 nMaxChars = nLabelLength;
485 sMax = m_saLegendLabels.GetAt(nGroup);
489 // Now calculate the pixels.
490 siz = dc.GetTextExtent(sMax);
492 _ASSERTE(-1 < siz.cx);
494 return siz.cx;
497 // Returns the largest number of data points in any series.
498 int MyGraph::GetMaxSeriesSize() const
500 VALIDATE;
502 int nMax(0);
503 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
505 while (pos) {
506 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
507 ASSERT_VALID(pSeries);
509 nMax = max(nMax, (int)pSeries->m_dwaValues.GetSize());
512 return nMax;
515 // Returns the largest number of non-zero data points in any series.
516 int MyGraph::GetMaxNonZeroSeriesSize() const
518 VALIDATE;
520 int nMax(0);
521 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
523 while (pos) {
524 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
525 ASSERT_VALID(pSeries);
527 nMax = max(nMax, pSeries->GetNonZeroElementCount());
530 return nMax;
533 // Get the largest data value in all series.
534 int MyGraph::GetMaxDataValue() const
536 VALIDATE;
538 int nMax(0);
539 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
541 while (pos) {
542 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
543 ASSERT_VALID(pSeries);
545 nMax = max(nMax, pSeries->GetMaxDataValue(m_bStackedGraph));
548 return nMax;
551 // Get the average data value in all series.
552 int MyGraph::GetAverageDataValue() const
554 VALIDATE;
556 int nTotal = 0, nCount = 0;
557 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
559 while (pos) {
560 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
561 ASSERT_VALID(pSeries);
563 nTotal += pSeries->GetAverageDataValue();
564 ++nCount;
567 if (nCount == 0)
568 return 0;
570 return nTotal / nCount;
573 // How many series are populated?
574 int MyGraph::GetNonZeroSeriesCount() const
576 VALIDATE;
578 int nCount(0);
579 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
581 while (pos) {
582 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
583 ASSERT_VALID(pSeries);
585 if (0 < pSeries->GetNonZeroElementCount()) {
586 ++nCount;
590 return nCount ? nCount : 1;
593 // Returns the group number for the sent label; -1 if not found.
594 int MyGraph::LookupLabel(const CString& sLabel) const
596 VALIDATE;
597 _ASSERTE(! sLabel.IsEmpty());
599 for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {
601 if (0 == sLabel.CompareNoCase(m_saLegendLabels.GetAt(nGroup))) {
602 return nGroup;
606 return -1;
609 void MyGraph::Clear()
611 m_dwaColors.RemoveAll();
612 m_saLegendLabels.RemoveAll();
613 m_olMyGraphSeries.RemoveAll();
617 void MyGraph::AddSeries(MyGraphSeries& rMyGraphSeries)
619 VALIDATE;
620 ASSERT_VALID(&rMyGraphSeries);
621 _ASSERTE(m_saLegendLabels.GetSize() == rMyGraphSeries.m_dwaValues.GetSize());
623 m_olMyGraphSeries.AddTail(&rMyGraphSeries);
627 void MyGraph::SetXAxisLabel(const CString& sLabel)
629 VALIDATE;
630 _ASSERTE(! sLabel.IsEmpty());
632 m_sXAxisLabel = sLabel;
636 void MyGraph::SetYAxisLabel(const CString& sLabel)
638 VALIDATE;
639 _ASSERTE(! sLabel.IsEmpty());
641 m_sYAxisLabel = sLabel;
644 // Returns the group number added. Also, makes sure that all the series have
645 // this many elements.
646 int MyGraph::AppendGroup(const CString& sLabel)
648 VALIDATE;
650 // Add the group.
651 int nGroup((int)m_saLegendLabels.GetSize());
652 SetLegend(nGroup, sLabel);
654 // Make sure that all series have this element.
655 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
657 while (pos) {
659 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
660 ASSERT_VALID(pSeries);
662 if (nGroup >= pSeries->m_dwaValues.GetSize()) {
663 pSeries->m_dwaValues.SetAtGrow(nGroup, 0);
667 return nGroup;
670 // Set this value to the legend.
671 void MyGraph::SetLegend(int nGroup, const CString& sLabel)
673 VALIDATE;
674 _ASSERTE(0 <= nGroup);
676 m_saLegendLabels.SetAtGrow(nGroup, sLabel);
680 void MyGraph::SetGraphTitle(const CString& sTitle)
682 VALIDATE;
683 _ASSERTE(! sTitle.IsEmpty());
685 m_sTitle = sTitle;
689 void MyGraph::DrawGraph(CDC& dc)
691 VALIDATE;
692 ASSERT_VALID(&dc);
694 if (GetMaxSeriesSize()) {
695 dc.SetBkMode(TRANSPARENT);
697 // Populate the colors as a group of evenly spaced colors of maximum
698 // saturation.
699 int nColorsDelta(240 / GetMaxSeriesSize());
701 int baseColorL = 120;
702 int diffColorL = 60;
703 DWORD backgroundColor = ::GetSysColor(COLOR_WINDOW);
704 // If graph is a non-stacked line graph, use darker colors if system window color is light.
705 #if 0
706 if (m_eGraphType == MyGraph::Line && !m_bStackedGraph) {
707 int backgroundLuma = (GetRValue(backgroundColor) + GetGValue(backgroundColor) + GetBValue(backgroundColor)) / 3;
708 if (backgroundLuma > 128) {
709 baseColorL = 70;
710 diffColorL = 50;
713 #endif
714 for (WORD nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup) {
715 WORD colorH = (WORD)(nColorsDelta * nGroup);
716 WORD colorL = (WORD)(baseColorL+(diffColorL*(nGroup%2)));
717 WORD colorS = (WORD)(180)+(30*((1-nGroup%2)*(nGroup%3)));
718 COLORREF cr(MyGraph::HLStoRGB(colorH, colorL, colorS)); // Populate colors cleverly
719 m_dwaColors.SetAtGrow(nGroup, cr);
722 // Reduce the graphable area by the frame window and status bar. We will
723 // leave GAP_PIXELS pixels blank on all sides of the graph. So top-left
724 // side of graph is at GAP_PIXELS,GAP_PIXELS and the bottom-right side
725 // of graph is at (m_rcGraph.Height() - GAP_PIXELS), (m_rcGraph.Width() -
726 // GAP_PIXELS). These settings are altered by axis labels and legends.
727 CRect rcWnd;
728 GetClientRect(&rcWnd);
729 m_rcGraph.left = GAP_PIXELS;
730 m_rcGraph.top = GAP_PIXELS;
731 m_rcGraph.right = rcWnd.Width() - GAP_PIXELS;
732 m_rcGraph.bottom = rcWnd.Height() - GAP_PIXELS;
734 CBrush br;
735 VERIFY(br.CreateSolidBrush(backgroundColor));
736 dc.FillRect(rcWnd, &br);
737 br.DeleteObject();
739 // Draw graph title.
740 DrawTitle(dc);
742 // Set the axes and origin values.
743 SetupAxes(dc);
745 // Draw legend if there is one and there's enough space.
746 if (m_saLegendLabels.GetSize() && m_rcGraph.right-m_rcGraph.left > LEGEND_VISIBILITY_THRESHOLD) {
747 DrawLegend(dc);
749 else{
750 m_rcLegend.SetRectEmpty();
753 // Draw axes unless it's a pie.
754 if (m_eGraphType != MyGraph::PieChart) {
755 DrawAxes(dc);
758 // Draw series data and labels.
759 switch (m_eGraphType) {
760 case MyGraph::Bar: DrawSeriesBar(dc); break;
761 case MyGraph::Line: if (m_bStackedGraph) DrawSeriesLineStacked(dc); else DrawSeriesLine(dc); break;
762 case MyGraph::PieChart: DrawSeriesPie(dc); break;
763 default: _ASSERTE(! "Bad default case"); break;
768 // Draw graph title; size is proportionate to width.
769 void MyGraph::DrawTitle(CDC& dc)
771 VALIDATE;
772 ASSERT_VALID(&dc);
774 // Create the title font.
775 CFont fontTitle;
776 VERIFY(fontTitle.CreatePointFont(max(m_rcGraph.Width() / TITLE_DIVISOR, MIN_FONT_SIZE),
777 _T("Arial"), &dc));
778 CFont* pFontOld = dc.SelectObject(&fontTitle);
779 ASSERT_VALID(pFontOld);
781 // Draw the title.
782 m_rcTitle.SetRect(GAP_PIXELS, GAP_PIXELS, m_rcGraph.Width() + GAP_PIXELS,
783 m_rcGraph.Height() + GAP_PIXELS);
785 dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE |
786 DT_TOP | DT_CALCRECT);
788 m_rcTitle.right = m_rcGraph.Width() + GAP_PIXELS;
790 dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE |
791 DT_TOP);
793 VERIFY(dc.SelectObject(pFontOld));
794 fontTitle.DeleteObject();
797 // Set the axes and origin values.
798 void MyGraph::SetupAxes(CDC& dc)
800 VALIDATE;
801 ASSERT_VALID(&dc);
803 // Since pie has no axis lines, set to full size minus GAP_PIXELS on each
804 // side. These are needed for legend to plot itself.
805 if (MyGraph::PieChart == m_eGraphType) {
806 m_nXAxisWidth = m_rcGraph.Width() - (GAP_PIXELS * 2);
807 m_nYAxisHeight = m_rcGraph.Height() - m_rcTitle.bottom;
808 m_ptOrigin.x = GAP_PIXELS;
809 m_ptOrigin.y = m_rcGraph.Height() - GAP_PIXELS;
811 else {
812 // Bar and Line graphs.
814 // Need to find out how wide the biggest Y-axis tick label is
816 // Get and store height of axis label font.
817 m_nAxisLabelHeight = max(m_rcGraph.Height() / Y_AXIS_LABEL_DIVISOR, MIN_FONT_SIZE);
818 // Get and store height of tick label font.
819 m_nAxisTickLabelHeight = max(int(m_nAxisLabelHeight*0.8), MIN_FONT_SIZE);
821 CFont fontTickLabels;
822 VERIFY(fontTickLabels.CreatePointFont(m_nAxisTickLabelHeight, _T("Arial"), &dc));
823 // Select font and store the old.
824 CFont* pFontOld = dc.SelectObject(&fontTickLabels);
825 ASSERT_VALID(pFontOld);
827 // Obtain tick label dimensions.
828 CString sTickLabel;
829 sTickLabel.Format(_T("%d"), GetMaxDataValue());
830 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));
832 // Set old font object again and delete temporary font object.
833 VERIFY(dc.SelectObject(pFontOld));
834 fontTickLabels.DeleteObject();
836 // Determine axis specifications.
837 m_ptOrigin.x = m_rcGraph.left + m_nAxisLabelHeight/10 + 2*GAP_PIXELS
838 + sizTickLabel.cx + GAP_PIXELS + TICK_PIXELS;
839 m_ptOrigin.y = m_rcGraph.bottom - m_nAxisLabelHeight/10 - 2*GAP_PIXELS -
840 sizTickLabel.cy - GAP_PIXELS - TICK_PIXELS;
841 m_nYAxisHeight = m_ptOrigin.y - m_rcTitle.bottom - (2 * GAP_PIXELS);
842 m_nXAxisWidth = (m_rcGraph.Width() - GAP_PIXELS) - m_ptOrigin.x;
847 void MyGraph::DrawLegend(CDC& dc)
849 VALIDATE;
850 ASSERT_VALID(&dc);
852 // Create the legend font.
853 CFont fontLegend;
854 int pointFontHeight = max(m_rcGraph.Height() / LEGEND_DIVISOR, MIN_FONT_SIZE);
855 VERIFY(fontLegend.CreatePointFont(pointFontHeight, _T("Arial"), &dc));
857 // Get the height of each label.
858 LOGFONT lf;
859 ::SecureZeroMemory(&lf, sizeof(lf));
860 VERIFY(fontLegend.GetLogFont(&lf));
861 int nLabelHeight(abs(lf.lfHeight));
863 // Get number of legend entries
864 int nLegendEntries = max(1, GetMaxSeriesSize());
866 // Calculate optimal label height = AvailableLegendHeight/AllAuthors
867 // Use a buffer of (GAP_PIXELS / 2) on each side inside the legend, and in addition the same
868 // gab above and below the legend frame, so in total 2*GAP_PIXELS
869 double optimalLabelHeight = double(m_rcGraph.Height() - 2*GAP_PIXELS)/nLegendEntries;
871 // Now relate the LabelHeight to the PointFontHeight
872 int optimalPointFontHeight = int(pointFontHeight*optimalLabelHeight/nLabelHeight);
874 // Limit the optimal PointFontHeight to the available range
875 optimalPointFontHeight = min( max(optimalPointFontHeight, MIN_FONT_SIZE), pointFontHeight);
877 // If the optimalPointFontHeight is different from the initial one, create a new legend font
878 if (optimalPointFontHeight != pointFontHeight) {
879 fontLegend.DeleteObject();
880 VERIFY(fontLegend.CreatePointFont(optimalPointFontHeight, _T("Arial"), &dc));
881 VERIFY(fontLegend.GetLogFont(&lf));
882 nLabelHeight = abs(lf.lfHeight);
885 // Calculate maximum number of authors that can be shown with the current label height
886 int nShownAuthors = (m_rcGraph.Height() - 2*GAP_PIXELS)/nLabelHeight - 1;
887 // Fix rounding errors.
888 if (nShownAuthors+1 == GetMaxSeriesSize())
889 ++nShownAuthors;
891 // Get number of authors to be shown.
892 nShownAuthors = min(nShownAuthors, GetMaxSeriesSize());
893 // nShownAuthors contains now the number of authors
895 CFont* pFontOld = dc.SelectObject(&fontLegend);
896 ASSERT_VALID(pFontOld);
898 // Determine actual size of legend. A buffer of (GAP_PIXELS / 2) on each side,
899 // plus the height of each label based on the pint size of the font.
900 int nLegendHeight = (GAP_PIXELS / 2) + (nShownAuthors * nLabelHeight) + (GAP_PIXELS / 2);
901 // Draw the legend border. Allow LEGEND_COLOR_BAR_PIXELS pixels for
902 // display of label bars.
903 m_rcLegend.top = (m_rcGraph.Height() - nLegendHeight) / 2;
904 m_rcLegend.bottom = m_rcLegend.top + nLegendHeight;
905 m_rcLegend.right = m_rcGraph.Width() - GAP_PIXELS;
906 m_rcLegend.left = m_rcLegend.right - GetMaxLegendLabelLength(dc) -
907 LEGEND_COLOR_BAR_WIDTH_PIXELS;
908 VERIFY(dc.Rectangle(m_rcLegend));
910 int skipped_row = -1; // if != -1, this is the row that we show the ... in
911 if (nShownAuthors < GetMaxSeriesSize())
912 skipped_row = nShownAuthors-2;
913 // Draw each group's label and bar.
914 for (int nGroup = 0; nGroup < nShownAuthors; ++nGroup) {
916 int nLabelTop(m_rcLegend.top + (nGroup * nLabelHeight) +
917 (GAP_PIXELS / 2));
919 int nShownGroup = nGroup; // introduce helper variable to avoid code duplication
921 // Do we have a skipped row?
922 if (skipped_row != -1)
924 if (nGroup == skipped_row) {
925 // draw the dots
926 VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop, _T("...") ));
927 continue;
929 if (nGroup == nShownAuthors-1) {
930 // we show the last group instead of the scheduled group
931 nShownGroup = GetMaxSeriesSize()-1;
934 // Draw the label.
935 VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop,
936 m_saLegendLabels.GetAt(nShownGroup)));
938 // Determine the bar.
939 CRect rcBar;
940 rcBar.left = m_rcLegend.left + GAP_PIXELS + GetMaxLegendLabelLength(dc) + GAP_PIXELS;
941 rcBar.top = nLabelTop + LEGEND_COLOR_BAR_GAP_PIXELS;
942 rcBar.right = m_rcLegend.right - GAP_PIXELS;
943 rcBar.bottom = rcBar.top + nLabelHeight - LEGEND_COLOR_BAR_GAP_PIXELS;
944 VERIFY(dc.Rectangle(rcBar));
946 // Draw bar for group.
947 COLORREF crBar(m_dwaColors.GetAt(nShownGroup));
948 CBrush br(crBar);
950 CBrush* pBrushOld = dc.SelectObject(&br);
951 ASSERT_VALID(pBrushOld);
953 rcBar.DeflateRect(LEGEND_COLOR_BAR_GAP_PIXELS, LEGEND_COLOR_BAR_GAP_PIXELS);
954 dc.FillRect(rcBar, &br);
956 dc.SelectObject(pBrushOld);
957 br.DeleteObject();
960 VERIFY(dc.SelectObject(pFontOld));
961 fontLegend.DeleteObject();
965 void MyGraph::DrawAxes(CDC& dc) const
967 VALIDATE;
968 ASSERT_VALID(&dc);
969 _ASSERTE(MyGraph::PieChart != m_eGraphType);
971 dc.SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));
973 // Draw y axis.
974 dc.MoveTo(m_ptOrigin);
975 VERIFY(dc.LineTo(m_ptOrigin.x, m_ptOrigin.y - m_nYAxisHeight));
977 // Draw x axis.
978 dc.MoveTo(m_ptOrigin);
980 if (m_saLegendLabels.GetSize()) {
982 VERIFY(dc.LineTo(m_ptOrigin.x +
983 (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)),
984 m_ptOrigin.y));
986 else {
987 VERIFY(dc.LineTo(m_ptOrigin.x + m_nXAxisWidth, m_ptOrigin.y));
990 // Note: m_nAxisLabelHeight and m_nAxisTickLabelHeight have been calculated in SetupAxis()
992 // Create the x-axis label font.
993 CFont fontXAxis;
994 VERIFY(fontXAxis.CreatePointFont(m_nAxisLabelHeight, _T("Arial"), &dc));
996 // Obtain the height of the font in device coordinates.
997 LOGFONT pLF;
998 VERIFY(fontXAxis.GetLogFont(&pLF));
999 int fontHeightDC = pLF.lfHeight;
1001 // Create the y-axis label font.
1002 CFont fontYAxis;
1003 VERIFY(fontYAxis.CreateFont(
1004 /* nHeight */ fontHeightDC,
1005 /* nWidth */ 0,
1006 /* nEscapement */ 90 * 10,
1007 /* nOrientation */ 0,
1008 /* nWeight */ FW_DONTCARE,
1009 /* bItalic */ false,
1010 /* bUnderline */ false,
1011 /* cStrikeOut */ 0,
1012 ANSI_CHARSET,
1013 OUT_DEFAULT_PRECIS,
1014 CLIP_DEFAULT_PRECIS,
1015 PROOF_QUALITY,
1016 VARIABLE_PITCH | FF_DONTCARE,
1017 _T("Arial"))
1020 // Set the y-axis label font and draw the label.
1021 CFont* pFontOld = dc.SelectObject(&fontYAxis);
1022 ASSERT_VALID(pFontOld);
1023 CSize sizYLabel(dc.GetTextExtent(m_sYAxisLabel));
1024 VERIFY(dc.TextOut(GAP_PIXELS, (m_rcGraph.Height() + sizYLabel.cx) / 2,
1025 m_sYAxisLabel));
1027 // Set the x-axis label font and draw the label.
1028 VERIFY(dc.SelectObject(&fontXAxis));
1029 CSize sizXLabel(dc.GetTextExtent(m_sXAxisLabel));
1030 VERIFY(dc.TextOut(m_ptOrigin.x + (m_nXAxisWidth - sizXLabel.cx) / 2,
1031 m_rcGraph.bottom - GAP_PIXELS - sizXLabel.cy, m_sXAxisLabel));
1033 // chose suitable tick step (1, 2, 5, 10, 20, 50, etc.)
1034 int nMaxDataValue(GetMaxDataValue());
1035 nMaxDataValue = max(nMaxDataValue, 1);
1036 int nTickStep = 1;
1037 while (10 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
1038 nTickStep *= 10;
1040 if (5 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
1041 nTickStep *= 5;
1042 if (2 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
1043 nTickStep *= 2;
1045 // We hardwire TITLE_DIVISOR y-axis ticks here for simplicity.
1046 int nTickCount(nMaxDataValue / nTickStep);
1047 double tickSpace = (double)m_nYAxisHeight * nTickStep / (double)nMaxDataValue;
1049 // create tick label font and set it in the device context
1050 CFont fontTickLabels;
1051 VERIFY(fontTickLabels.CreatePointFont(m_nAxisTickLabelHeight, _T("Arial"), &dc));
1052 VERIFY(dc.SelectObject(&fontTickLabels));
1054 for (int nTick = 0; nTick < nTickCount; ++nTick)
1056 int nTickYLocation = static_cast<int>(m_ptOrigin.y - tickSpace * (nTick + 1) + 0.5);
1057 dc.MoveTo(m_ptOrigin.x - TICK_PIXELS, nTickYLocation);
1058 VERIFY(dc.LineTo(m_ptOrigin.x + TICK_PIXELS, nTickYLocation));
1060 // Draw tick label.
1061 CString sTickLabel;
1062 sTickLabel.Format(_T("%d"), nTickStep * (nTick+1));
1063 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));
1065 VERIFY(dc.TextOut(m_ptOrigin.x - GAP_PIXELS - sizTickLabel.cx - TICK_PIXELS,
1066 nTickYLocation - sizTickLabel.cy/2, sTickLabel));
1069 // Draw X axis tick marks.
1070 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1071 int nSeries(0);
1073 while (pos) {
1075 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1076 ASSERT_VALID(pSeries);
1078 // Ignore unpopulated series if bar chart.
1079 if (m_eGraphType != MyGraph::Bar ||
1080 0 < pSeries->GetNonZeroElementCount()) {
1082 // Get the spacing of the series.
1083 int nSeriesSpace(0);
1085 if (m_saLegendLabels.GetSize()) {
1087 nSeriesSpace =
1088 (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
1089 (m_eGraphType == MyGraph::Bar ?
1090 GetNonZeroSeriesCount() : (int)m_olMyGraphSeries.GetCount());
1092 else {
1093 nSeriesSpace = m_nXAxisWidth / (m_eGraphType == MyGraph::Bar ?
1094 GetNonZeroSeriesCount() : (int)m_olMyGraphSeries.GetCount());
1097 int nTickXLocation(m_ptOrigin.x + ((nSeries + 1) * nSeriesSpace) -
1098 (nSeriesSpace / 2));
1100 dc.MoveTo(nTickXLocation, m_ptOrigin.y - TICK_PIXELS);
1101 VERIFY(dc.LineTo(nTickXLocation, m_ptOrigin.y + TICK_PIXELS));
1103 // Draw x-axis tick label.
1104 CString sTickLabel(pSeries->GetLabel());
1105 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));
1107 VERIFY(dc.TextOut(nTickXLocation - (sizTickLabel.cx / 2),
1108 m_ptOrigin.y + TICK_PIXELS + GAP_PIXELS, sTickLabel));
1110 ++nSeries;
1114 VERIFY(dc.SelectObject(pFontOld));
1115 fontXAxis.DeleteObject();
1116 fontYAxis.DeleteObject();
1117 fontTickLabels.DeleteObject();
1121 void MyGraph::DrawSeriesBar(CDC& dc) const
1123 VALIDATE;
1124 ASSERT_VALID(&dc);
1126 // How much space does each series get (includes inter series space)?
1127 // We ignore series whose members are all zero.
1128 double availableSpace = m_saLegendLabels.GetSize()
1129 ? m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)
1130 : m_nXAxisWidth;
1132 double seriesSpace = availableSpace / (double)GetNonZeroSeriesCount();
1134 // Determine width of bars. Data points with a value of zero are assumed
1135 // to be empty. This is a bad assumption.
1136 double barWidth(0.0);
1138 // This is the width of the largest series (no inter series space).
1139 double maxSeriesPlotSize(0.0);
1141 if(!m_bStackedGraph){
1142 int seriessize = GetMaxNonZeroSeriesSize();
1143 barWidth = seriessize ? seriesSpace / seriessize : 0;
1144 if (1 < GetNonZeroSeriesCount()) {
1145 barWidth *= INTERSERIES_PERCENT_USED;
1147 maxSeriesPlotSize = GetMaxNonZeroSeriesSize() * barWidth;
1149 else{
1150 barWidth = seriesSpace * INTERSERIES_PERCENT_USED;
1151 maxSeriesPlotSize = barWidth;
1154 // Iterate the series.
1155 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1156 int nSeries(0);
1158 while (pos) {
1160 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1161 ASSERT_VALID(pSeries);
1163 // Ignore unpopulated series.
1164 if (0 < pSeries->GetNonZeroElementCount()) {
1166 // Draw each bar; empty bars are not drawn.
1167 double runningLeft(m_ptOrigin.x + (nSeries + 1) * seriesSpace -
1168 maxSeriesPlotSize);
1170 double stackAccumulator(0.0);
1172 for (int nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup) {
1174 if (pSeries->GetData(nGroup)) {
1176 int nMaxDataValue(GetMaxDataValue());
1177 nMaxDataValue = max(nMaxDataValue, 1);
1178 double barTop = m_ptOrigin.y - (double)m_nYAxisHeight *
1179 pSeries->GetData(nGroup) / (double)nMaxDataValue - stackAccumulator;
1181 CRect rcBar;
1182 rcBar.left = (int)runningLeft;
1183 rcBar.top = (int)barTop;
1184 // Make adjacent bar borders overlap, so there's only one pixel border line between them.
1185 rcBar.right = (int)(runningLeft + barWidth) + 1;
1186 rcBar.bottom = (int)((double)m_ptOrigin.y - stackAccumulator) + 1;
1188 if(m_bStackedGraph){
1189 stackAccumulator = (double)m_ptOrigin.y - barTop;
1192 pSeries->SetTipRegion(nGroup, rcBar);
1194 COLORREF crBar(m_dwaColors.GetAt(nGroup));
1195 CBrush br(crBar);
1196 CBrush* pBrushOld = dc.SelectObject(&br);
1197 ASSERT_VALID(pBrushOld);
1199 VERIFY(dc.Rectangle(rcBar));
1200 dc.SelectObject(pBrushOld);
1201 br.DeleteObject();
1203 if(!m_bStackedGraph){
1204 runningLeft += barWidth;
1209 ++nSeries;
1213 if (!m_bStackedGraph) {
1214 int nMaxDataValue = max(GetMaxDataValue(), 1);
1215 double barTop = m_ptOrigin.y - (double)m_nYAxisHeight *
1216 (GetAverageDataValue() / (double)nMaxDataValue);
1217 dc.MoveTo(m_ptOrigin.x, barTop);
1218 VERIFY(dc.LineTo(m_ptOrigin.x + (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)), barTop));
1223 void MyGraph::DrawSeriesLine(CDC& dc) const
1225 VALIDATE;
1226 ASSERT_VALID(&dc);
1227 _ASSERTE(!m_bStackedGraph);
1229 // Iterate the groups.
1230 CPoint ptLastLoc(0,0);
1231 int dataLastLoc(0);
1233 for (int nGroup = 0; nGroup < GetMaxSeriesSize(); nGroup++) {
1235 // How much space does each series get (includes inter series space)?
1236 int nSeriesSpace(0);
1238 if (m_saLegendLabels.GetSize()) {
1240 nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
1241 (int)m_olMyGraphSeries.GetCount();
1243 else {
1244 nSeriesSpace = m_nXAxisWidth / (int)m_olMyGraphSeries.GetCount();
1247 // Determine width of bars.
1248 int nMaxSeriesSize(GetMaxSeriesSize());
1249 nMaxSeriesSize = max(nMaxSeriesSize, 1);
1250 int nBarWidth(nSeriesSpace / nMaxSeriesSize);
1252 if (1 < m_olMyGraphSeries.GetCount()) {
1253 nBarWidth = (int) ((double) nBarWidth * INTERSERIES_PERCENT_USED);
1256 // This is the width of the largest series (no inter series space).
1257 //int nMaxSeriesPlotSize(GetMaxSeriesSize() * nBarWidth);
1259 // Iterate the series.
1260 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1262 // Build objects.
1263 COLORREF crLine(m_dwaColors.GetAt(nGroup));
1264 CBrush br(crLine);
1265 CBrush* pBrushOld = dc.SelectObject(&br);
1266 ASSERT_VALID(pBrushOld);
1267 CPen penLine(PS_SOLID, 1, crLine);
1268 CPen* pPenOld = dc.SelectObject(&penLine);
1269 ASSERT_VALID(pPenOld);
1271 for (int nSeries = 0; nSeries < m_olMyGraphSeries.GetCount(); ++nSeries) {
1273 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1274 ASSERT_VALID(pSeries);
1276 // Get x and y location of center of ellipse.
1277 CPoint ptLoc(0,0);
1279 ptLoc.x = m_ptOrigin.x + (((nSeries + 1) * nSeriesSpace) -
1280 (nSeriesSpace / 2));
1282 int nMaxDataValue(GetMaxDataValue());
1283 nMaxDataValue = max(nMaxDataValue, 1);
1284 double dLineHeight(pSeries->GetData(nGroup) * m_nYAxisHeight /
1285 double(nMaxDataValue));
1287 ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);
1290 // Draw line back to last data member.
1291 if (nSeries > 0 && (pSeries->GetData(nGroup)!=0 || dataLastLoc != 0)) {
1293 dc.MoveTo(ptLastLoc.x, ptLastLoc.y - 1);
1294 VERIFY(dc.LineTo(ptLoc.x - 1, ptLoc.y - 1));
1297 // Now draw ellipse.
1298 CRect rcEllipse(ptLoc.x - 3, ptLoc.y - 3, ptLoc.x + 3, ptLoc.y + 3);
1299 if(pSeries->GetData(nGroup)!=0){
1300 VERIFY(dc.Ellipse(rcEllipse));
1302 if (m_olMyGraphSeries.GetCount() < 40)
1304 pSeries->SetTipRegion(nGroup, rcEllipse);
1307 // Save last pt and data
1308 ptLastLoc = ptLoc;
1309 dataLastLoc = pSeries->GetData(nGroup);
1311 VERIFY(dc.SelectObject(pPenOld));
1312 penLine.DeleteObject();
1313 VERIFY(dc.SelectObject(pBrushOld));
1314 br.DeleteObject();
1317 int nMaxDataValue = max(GetMaxDataValue(), 1);
1318 double barTop = m_ptOrigin.y - (double)m_nYAxisHeight *
1319 (GetAverageDataValue() / (double)nMaxDataValue);
1320 dc.MoveTo(m_ptOrigin.x, barTop);
1321 VERIFY(dc.LineTo(m_ptOrigin.x + (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)), barTop));
1325 void MyGraph::DrawSeriesLineStacked(CDC& dc) const
1327 VALIDATE;
1328 ASSERT_VALID(&dc);
1329 _ASSERTE(m_bStackedGraph);
1331 int nSeriesCount = (int)m_olMyGraphSeries.GetCount();
1333 CArray<int> stackAccumulator;
1334 stackAccumulator.SetSize(nSeriesCount);
1336 CArray<CPoint> polygon;
1337 // Special case: if we only have single series, make polygon
1338 // a bar instead of one pixel line.
1339 polygon.SetSize(nSeriesCount==1 ? 4 : nSeriesCount * 2);
1341 // How much space does each series get?
1342 int nSeriesSpace(0);
1343 if (m_saLegendLabels.GetSize()) {
1344 nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
1345 nSeriesCount;
1347 else {
1348 nSeriesSpace = m_nXAxisWidth / nSeriesCount;
1351 int nMaxDataValue(GetMaxDataValue());
1352 nMaxDataValue = max(nMaxDataValue, 1);
1353 double dYScaling = double(m_nYAxisHeight) / nMaxDataValue;
1355 // Iterate the groups.
1356 for (int nGroup = 0; nGroup < GetMaxSeriesSize(); nGroup++) {
1358 // Build objects.
1359 COLORREF crGroup(m_dwaColors.GetAt(nGroup));
1360 CBrush br(crGroup);
1361 CBrush* pBrushOld = dc.SelectObject(&br);
1362 ASSERT_VALID(pBrushOld);
1363 // For polygon outline, use average of this and previous color, and darken it.
1364 COLORREF crPrevGroup(nGroup > 0 ? m_dwaColors.GetAt(nGroup-1) : crGroup);
1365 COLORREF crOutline = RGB(
1366 (GetRValue(crGroup)+GetRValue(crPrevGroup))/3,
1367 (GetGValue(crGroup)+GetGValue(crPrevGroup))/3,
1368 (GetBValue(crGroup)+GetBValue(crPrevGroup))/3);
1369 CPen penLine(PS_SOLID, 1, crOutline);
1370 CPen* pPenOld = dc.SelectObject(&penLine);
1371 ASSERT_VALID(pPenOld);
1373 // Construct bottom part of polygon from current stack accumulator
1374 for (int nPolyBottom = 0; nPolyBottom < nSeriesCount; ++nPolyBottom) {
1375 CPoint ptLoc;
1376 ptLoc.x = m_ptOrigin.x + (((nPolyBottom + 1) * nSeriesSpace) - (nSeriesSpace / 2));
1377 double dLineHeight((stackAccumulator[nPolyBottom]) * dYScaling);
1378 ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);
1380 if (nSeriesCount > 1) {
1381 polygon[nSeriesCount-nPolyBottom-1] = ptLoc;
1382 } else {
1383 // special case: when there's one series, make polygon a bar
1384 polygon[0] = CPoint(ptLoc.x-GAP_PIXELS/2, ptLoc.y);
1385 polygon[1] = CPoint(ptLoc.x+GAP_PIXELS/2, ptLoc.y);
1389 // Iterate the series, construct upper part of polygon and upadte stack accumulator
1390 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1391 for (int nSeries = 0; nSeries < nSeriesCount; ++nSeries) {
1393 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1394 ASSERT_VALID(pSeries);
1396 CPoint ptLoc;
1397 ptLoc.x = m_ptOrigin.x + (((nSeries + 1) * nSeriesSpace) -
1398 (nSeriesSpace / 2));
1399 double dLineHeight((pSeries->GetData(nGroup) + stackAccumulator[nSeries]) * dYScaling);
1400 ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);
1401 if (nSeriesCount > 1) {
1402 polygon[nSeriesCount+nSeries] = ptLoc;
1403 } else {
1404 // special case: when there's one series, make polygon a bar
1405 polygon[2] = CPoint(ptLoc.x+GAP_PIXELS/2, ptLoc.y);
1406 polygon[3] = CPoint(ptLoc.x-GAP_PIXELS/2, ptLoc.y);
1409 stackAccumulator[nSeries] += pSeries->GetData(nGroup);
1412 // Draw polygon
1413 VERIFY(dc.Polygon(polygon.GetData(), (int)polygon.GetSize()));
1415 VERIFY(dc.SelectObject(pPenOld));
1416 penLine.DeleteObject();
1417 VERIFY(dc.SelectObject(pBrushOld));
1418 br.DeleteObject();
1423 void MyGraph::DrawSeriesPie(CDC& dc) const
1425 VALIDATE;
1426 ASSERT_VALID(&dc);
1428 // Determine width of pie display area (pie and space).
1429 int nSeriesSpace(0);
1431 int seriesCount = GetNonZeroSeriesCount();
1432 int horizontalSpace(0);
1434 if (m_saLegendLabels.GetSize()) {
1435 // With legend box.
1437 horizontalSpace = m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2);
1438 int nPieAndSpaceWidth(horizontalSpace / (seriesCount ? seriesCount : 1));
1440 // Height is limiting factor.
1441 if (nPieAndSpaceWidth > m_nYAxisHeight - (GAP_PIXELS * 2)) {
1442 nSeriesSpace = (m_nYAxisHeight - (GAP_PIXELS * 2));
1444 else {
1445 // Width is limiting factor.
1446 nSeriesSpace = nPieAndSpaceWidth;
1449 else {
1450 // No legend box.
1452 horizontalSpace = m_nXAxisWidth;
1454 // Height is limiting factor.
1455 if (m_nXAxisWidth > m_nYAxisHeight * (seriesCount ? seriesCount : 1)) {
1456 nSeriesSpace = m_nYAxisHeight;
1458 else {
1459 // Width is limiting factor.
1460 nSeriesSpace = m_nXAxisWidth / (seriesCount ? seriesCount : 1);
1464 // Make pies be centered horizontally
1465 int xOrigin = m_ptOrigin.x + GAP_PIXELS + (horizontalSpace - nSeriesSpace * seriesCount) / 2;
1467 // Create font for labels.
1468 CFont fontLabels;
1469 int pointFontHeight = max(m_rcGraph.Height() / Y_AXIS_LABEL_DIVISOR, MIN_FONT_SIZE);
1470 VERIFY(fontLabels.CreatePointFont(pointFontHeight, _T("Arial"), &dc));
1471 CFont* pFontOld = dc.SelectObject(&fontLabels);
1472 ASSERT_VALID(pFontOld);
1474 // Draw each pie.
1475 int nPie(0);
1476 int nRadius((int) (nSeriesSpace * INTERSERIES_PERCENT_USED / 2.0));
1477 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1479 while (pos) {
1481 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1482 ASSERT_VALID(pSeries);
1484 // Don't leave a space for empty pies.
1485 if (0 < pSeries->GetNonZeroElementCount()) {
1487 // Locate this pie.
1488 CPoint ptCenter;
1489 ptCenter.x = xOrigin + (nSeriesSpace * nPie) + nSeriesSpace / 2;
1490 ptCenter.y = m_ptOrigin.y - m_nYAxisHeight / 2;
1492 CRect rcPie;
1493 rcPie.left = ptCenter.x - nRadius;
1494 rcPie.right = ptCenter.x + nRadius;
1495 rcPie.top = ptCenter.y - nRadius;
1496 rcPie.bottom = ptCenter.y + nRadius;
1498 // Draw series label.
1499 CSize sizPieLabel(dc.GetTextExtent(pSeries->GetLabel()));
1501 VERIFY(dc.TextOut(ptCenter.x - (sizPieLabel.cx / 2),
1502 ptCenter.y + nRadius + GAP_PIXELS, pSeries->GetLabel()));
1504 // How much do the wedges total to?
1505 double dPieTotal(pSeries->GetDataTotal());
1507 // Draw each wedge in this pie.
1508 CPoint ptStart(rcPie.left, ptCenter.y);
1509 double dRunningWedgeTotal(0.0);
1511 for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {
1513 // Ignore empty wedges.
1514 if (0 < pSeries->GetData(nGroup)) {
1516 // Get the degrees of this wedge.
1517 dRunningWedgeTotal += pSeries->GetData(nGroup);
1518 double dPercent(dRunningWedgeTotal * 100.0 / dPieTotal);
1519 double degrees(360.0 * dPercent / 100.0);
1521 // Find the location of the wedge's endpoint.
1522 CPoint ptEnd(WedgeEndFromDegrees(degrees, ptCenter, nRadius));
1524 // Special case: a wedge that takes up the whole pie would
1525 // otherwise be confused with an empty wedge.
1526 bool drawEmptyWedges = false;
1527 if (1 == pSeries->GetNonZeroElementCount()) {
1528 _ASSERTE(360 == (int)degrees && ptStart == ptEnd && "This is the problem we're correcting");
1529 --ptEnd.y;
1530 drawEmptyWedges = true;
1533 // If the wedge is zero size or very narrow, don't paint it.
1534 // If pie is small, and wedge data is small, we might get a wedges
1535 // where center and both endpoints lie on the same coordinate,
1536 // and endpoints differ only in one pixel. GDI draws such pie as whole pie,
1537 // so we just skip them instead.
1538 int distance = abs(ptStart.x-ptEnd.x) + abs(ptStart.y-ptEnd.y);
1539 if (drawEmptyWedges || distance > 1) {
1541 // Draw wedge.
1542 COLORREF crWedge(m_dwaColors.GetAt(nGroup));
1543 CBrush br(crWedge);
1544 CBrush* pBrushOld = dc.SelectObject(&br);
1545 ASSERT_VALID(pBrushOld);
1546 VERIFY(dc.Pie(rcPie, ptStart, ptEnd));
1548 // Create a region from the path we create.
1549 VERIFY(dc.BeginPath());
1550 VERIFY(dc.Pie(rcPie, ptStart, ptEnd));
1551 VERIFY(dc.EndPath());
1552 std::unique_ptr<CRgn> prgnWedge (new CRgn);
1553 VERIFY(prgnWedge->CreateFromPath(&dc));
1554 pSeries->SetTipRegion(nGroup, prgnWedge.release());
1556 // Cleanup.
1557 dc.SelectObject(pBrushOld);
1558 br.DeleteObject();
1559 ptStart = ptEnd;
1564 ++nPie;
1568 // Draw X axis title
1569 CSize sizXLabel(dc.GetTextExtent(m_sXAxisLabel));
1570 VERIFY(dc.TextOut(xOrigin + (nSeriesSpace * nPie - sizXLabel.cx)/2,
1571 m_ptOrigin.y - m_nYAxisHeight/2 + nRadius + GAP_PIXELS*2 + sizXLabel.cy, m_sXAxisLabel));
1573 VERIFY(dc.SelectObject(pFontOld));
1574 fontLabels.DeleteObject();
1577 // Convert degrees to x and y coords.
1578 CPoint MyGraph::WedgeEndFromDegrees(double degrees, const CPoint& ptCenter,
1579 double radius) const
1581 VALIDATE;
1583 CPoint pt;
1585 double radians = degrees / 360.0 * M_PI * 2.0;
1587 pt.x = (int) (radius * cos(radians));
1588 pt.x = ptCenter.x - pt.x;
1590 pt.y = (int) (radius * sin(radians));
1591 pt.y = ptCenter.y + pt.y;
1593 return pt;
1596 // Spin The Message Loop: C++ version. See "Advanced Windows Programming",
1597 // M. Heller, p. 153, and the MS TechNet CD, PSS ID Number: Q99999.
1598 /* static */ UINT MyGraph::SpinTheMessageLoop(bool bNoDrawing /* = false */ ,
1599 bool bOnlyDrawing /* = false */ ,
1600 UINT uiMsgAllowed /* = WM_NULL */ )
1602 MSG msg;
1603 ::SecureZeroMemory(&msg, sizeof(msg));
1605 while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
1607 // Do painting only.
1608 if (bOnlyDrawing && WM_PAINT == msg.message) {
1609 ::TranslateMessage(&msg);
1610 ::DispatchMessage(&msg);
1612 // Update user interface.
1613 AfxGetApp()->OnIdle(0);
1615 // Do everything *but* painting.
1616 else if (bNoDrawing && WM_PAINT == msg.message) {
1617 break;
1619 // Special handling for this message.
1620 else if (WM_QUIT == msg.message) {
1621 ::PostQuitMessage(static_cast<int>(msg.wParam));
1622 break;
1624 // Allow one message (like WM_LBUTTONDOWN).
1625 else if (uiMsgAllowed == msg.message
1626 && ! AfxGetApp()->PreTranslateMessage(&msg)) {
1627 ::TranslateMessage(&msg);
1628 ::DispatchMessage(&msg);
1629 break;
1631 // This is the general case.
1632 else if (! bOnlyDrawing && ! AfxGetApp()->PreTranslateMessage(&msg)) {
1633 ::TranslateMessage(&msg);
1634 ::DispatchMessage(&msg);
1636 // Update user interface, then free temporary objects.
1637 AfxGetApp()->OnIdle(0);
1638 AfxGetApp()->OnIdle(1);
1642 return msg.message;
1646 /////////////////////////////////////////////////////////////////////////////
1647 // Conversion routines: RGB to HLS (Red-Green-Blue to Hue-Luminosity-Saturation).
1648 // See Microsoft KnowledgeBase article Q29240.
1650 #define HLSMAX 240 // H,L, and S vary over 0-HLSMAX
1651 #define RGBMAX 255 // R,G, and B vary over 0-RGBMAX
1652 // HLSMAX BEST IF DIVISIBLE BY 6
1653 // RGBMAX, HLSMAX must each fit in a byte (255).
1655 #define UNDEFINED (HLSMAX * 2 / 3) // Hue is undefined if Saturation is 0
1656 // (grey-scale). This value determines
1657 // where the Hue scrollbar is initially
1658 // set for achromatic colors.
1661 // Convert HLS to RGB.
1662 /* static */ COLORREF MyGraph::HLStoRGB(WORD wH, WORD wL, WORD wS)
1664 _ASSERTE(240 >= wH && "Illegal hue value");
1665 _ASSERTE(240 >= wL && "Illegal lum value");
1666 _ASSERTE(240 >= wS && "Illegal sat value");
1668 WORD wR(0);
1669 WORD wG(0);
1670 WORD wB(0);
1672 // Achromatic case.
1673 if (0 == wS) {
1674 wR = wG = wB = (wL * RGBMAX) / HLSMAX;
1676 if (UNDEFINED != wH) {
1677 _ASSERTE(! "ERROR");
1680 else {
1681 // Chromatic case.
1682 WORD Magic1(0);
1683 WORD Magic2(0);
1685 // Set up magic numbers.
1686 if (wL <= HLSMAX / 2) {
1687 Magic2 = (wL * (HLSMAX + wS) + (HLSMAX / 2)) / HLSMAX;
1689 else {
1690 Magic2 = wL + wS - ((wL * wS) + (HLSMAX / 2)) / HLSMAX;
1693 Magic1 = 2 * wL - Magic2;
1695 // Get RGB, change units from HLSMAX to RGBMAX.
1696 wR = (HueToRGB(Magic1, Magic2, wH + (HLSMAX / 3)) * RGBMAX + (HLSMAX / 2)) / HLSMAX;
1697 wG = (HueToRGB(Magic1, Magic2, wH) * RGBMAX + (HLSMAX / 2)) / HLSMAX;
1698 wB = (HueToRGB(Magic1, Magic2, wH - (HLSMAX / 3)) * RGBMAX + (HLSMAX / 2)) / HLSMAX;
1701 return RGB(wR,wG,wB);
1704 // Utility routine for HLStoRGB.
1705 /* static */ WORD MyGraph::HueToRGB(WORD w1, WORD w2, WORD wH)
1707 // Range check: note values passed add/subtract thirds of range.
1708 if (wH > HLSMAX) {
1709 wH -= HLSMAX;
1712 // Return r, g, or b value from this tridrant.
1713 if (wH < HLSMAX / 6) {
1714 return w1 + (((w2 - w1) * wH + (HLSMAX / 12)) / (HLSMAX / 6));
1717 if (wH < HLSMAX / 2) {
1718 return w2;
1721 if (wH < (HLSMAX * 2) / 3) {
1722 return w1 + (((w2 - w1) * (((HLSMAX * 2) / 3) - wH) + (HLSMAX / 12)) / (HLSMAX / 6));
1724 else {
1725 return w1;