Add context menu to delete all tags in Reference Browser
[TortoiseGit.git] / src / Utils / MiscUI / MyGraph.cpp
blob54fa89479c2d7cf2458a91adb78b93e78c5d4e7c
1 // MyGraph.cpp
3 #include "stdafx.h"
4 #include "MyGraph.h"
5 #include "BufferDC.h"
7 #include <cmath>
8 #include <memory>
9 using namespace std;
11 #ifdef _DEBUG
12 #define new DEBUG_NEW
13 #undef THIS_FILE
14 static char THIS_FILE[] = __FILE__;
15 #endif
18 /////////////////////////////////////////////////////////////////////////////
19 // This macro can be called at the beginning and ending of every
20 // method. It is identical to saying "ASSERT_VALID(); ASSERT_KINDOF();"
21 // but is written like this so that VALIDATE can be a macro. It is useful
22 // as an "early warning" that something has gone wrong with "this" object.
23 #ifndef VALIDATE
24 #ifdef _DEBUG
25 #define VALIDATE ::AfxAssertValidObject(this, __FILE__ , __LINE__ ); \
26 _ASSERTE(IsKindOf(GetRuntimeClass()));
27 #else
28 #define VALIDATE
29 #endif
30 #endif
33 /////////////////////////////////////////////////////////////////////////////
34 // Constants.
36 #define TICK_PIXELS 4 // Size of tick marks.
37 #define GAP_PIXELS 6 // Better if an even value.
38 #define LEGEND_COLOR_BAR_WIDTH_PIXELS 50 // Width of color bar.
39 #define LEGEND_COLOR_BAR_GAP_PIXELS 1 // Space between color bars.
40 #define Y_AXIS_TICK_COUNT_TARGET 5 // How many ticks should be there on the y axis.
41 #define MIN_FONT_SIZE 70 // The minimum font-size in pt*10.
42 #define LEGEND_VISIBILITY_THRESHOLD 300 // The width of the graph in pixels when the legend gets hidden.
44 #define INTERSERIES_PERCENT_USED 0.85 // How much of the graph is
45 // used for bars/pies (the
46 // rest is for inter-series
47 // spacing).
49 #define TITLE_DIVISOR 5 // Scale font to graph width.
50 #define LEGEND_DIVISOR 8 // Scale font to graph height.
51 #define Y_AXIS_LABEL_DIVISOR 6 // Scale font to graph height.
53 const double PI = 3.1415926535897932384626433832795;
55 /////////////////////////////////////////////////////////////////////////////
56 // MyGraphSeries
58 // Constructor.
59 MyGraphSeries::MyGraphSeries(const CString& sLabel /* = "" */ )
60 : m_sLabel(sLabel)
64 // Destructor.
65 /* virtual */ MyGraphSeries::~MyGraphSeries()
67 for (int nGroup = 0; nGroup < m_oaRegions.GetSize(); ++nGroup) {
68 delete m_oaRegions.GetAt(nGroup);
73 void MyGraphSeries::SetLabel(const CString& sLabel)
75 VALIDATE;
77 m_sLabel = sLabel;
81 void MyGraphSeries::SetData(int nGroup, int nValue)
83 VALIDATE;
84 _ASSERTE(0 <= nGroup);
86 m_dwaValues.SetAtGrow(nGroup, nValue);
90 void MyGraphSeries::SetTipRegion(int nGroup, const CRect& rc)
92 VALIDATE;
94 CRgn* prgnNew = new CRgn;
95 ASSERT_VALID(prgnNew);
97 VERIFY(prgnNew->CreateRectRgnIndirect(rc));
98 SetTipRegion(nGroup, prgnNew);
102 void MyGraphSeries::SetTipRegion(int nGroup, CRgn* prgn)
104 VALIDATE;
105 _ASSERTE(0 <= nGroup);
106 ASSERT_VALID(prgn);
108 // If there is an existing region, delete it.
109 CRgn* prgnOld = NULL;
111 if (nGroup < m_oaRegions.GetSize())
113 prgnOld = m_oaRegions.GetAt(nGroup);
114 ASSERT_NULL_OR_POINTER(prgnOld, CRgn);
117 if (prgnOld) {
118 delete prgnOld;
119 prgnOld = NULL;
122 // Add the new region.
123 m_oaRegions.SetAtGrow(nGroup, prgn);
125 _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());
129 CString MyGraphSeries::GetLabel() const
131 VALIDATE;
133 return m_sLabel;
137 int MyGraphSeries::GetData(int nGroup) const
139 VALIDATE;
140 _ASSERTE(0 <= nGroup);
141 _ASSERTE(m_dwaValues.GetSize() > nGroup);
143 return m_dwaValues[nGroup];
146 // Returns the largest data value in this series.
147 int MyGraphSeries::GetMaxDataValue(bool bStackedGraph) const
149 VALIDATE;
151 int nMax(0);
153 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
154 if(!bStackedGraph){
155 nMax = max(nMax, static_cast<int> (m_dwaValues.GetAt(nGroup)));
157 else{
158 nMax += static_cast<int> (m_dwaValues.GetAt(nGroup));
162 return nMax;
165 // Returns the number of data points that are not zero.
166 int MyGraphSeries::GetNonZeroElementCount() const
168 VALIDATE;
170 int nCount(0);
172 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
174 if (m_dwaValues.GetAt(nGroup)) {
175 ++nCount;
179 return nCount;
182 // Returns the sum of the data points for this series.
183 int MyGraphSeries::GetDataTotal() const
185 VALIDATE;
187 int nTotal(0);
189 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
190 nTotal += m_dwaValues.GetAt(nGroup);
193 return nTotal;
196 // Returns which group (if any) the sent point lies within in this series.
197 int MyGraphSeries::HitTest(const CPoint& pt, int searchStart = 0) const
199 VALIDATE;
201 for (int nGroup = searchStart; nGroup < m_oaRegions.GetSize(); ++nGroup) {
202 CRgn* prgnData = m_oaRegions.GetAt(nGroup);
203 ASSERT_NULL_OR_POINTER(prgnData, CRgn);
205 if (prgnData && prgnData->PtInRegion(pt)) {
206 return nGroup;
210 return -1;
213 // Get the series portion of the tip for this group in this series.
214 CString MyGraphSeries::GetTipText(int nGroup, const CString &unitString) const
216 VALIDATE;
217 _ASSERTE(0 <= nGroup);
218 _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());
220 CString sTip;
222 sTip.Format(_T("%d %s (%d%%)"), m_dwaValues.GetAt(nGroup),
223 unitString,
224 GetDataTotal() ? (int) (100.0 * (double) m_dwaValues.GetAt(nGroup) /
225 (double) GetDataTotal()) : 0);
227 return sTip;
231 /////////////////////////////////////////////////////////////////////////////
232 // MyGraph
234 // Constructor.
235 MyGraph::MyGraph(GraphType eGraphType /* = MyGraph::Pie */ , bool bStackedGraph /* = false */)
236 : m_nXAxisWidth(0)
237 , m_nYAxisHeight(0)
238 , m_nAxisLabelHeight(0)
239 , m_nAxisTickLabelHeight(0)
240 , m_eGraphType(eGraphType)
241 , m_bStackedGraph(bStackedGraph)
243 m_ptOrigin.x = m_ptOrigin.y = 0;
244 m_rcGraph.SetRectEmpty();
245 m_rcLegend.SetRectEmpty();
246 m_rcTitle.SetRectEmpty();
249 // Destructor.
250 /* virtual */ MyGraph::~MyGraph()
254 BEGIN_MESSAGE_MAP(MyGraph, CStatic)
255 //{{AFX_MSG_MAP(MyGraph)
256 ON_WM_PAINT()
257 ON_WM_SIZE()
258 //}}AFX_MSG_MAP
259 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnNeedText)
260 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnNeedText)
261 END_MESSAGE_MAP()
263 // Called by the framework to allow other necessary sub classing to occur
264 // before the window is sub classed.
265 void MyGraph::PreSubclassWindow()
267 VALIDATE;
269 CStatic::PreSubclassWindow();
271 VERIFY(EnableToolTips(true));
275 /////////////////////////////////////////////////////////////////////////////
276 // MyGraph message handlers
278 // Handle the tooltip messages. Returns true to mean message was handled.
279 BOOL MyGraph::OnNeedText(UINT /*uiId*/, NMHDR* pNMHDR, LRESULT* pResult)
281 _ASSERTE(pNMHDR && "Bad parameter passed");
282 _ASSERTE(pResult && "Bad parameter passed");
284 bool bReturn(false);
285 UINT_PTR uiID(pNMHDR->idFrom);
287 // Notification in NT from automatically created tooltip.
288 if (0U != uiID) {
289 bReturn = true;
291 // Need to handle both ANSI and UNICODE versions of the message.
292 TOOLTIPTEXTA* pTTTA = reinterpret_cast<TOOLTIPTEXTA*> (pNMHDR);
293 ASSERT_POINTER(pTTTA, TOOLTIPTEXTA);
295 TOOLTIPTEXTW* pTTTW = reinterpret_cast<TOOLTIPTEXTW*> (pNMHDR);
296 ASSERT_POINTER(pTTTW, TOOLTIPTEXTW);
298 CString sTipText(GetTipText());
300 #ifndef _UNICODE
301 if (TTN_NEEDTEXTA == pNMHDR->code) {
302 lstrcpyn(pTTTA->szText, sTipText, _countof(pTTTA->szText));
304 else {
305 _mbstowcsz(pTTTW->szText, sTipText, _countof(pTTTA->szText));
307 #else
308 if (pNMHDR->code == TTN_NEEDTEXTA) {
309 _wcstombsz(pTTTA->szText, sTipText, _countof(pTTTA->szText));
311 else {
312 lstrcpyn(pTTTW->szText, sTipText, _countof(pTTTA->szText));
314 #endif
316 *pResult = 0;
319 return bReturn;
322 // The framework calls this member function to determine whether a point is in
323 // the bounding rectangle of the specified tool.
324 INT_PTR MyGraph::OnToolHitTest(CPoint point, TOOLINFO* pTI) const
326 _ASSERTE(pTI && "Bad parameter passed");
328 // This works around the problem of the tip remaining visible when you move
329 // the mouse to various positions over this control.
330 INT_PTR nReturn(0);
331 static bool bTipPopped(false);
332 static CPoint ptPrev(-1,-1);
334 if (point != ptPrev) {
335 ptPrev = point;
337 if (bTipPopped) {
338 bTipPopped = false;
339 nReturn = -1;
341 else {
342 ::Sleep(50);
343 bTipPopped = true;
345 pTI->hwnd = m_hWnd;
346 pTI->uId = (UINT_PTR) m_hWnd;
347 pTI->lpszText = LPSTR_TEXTCALLBACK;
349 CRect rcWnd;
350 GetClientRect(&rcWnd);
351 pTI->rect = rcWnd;
352 nReturn = 1;
355 else {
356 nReturn = 1;
359 MyGraph::SpinTheMessageLoop();
361 return nReturn;
364 // Build the tip text for the part of the graph that the mouse is currently
365 // over.
366 CString MyGraph::GetTipText() const
368 VALIDATE;
370 CString sTip("");
372 // Get the position of the mouse.
373 CPoint pt;
374 VERIFY(::GetCursorPos(&pt));
375 ScreenToClient(&pt);
377 // Ask each part of the graph to check and see if the mouse is over it.
378 if (m_rcLegend.PtInRect(pt)) {
379 sTip = "Legend";
381 else if (m_rcTitle.PtInRect(pt)) {
382 sTip = "Title";
384 else {
385 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
387 while (pos && sTip=="") {
388 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
389 ASSERT_VALID(pSeries);
391 int nGroup(0);
393 nGroup = pSeries->HitTest(pt,nGroup);
395 if (-1 != nGroup) {
396 if("" != sTip){
397 sTip += _T(", ");
399 sTip += m_saLegendLabels.GetAt(nGroup) + _T(": ");
400 sTip += pSeries->GetTipText(nGroup, m_sYAxisLabel);
401 nGroup++;
403 }while(-1 != nGroup);
407 return sTip;
410 // Handle WM_PAINT.
411 void MyGraph::OnPaint()
413 VALIDATE;
415 CBufferDC dc(this);
416 DrawGraph(dc);
419 // Handle WM_SIZE.
420 void MyGraph::OnSize(UINT nType, int cx, int cy)
422 VALIDATE;
424 CStatic::OnSize(nType, cx, cy);
426 Invalidate();
429 // Change the type of the graph; the caller should call Invalidate() on this
430 // window to make the effect of this change visible.
431 void MyGraph::SetGraphType(GraphType e, bool bStackedGraph)
433 VALIDATE;
435 m_eGraphType = e;
436 m_bStackedGraph = bStackedGraph;
439 // Calculate the current max legend label length in pixels.
440 int MyGraph::GetMaxLegendLabelLength(CDC& dc) const
442 VALIDATE;
443 ASSERT_VALID(&dc);
445 CString sMax;
446 int nMaxChars(-1);
447 CSize siz(-1,-1);
449 // First get max number of characters.
450 for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {
451 int nLabelLength(m_saLegendLabels.GetAt(nGroup).GetLength());
453 if (nMaxChars < nLabelLength) {
454 nMaxChars = nLabelLength;
455 sMax = m_saLegendLabels.GetAt(nGroup);
459 // Now calculate the pixels.
460 siz = dc.GetTextExtent(sMax);
462 _ASSERTE(-1 < siz.cx);
464 return siz.cx;
467 // Returns the largest number of data points in any series.
468 int MyGraph::GetMaxSeriesSize() const
470 VALIDATE;
472 int nMax(0);
473 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
475 while (pos) {
476 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
477 ASSERT_VALID(pSeries);
479 nMax = max(nMax, (int)pSeries->m_dwaValues.GetSize());
482 return nMax;
485 // Returns the largest number of non-zero data points in any series.
486 int MyGraph::GetMaxNonZeroSeriesSize() const
488 VALIDATE;
490 int nMax(0);
491 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
493 while (pos) {
494 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
495 ASSERT_VALID(pSeries);
497 nMax = max(nMax, pSeries->GetNonZeroElementCount());
500 return nMax;
503 // Get the largest data value in all series.
504 int MyGraph::GetMaxDataValue() const
506 VALIDATE;
508 int nMax(0);
509 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
511 while (pos) {
512 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
513 ASSERT_VALID(pSeries);
515 nMax = max(nMax, pSeries->GetMaxDataValue(m_bStackedGraph));
518 return nMax;
521 // How many series are populated?
522 int MyGraph::GetNonZeroSeriesCount() const
524 VALIDATE;
526 int nCount(0);
527 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
529 while (pos) {
530 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
531 ASSERT_VALID(pSeries);
533 if (0 < pSeries->GetNonZeroElementCount()) {
534 ++nCount;
538 return nCount ? nCount : 1;
541 // Returns the group number for the sent label; -1 if not found.
542 int MyGraph::LookupLabel(const CString& sLabel) const
544 VALIDATE;
545 _ASSERTE(! sLabel.IsEmpty());
547 for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {
549 if (0 == sLabel.CompareNoCase(m_saLegendLabels.GetAt(nGroup))) {
550 return nGroup;
554 return -1;
557 void MyGraph::Clear()
559 m_dwaColors.RemoveAll();
560 m_saLegendLabels.RemoveAll();
561 m_olMyGraphSeries.RemoveAll();
565 void MyGraph::AddSeries(MyGraphSeries& rMyGraphSeries)
567 VALIDATE;
568 ASSERT_VALID(&rMyGraphSeries);
569 _ASSERTE(m_saLegendLabels.GetSize() == rMyGraphSeries.m_dwaValues.GetSize());
571 m_olMyGraphSeries.AddTail(&rMyGraphSeries);
575 void MyGraph::SetXAxisLabel(const CString& sLabel)
577 VALIDATE;
578 _ASSERTE(! sLabel.IsEmpty());
580 m_sXAxisLabel = sLabel;
584 void MyGraph::SetYAxisLabel(const CString& sLabel)
586 VALIDATE;
587 _ASSERTE(! sLabel.IsEmpty());
589 m_sYAxisLabel = sLabel;
592 // Returns the group number added. Also, makes sure that all the series have
593 // this many elements.
594 int MyGraph::AppendGroup(const CString& sLabel)
596 VALIDATE;
598 // Add the group.
599 int nGroup((int)m_saLegendLabels.GetSize());
600 SetLegend(nGroup, sLabel);
602 // Make sure that all series have this element.
603 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
605 while (pos) {
607 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
608 ASSERT_VALID(pSeries);
610 if (nGroup >= pSeries->m_dwaValues.GetSize()) {
611 pSeries->m_dwaValues.SetAtGrow(nGroup, 0);
615 return nGroup;
618 // Set this value to the legend.
619 void MyGraph::SetLegend(int nGroup, const CString& sLabel)
621 VALIDATE;
622 _ASSERTE(0 <= nGroup);
624 m_saLegendLabels.SetAtGrow(nGroup, sLabel);
628 void MyGraph::SetGraphTitle(const CString& sTitle)
630 VALIDATE;
631 _ASSERTE(! sTitle.IsEmpty());
633 m_sTitle = sTitle;
637 void MyGraph::DrawGraph(CDC& dc)
639 VALIDATE;
640 ASSERT_VALID(&dc);
642 if (GetMaxSeriesSize()) {
643 dc.SetBkMode(TRANSPARENT);
645 // Populate the colors as a group of evenly spaced colors of maximum
646 // saturation.
647 int nColorsDelta(240 / GetMaxSeriesSize());
649 int baseColorL = 120;
650 int diffColorL = 60;
651 DWORD backgroundColor = ::GetSysColor(COLOR_WINDOW);
652 // If graph is a non-stacked line graph, use darker colors if system window color is light.
653 #if 0
654 if (m_eGraphType == MyGraph::Line && !m_bStackedGraph) {
655 int backgroundLuma = (GetRValue(backgroundColor) + GetGValue(backgroundColor) + GetBValue(backgroundColor)) / 3;
656 if (backgroundLuma > 128) {
657 baseColorL = 70;
658 diffColorL = 50;
661 #endif
662 for (WORD nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup) {
663 WORD colorH = (WORD)(nColorsDelta * nGroup);
664 WORD colorL = (WORD)(baseColorL+(diffColorL*(nGroup%2)));
665 WORD colorS = (WORD)(180)+(30*((1-nGroup%2)*(nGroup%3)));
666 COLORREF cr(MyGraph::HLStoRGB(colorH, colorL, colorS)); // Populate colors cleverly
667 m_dwaColors.SetAtGrow(nGroup, cr);
670 // Reduce the graphable area by the frame window and status bar. We will
671 // leave GAP_PIXELS pixels blank on all sides of the graph. So top-left
672 // side of graph is at GAP_PIXELS,GAP_PIXELS and the bottom-right side
673 // of graph is at (m_rcGraph.Height() - GAP_PIXELS), (m_rcGraph.Width() -
674 // GAP_PIXELS). These settings are altered by axis labels and legends.
675 CRect rcWnd;
676 GetClientRect(&rcWnd);
677 m_rcGraph.left = GAP_PIXELS;
678 m_rcGraph.top = GAP_PIXELS;
679 m_rcGraph.right = rcWnd.Width() - GAP_PIXELS;
680 m_rcGraph.bottom = rcWnd.Height() - GAP_PIXELS;
682 CBrush br;
683 VERIFY(br.CreateSolidBrush(backgroundColor));
684 dc.FillRect(rcWnd, &br);
685 br.DeleteObject();
687 // Draw graph title.
688 DrawTitle(dc);
690 // Set the axes and origin values.
691 SetupAxes(dc);
693 // Draw legend if there is one and there's enough space.
694 if (m_saLegendLabels.GetSize() && m_rcGraph.right-m_rcGraph.left > LEGEND_VISIBILITY_THRESHOLD) {
695 DrawLegend(dc);
697 else{
698 m_rcLegend.SetRectEmpty();
701 // Draw axes unless it's a pie.
702 if (m_eGraphType != MyGraph::PieChart) {
703 DrawAxes(dc);
706 // Draw series data and labels.
707 switch (m_eGraphType) {
708 case MyGraph::Bar: DrawSeriesBar(dc); break;
709 case MyGraph::Line: if (m_bStackedGraph) DrawSeriesLineStacked(dc); else DrawSeriesLine(dc); break;
710 case MyGraph::PieChart: DrawSeriesPie(dc); break;
711 default: _ASSERTE(! "Bad default case"); break;
716 // Draw graph title; size is proportionate to width.
717 void MyGraph::DrawTitle(CDC& dc)
719 VALIDATE;
720 ASSERT_VALID(&dc);
722 // Create the title font.
723 CFont fontTitle;
724 VERIFY(fontTitle.CreatePointFont(max(m_rcGraph.Width() / TITLE_DIVISOR, MIN_FONT_SIZE),
725 _T("Arial"), &dc));
726 CFont* pFontOld = dc.SelectObject(&fontTitle);
727 ASSERT_VALID(pFontOld);
729 // Draw the title.
730 m_rcTitle.SetRect(GAP_PIXELS, GAP_PIXELS, m_rcGraph.Width() + GAP_PIXELS,
731 m_rcGraph.Height() + GAP_PIXELS);
733 dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE |
734 DT_TOP | DT_CALCRECT);
736 m_rcTitle.right = m_rcGraph.Width() + GAP_PIXELS;
738 dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE |
739 DT_TOP);
741 VERIFY(dc.SelectObject(pFontOld));
742 fontTitle.DeleteObject();
745 // Set the axes and origin values.
746 void MyGraph::SetupAxes(CDC& dc)
748 VALIDATE;
749 ASSERT_VALID(&dc);
751 // Since pie has no axis lines, set to full size minus GAP_PIXELS on each
752 // side. These are needed for legend to plot itself.
753 if (MyGraph::PieChart == m_eGraphType) {
754 m_nXAxisWidth = m_rcGraph.Width() - (GAP_PIXELS * 2);
755 m_nYAxisHeight = m_rcGraph.Height() - m_rcTitle.bottom;
756 m_ptOrigin.x = GAP_PIXELS;
757 m_ptOrigin.y = m_rcGraph.Height() - GAP_PIXELS;
759 else {
760 // Bar and Line graphs.
762 // Need to find out how wide the biggest Y-axis tick label is
764 // Get and store height of axis label font.
765 m_nAxisLabelHeight = max(m_rcGraph.Height() / Y_AXIS_LABEL_DIVISOR, MIN_FONT_SIZE);
766 // Get and store height of tick label font.
767 m_nAxisTickLabelHeight = max(int(m_nAxisLabelHeight*0.8), MIN_FONT_SIZE);
769 CFont fontTickLabels;
770 VERIFY(fontTickLabels.CreatePointFont(m_nAxisTickLabelHeight, _T("Arial"), &dc));
771 // Select font and store the old.
772 CFont* pFontOld = dc.SelectObject(&fontTickLabels);
773 ASSERT_VALID(pFontOld);
775 // Obtain tick label dimensions.
776 CString sTickLabel;
777 sTickLabel.Format(_T("%d"), GetMaxDataValue());
778 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));
780 // Set old font object again and delete temporary font object.
781 VERIFY(dc.SelectObject(pFontOld));
782 fontTickLabels.DeleteObject();
784 // Determine axis specifications.
785 m_ptOrigin.x = m_rcGraph.left + m_nAxisLabelHeight/10 + 2*GAP_PIXELS
786 + sizTickLabel.cx + GAP_PIXELS + TICK_PIXELS;
787 m_ptOrigin.y = m_rcGraph.bottom - m_nAxisLabelHeight/10 - 2*GAP_PIXELS -
788 sizTickLabel.cy - GAP_PIXELS - TICK_PIXELS;
789 m_nYAxisHeight = m_ptOrigin.y - m_rcTitle.bottom - (2 * GAP_PIXELS);
790 m_nXAxisWidth = (m_rcGraph.Width() - GAP_PIXELS) - m_ptOrigin.x;
795 void MyGraph::DrawLegend(CDC& dc)
797 VALIDATE;
798 ASSERT_VALID(&dc);
800 // Create the legend font.
801 CFont fontLegend;
802 int pointFontHeight = max(m_rcGraph.Height() / LEGEND_DIVISOR, MIN_FONT_SIZE);
803 VERIFY(fontLegend.CreatePointFont(pointFontHeight, _T("Arial"), &dc));
805 // Get the height of each label.
806 LOGFONT lf;
807 ::SecureZeroMemory(&lf, sizeof(lf));
808 VERIFY(fontLegend.GetLogFont(&lf));
809 int nLabelHeight(abs(lf.lfHeight));
811 // Get number of legend entries
812 int nLegendEntries = max(1, GetMaxSeriesSize());
814 // Calculate optimal label height = AvailableLegendHeight/AllAuthors
815 // Use a buffer of (GAP_PIXELS / 2) on each side inside the legend, and in addition the same
816 // gab above and below the legend frame, so in total 2*GAP_PIXELS
817 double optimalLabelHeight = double(m_rcGraph.Height() - 2*GAP_PIXELS)/nLegendEntries;
819 // Now relate the LabelHeight to the PointFontHeight
820 int optimalPointFontHeight = int(pointFontHeight*optimalLabelHeight/nLabelHeight);
822 // Limit the optimal PointFontHeight to the available range
823 optimalPointFontHeight = min( max(optimalPointFontHeight, MIN_FONT_SIZE), pointFontHeight);
825 // If the optimalPointFontHeight is different from the initial one, create a new legend font
826 if (optimalPointFontHeight != pointFontHeight) {
827 fontLegend.DeleteObject();
828 VERIFY(fontLegend.CreatePointFont(optimalPointFontHeight, _T("Arial"), &dc));
829 VERIFY(fontLegend.GetLogFont(&lf));
830 nLabelHeight = abs(lf.lfHeight);
833 // Calculate maximum number of authors that can be shown with the current label height
834 int nShownAuthors = (m_rcGraph.Height() - 2*GAP_PIXELS)/nLabelHeight - 1;
835 // Fix rounding errors.
836 if (nShownAuthors+1 == GetMaxSeriesSize())
837 ++nShownAuthors;
839 // Get number of authors to be shown.
840 nShownAuthors = min(nShownAuthors, GetMaxSeriesSize());
841 // nShownAuthors contains now the number of authors
843 CFont* pFontOld = dc.SelectObject(&fontLegend);
844 ASSERT_VALID(pFontOld);
846 // Determine actual size of legend. A buffer of (GAP_PIXELS / 2) on each side,
847 // plus the height of each label based on the pint size of the font.
848 int nLegendHeight = (GAP_PIXELS / 2) + (nShownAuthors * nLabelHeight) + (GAP_PIXELS / 2);
849 // Draw the legend border. Allow LEGEND_COLOR_BAR_PIXELS pixels for
850 // display of label bars.
851 m_rcLegend.top = (m_rcGraph.Height() - nLegendHeight) / 2;
852 m_rcLegend.bottom = m_rcLegend.top + nLegendHeight;
853 m_rcLegend.right = m_rcGraph.Width() - GAP_PIXELS;
854 m_rcLegend.left = m_rcLegend.right - GetMaxLegendLabelLength(dc) -
855 LEGEND_COLOR_BAR_WIDTH_PIXELS;
856 VERIFY(dc.Rectangle(m_rcLegend));
858 int skipped_row = -1; // if != -1, this is the row that we show the ... in
859 if (nShownAuthors < GetMaxSeriesSize())
860 skipped_row = nShownAuthors-2;
861 // Draw each group's label and bar.
862 for (int nGroup = 0; nGroup < nShownAuthors; ++nGroup) {
864 int nLabelTop(m_rcLegend.top + (nGroup * nLabelHeight) +
865 (GAP_PIXELS / 2));
867 int nShownGroup = nGroup; // introduce helper variable to avoid code duplication
869 // Do we have a skipped row?
870 if (skipped_row != -1)
872 if (nGroup == skipped_row) {
873 // draw the dots
874 VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop, _T("...") ));
875 continue;
877 if (nGroup == nShownAuthors-1) {
878 // we show the last group instead of the scheduled group
879 nShownGroup = GetMaxSeriesSize()-1;
882 // Draw the label.
883 VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop,
884 m_saLegendLabels.GetAt(nShownGroup)));
886 // Determine the bar.
887 CRect rcBar;
888 rcBar.left = m_rcLegend.left + GAP_PIXELS + GetMaxLegendLabelLength(dc) + GAP_PIXELS;
889 rcBar.top = nLabelTop + LEGEND_COLOR_BAR_GAP_PIXELS;
890 rcBar.right = m_rcLegend.right - GAP_PIXELS;
891 rcBar.bottom = rcBar.top + nLabelHeight - LEGEND_COLOR_BAR_GAP_PIXELS;
892 VERIFY(dc.Rectangle(rcBar));
894 // Draw bar for group.
895 COLORREF crBar(m_dwaColors.GetAt(nShownGroup));
896 CBrush br(crBar);
898 CBrush* pBrushOld = dc.SelectObject(&br);
899 ASSERT_VALID(pBrushOld);
901 rcBar.DeflateRect(LEGEND_COLOR_BAR_GAP_PIXELS, LEGEND_COLOR_BAR_GAP_PIXELS);
902 dc.FillRect(rcBar, &br);
904 dc.SelectObject(pBrushOld);
905 br.DeleteObject();
908 VERIFY(dc.SelectObject(pFontOld));
909 fontLegend.DeleteObject();
913 void MyGraph::DrawAxes(CDC& dc) const
915 VALIDATE;
916 ASSERT_VALID(&dc);
917 _ASSERTE(MyGraph::PieChart != m_eGraphType);
919 dc.SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));
921 // Draw y axis.
922 dc.MoveTo(m_ptOrigin);
923 VERIFY(dc.LineTo(m_ptOrigin.x, m_ptOrigin.y - m_nYAxisHeight));
925 // Draw x axis.
926 dc.MoveTo(m_ptOrigin);
928 if (m_saLegendLabels.GetSize()) {
930 VERIFY(dc.LineTo(m_ptOrigin.x +
931 (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)),
932 m_ptOrigin.y));
934 else {
935 VERIFY(dc.LineTo(m_ptOrigin.x + m_nXAxisWidth, m_ptOrigin.y));
938 // Note: m_nAxisLabelHeight and m_nAxisTickLabelHeight have been calculated in SetupAxis()
940 // Create the x-axis label font.
941 CFont fontXAxis;
942 VERIFY(fontXAxis.CreatePointFont(m_nAxisLabelHeight, _T("Arial"), &dc));
944 // Obtain the height of the font in device coordinates.
945 LOGFONT pLF;
946 VERIFY(fontXAxis.GetLogFont(&pLF));
947 int fontHeightDC = pLF.lfHeight;
949 // Create the y-axis label font.
950 CFont fontYAxis;
951 VERIFY(fontYAxis.CreateFont(
952 /* nHeight */ fontHeightDC,
953 /* nWidth */ 0,
954 /* nEscapement */ 90 * 10,
955 /* nOrientation */ 0,
956 /* nWeight */ FW_DONTCARE,
957 /* bItalic */ false,
958 /* bUnderline */ false,
959 /* cStrikeOut */ 0,
960 ANSI_CHARSET,
961 OUT_DEFAULT_PRECIS,
962 CLIP_DEFAULT_PRECIS,
963 PROOF_QUALITY,
964 VARIABLE_PITCH | FF_DONTCARE,
965 _T("Arial"))
968 // Set the y-axis label font and draw the label.
969 CFont* pFontOld = dc.SelectObject(&fontYAxis);
970 ASSERT_VALID(pFontOld);
971 CSize sizYLabel(dc.GetTextExtent(m_sYAxisLabel));
972 VERIFY(dc.TextOut(GAP_PIXELS, (m_rcGraph.Height() - sizYLabel.cy) / 2,
973 m_sYAxisLabel));
975 // Set the x-axis label font and draw the label.
976 VERIFY(dc.SelectObject(&fontXAxis));
977 CSize sizXLabel(dc.GetTextExtent(m_sXAxisLabel));
978 VERIFY(dc.TextOut(m_ptOrigin.x + (m_nXAxisWidth - sizXLabel.cx) / 2,
979 m_rcGraph.bottom - GAP_PIXELS - sizXLabel.cy, m_sXAxisLabel));
981 // chose suitable tick step (1, 2, 5, 10, 20, 50, etc.)
982 int nMaxDataValue(GetMaxDataValue());
983 nMaxDataValue = max(nMaxDataValue, 1);
984 int nTickStep = 1;
985 while (10 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
986 nTickStep *= 10;
988 if (5 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
989 nTickStep *= 5;
990 if (2 * nTickStep * Y_AXIS_TICK_COUNT_TARGET <= nMaxDataValue)
991 nTickStep *= 2;
993 // We hardwire TITLE_DIVISOR y-axis ticks here for simplicity.
994 int nTickCount(nMaxDataValue / nTickStep);
995 double tickSpace = (double)m_nYAxisHeight * nTickStep / (double)nMaxDataValue;
997 // create tick label font and set it in the device context
998 CFont fontTickLabels;
999 VERIFY(fontTickLabels.CreatePointFont(m_nAxisTickLabelHeight, _T("Arial"), &dc));
1000 VERIFY(dc.SelectObject(&fontTickLabels));
1002 for (int nTick = 0; nTick < nTickCount; ++nTick)
1004 int nTickYLocation = static_cast<int>(m_ptOrigin.y - tickSpace * (nTick + 1) + 0.5);
1005 dc.MoveTo(m_ptOrigin.x - TICK_PIXELS, nTickYLocation);
1006 VERIFY(dc.LineTo(m_ptOrigin.x + TICK_PIXELS, nTickYLocation));
1008 // Draw tick label.
1009 CString sTickLabel;
1010 sTickLabel.Format(_T("%d"), nTickStep * (nTick+1));
1011 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));
1013 VERIFY(dc.TextOut(m_ptOrigin.x - GAP_PIXELS - sizTickLabel.cx - TICK_PIXELS,
1014 nTickYLocation - sizTickLabel.cy/2, sTickLabel));
1017 // Draw X axis tick marks.
1018 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1019 int nSeries(0);
1021 while (pos) {
1023 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1024 ASSERT_VALID(pSeries);
1026 // Ignore unpopulated series if bar chart.
1027 if (m_eGraphType != MyGraph::Bar ||
1028 0 < pSeries->GetNonZeroElementCount()) {
1030 // Get the spacing of the series.
1031 int nSeriesSpace(0);
1033 if (m_saLegendLabels.GetSize()) {
1035 nSeriesSpace =
1036 (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
1037 (m_eGraphType == MyGraph::Bar ?
1038 GetNonZeroSeriesCount() : (int)m_olMyGraphSeries.GetCount());
1040 else {
1041 nSeriesSpace = m_nXAxisWidth / (m_eGraphType == MyGraph::Bar ?
1042 GetNonZeroSeriesCount() : (int)m_olMyGraphSeries.GetCount());
1045 int nTickXLocation(m_ptOrigin.x + ((nSeries + 1) * nSeriesSpace) -
1046 (nSeriesSpace / 2));
1048 dc.MoveTo(nTickXLocation, m_ptOrigin.y - TICK_PIXELS);
1049 VERIFY(dc.LineTo(nTickXLocation, m_ptOrigin.y + TICK_PIXELS));
1051 // Draw x-axis tick label.
1052 CString sTickLabel(pSeries->GetLabel());
1053 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));
1055 VERIFY(dc.TextOut(nTickXLocation - (sizTickLabel.cx / 2),
1056 m_ptOrigin.y + TICK_PIXELS + GAP_PIXELS, sTickLabel));
1058 ++nSeries;
1062 VERIFY(dc.SelectObject(pFontOld));
1063 fontXAxis.DeleteObject();
1064 fontYAxis.DeleteObject();
1065 fontTickLabels.DeleteObject();
1069 void MyGraph::DrawSeriesBar(CDC& dc) const
1071 VALIDATE;
1072 ASSERT_VALID(&dc);
1074 // How much space does each series get (includes inter series space)?
1075 // We ignore series whose members are all zero.
1076 double availableSpace = m_saLegendLabels.GetSize()
1077 ? m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)
1078 : m_nXAxisWidth;
1080 double seriesSpace = availableSpace / (double)GetNonZeroSeriesCount();
1082 // Determine width of bars. Data points with a value of zero are assumed
1083 // to be empty. This is a bad assumption.
1084 double barWidth(0.0);
1086 // This is the width of the largest series (no inter series space).
1087 double maxSeriesPlotSize(0.0);
1089 if(!m_bStackedGraph){
1090 barWidth = seriesSpace / GetMaxNonZeroSeriesSize();
1091 if (1 < GetNonZeroSeriesCount()) {
1092 barWidth *= INTERSERIES_PERCENT_USED;
1094 maxSeriesPlotSize = GetMaxNonZeroSeriesSize() * barWidth;
1096 else{
1097 barWidth = seriesSpace * INTERSERIES_PERCENT_USED;
1098 maxSeriesPlotSize = barWidth;
1101 // Iterate the series.
1102 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1103 int nSeries(0);
1105 while (pos) {
1107 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1108 ASSERT_VALID(pSeries);
1110 // Ignore unpopulated series.
1111 if (0 < pSeries->GetNonZeroElementCount()) {
1113 // Draw each bar; empty bars are not drawn.
1114 double runningLeft(m_ptOrigin.x + (nSeries + 1) * seriesSpace -
1115 maxSeriesPlotSize);
1117 double stackAccumulator(0.0);
1119 for (int nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup) {
1121 if (pSeries->GetData(nGroup)) {
1123 int nMaxDataValue(GetMaxDataValue());
1124 nMaxDataValue = max(nMaxDataValue, 1);
1125 double barTop = m_ptOrigin.y - (double)m_nYAxisHeight *
1126 pSeries->GetData(nGroup) / (double)nMaxDataValue - stackAccumulator;
1128 CRect rcBar;
1129 rcBar.left = (int)runningLeft;
1130 rcBar.top = (int)barTop;
1131 // Make adjacent bar borders overlap, so there's only one pixel border line between them.
1132 rcBar.right = (int)(runningLeft + barWidth) + 1;
1133 rcBar.bottom = (int)((double)m_ptOrigin.y - stackAccumulator) + 1;
1135 if(m_bStackedGraph){
1136 stackAccumulator = (double)m_ptOrigin.y - barTop;
1139 pSeries->SetTipRegion(nGroup, rcBar);
1141 COLORREF crBar(m_dwaColors.GetAt(nGroup));
1142 CBrush br(crBar);
1143 CBrush* pBrushOld = dc.SelectObject(&br);
1144 ASSERT_VALID(pBrushOld);
1146 VERIFY(dc.Rectangle(rcBar));
1147 dc.SelectObject(pBrushOld);
1148 br.DeleteObject();
1150 if(!m_bStackedGraph){
1151 runningLeft += barWidth;
1156 ++nSeries;
1162 void MyGraph::DrawSeriesLine(CDC& dc) const
1164 VALIDATE;
1165 ASSERT_VALID(&dc);
1166 _ASSERTE(!m_bStackedGraph);
1168 // Iterate the groups.
1169 CPoint ptLastLoc(0,0);
1170 int dataLastLoc(0);
1172 for (int nGroup = 0; nGroup < GetMaxSeriesSize(); nGroup++) {
1174 // How much space does each series get (includes inter series space)?
1175 int nSeriesSpace(0);
1177 if (m_saLegendLabels.GetSize()) {
1179 nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
1180 (int)m_olMyGraphSeries.GetCount();
1182 else {
1183 nSeriesSpace = m_nXAxisWidth / (int)m_olMyGraphSeries.GetCount();
1186 // Determine width of bars.
1187 int nMaxSeriesSize(GetMaxSeriesSize());
1188 nMaxSeriesSize = max(nMaxSeriesSize, 1);
1189 int nBarWidth(nSeriesSpace / nMaxSeriesSize);
1191 if (1 < m_olMyGraphSeries.GetCount()) {
1192 nBarWidth = (int) ((double) nBarWidth * INTERSERIES_PERCENT_USED);
1195 // This is the width of the largest series (no inter series space).
1196 //int nMaxSeriesPlotSize(GetMaxSeriesSize() * nBarWidth);
1198 // Iterate the series.
1199 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1201 // Build objects.
1202 COLORREF crLine(m_dwaColors.GetAt(nGroup));
1203 CBrush br(crLine);
1204 CBrush* pBrushOld = dc.SelectObject(&br);
1205 ASSERT_VALID(pBrushOld);
1206 CPen penLine(PS_SOLID, 1, crLine);
1207 CPen* pPenOld = dc.SelectObject(&penLine);
1208 ASSERT_VALID(pPenOld);
1210 for (int nSeries = 0; nSeries < m_olMyGraphSeries.GetCount(); ++nSeries) {
1212 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1213 ASSERT_VALID(pSeries);
1215 // Get x and y location of center of ellipse.
1216 CPoint ptLoc(0,0);
1218 ptLoc.x = m_ptOrigin.x + (((nSeries + 1) * nSeriesSpace) -
1219 (nSeriesSpace / 2));
1221 int nMaxDataValue(GetMaxDataValue());
1222 nMaxDataValue = max(nMaxDataValue, 1);
1223 double dLineHeight(pSeries->GetData(nGroup) * m_nYAxisHeight /
1224 nMaxDataValue);
1226 ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);
1229 // Draw line back to last data member.
1230 if (nSeries > 0 && (pSeries->GetData(nGroup)!=0 || dataLastLoc != 0)) {
1232 dc.MoveTo(ptLastLoc.x, ptLastLoc.y - 1);
1233 VERIFY(dc.LineTo(ptLoc.x - 1, ptLoc.y - 1));
1236 // Now draw ellipse.
1237 CRect rcEllipse(ptLoc.x - 3, ptLoc.y - 3, ptLoc.x + 3, ptLoc.y + 3);
1238 if(pSeries->GetData(nGroup)!=0){
1239 VERIFY(dc.Ellipse(rcEllipse));
1241 if (m_olMyGraphSeries.GetCount() < 40)
1243 pSeries->SetTipRegion(nGroup, rcEllipse);
1246 // Save last pt and data
1247 ptLastLoc = ptLoc;
1248 dataLastLoc = pSeries->GetData(nGroup);
1250 VERIFY(dc.SelectObject(pPenOld));
1251 penLine.DeleteObject();
1252 VERIFY(dc.SelectObject(pBrushOld));
1253 br.DeleteObject();
1258 void MyGraph::DrawSeriesLineStacked(CDC& dc) const
1260 VALIDATE;
1261 ASSERT_VALID(&dc);
1262 _ASSERTE(m_bStackedGraph);
1264 int nSeriesCount = (int)m_olMyGraphSeries.GetCount();
1266 CArray<int> stackAccumulator;
1267 stackAccumulator.SetSize(nSeriesCount);
1269 CArray<CPoint> polygon;
1270 // Special case: if we only have single series, make polygon
1271 // a bar instead of one pixel line.
1272 polygon.SetSize(nSeriesCount==1 ? 4 : nSeriesCount * 2);
1274 // How much space does each series get?
1275 int nSeriesSpace(0);
1276 if (m_saLegendLabels.GetSize()) {
1277 nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
1278 nSeriesCount;
1280 else {
1281 nSeriesSpace = m_nXAxisWidth / nSeriesCount;
1284 int nMaxDataValue(GetMaxDataValue());
1285 nMaxDataValue = max(nMaxDataValue, 1);
1286 double dYScaling = double(m_nYAxisHeight) / nMaxDataValue;
1288 // Iterate the groups.
1289 for (int nGroup = 0; nGroup < GetMaxSeriesSize(); nGroup++) {
1291 // Build objects.
1292 COLORREF crGroup(m_dwaColors.GetAt(nGroup));
1293 CBrush br(crGroup);
1294 CBrush* pBrushOld = dc.SelectObject(&br);
1295 ASSERT_VALID(pBrushOld);
1296 // For polygon outline, use average of this and previous color, and darken it.
1297 COLORREF crPrevGroup(nGroup > 0 ? m_dwaColors.GetAt(nGroup-1) : crGroup);
1298 COLORREF crOutline = RGB(
1299 (GetRValue(crGroup)+GetRValue(crPrevGroup))/3,
1300 (GetGValue(crGroup)+GetGValue(crPrevGroup))/3,
1301 (GetBValue(crGroup)+GetBValue(crPrevGroup))/3);
1302 CPen penLine(PS_SOLID, 1, crOutline);
1303 CPen* pPenOld = dc.SelectObject(&penLine);
1304 ASSERT_VALID(pPenOld);
1306 // Construct bottom part of polygon from current stack accumulator
1307 for (int nPolyBottom = 0; nPolyBottom < nSeriesCount; ++nPolyBottom) {
1308 CPoint ptLoc;
1309 ptLoc.x = m_ptOrigin.x + (((nPolyBottom + 1) * nSeriesSpace) - (nSeriesSpace / 2));
1310 double dLineHeight((stackAccumulator[nPolyBottom]) * dYScaling);
1311 ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);
1313 if (nSeriesCount > 1) {
1314 polygon[nSeriesCount-nPolyBottom-1] = ptLoc;
1315 } else {
1316 // special case: when there's one series, make polygon a bar
1317 polygon[0] = CPoint(ptLoc.x-GAP_PIXELS/2, ptLoc.y);
1318 polygon[1] = CPoint(ptLoc.x+GAP_PIXELS/2, ptLoc.y);
1322 // Iterate the series, construct upper part of polygon and upadte stack accumulator
1323 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1324 for (int nSeries = 0; nSeries < nSeriesCount; ++nSeries) {
1326 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1327 ASSERT_VALID(pSeries);
1329 CPoint ptLoc;
1330 ptLoc.x = m_ptOrigin.x + (((nSeries + 1) * nSeriesSpace) -
1331 (nSeriesSpace / 2));
1332 double dLineHeight((pSeries->GetData(nGroup) + stackAccumulator[nSeries]) * dYScaling);
1333 ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);
1334 if (nSeriesCount > 1) {
1335 polygon[nSeriesCount+nSeries] = ptLoc;
1336 } else {
1337 // special case: when there's one series, make polygon a bar
1338 polygon[2] = CPoint(ptLoc.x+GAP_PIXELS/2, ptLoc.y);
1339 polygon[3] = CPoint(ptLoc.x-GAP_PIXELS/2, ptLoc.y);
1342 stackAccumulator[nSeries] += pSeries->GetData(nGroup);
1345 // Draw polygon
1346 VERIFY(dc.Polygon(polygon.GetData(), (int)polygon.GetSize()));
1348 VERIFY(dc.SelectObject(pPenOld));
1349 penLine.DeleteObject();
1350 VERIFY(dc.SelectObject(pBrushOld));
1351 br.DeleteObject();
1356 void MyGraph::DrawSeriesPie(CDC& dc) const
1358 VALIDATE;
1359 ASSERT_VALID(&dc);
1361 // Determine width of pie display area (pie and space).
1362 int nSeriesSpace(0);
1364 int seriesCount = GetNonZeroSeriesCount();
1365 int horizontalSpace(0);
1367 if (m_saLegendLabels.GetSize()) {
1368 // With legend box.
1370 horizontalSpace = m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2);
1371 int nPieAndSpaceWidth(horizontalSpace / (seriesCount ? seriesCount : 1));
1373 // Height is limiting factor.
1374 if (nPieAndSpaceWidth > m_nYAxisHeight - (GAP_PIXELS * 2)) {
1375 nSeriesSpace = (m_nYAxisHeight - (GAP_PIXELS * 2));
1377 else {
1378 // Width is limiting factor.
1379 nSeriesSpace = nPieAndSpaceWidth;
1382 else {
1383 // No legend box.
1385 horizontalSpace = m_nXAxisWidth;
1387 // Height is limiting factor.
1388 if (m_nXAxisWidth > m_nYAxisHeight * (seriesCount ? seriesCount : 1)) {
1389 nSeriesSpace = m_nYAxisHeight;
1391 else {
1392 // Width is limiting factor.
1393 nSeriesSpace = m_nXAxisWidth / (seriesCount ? seriesCount : 1);
1397 // Make pies be centered horizontally
1398 int xOrigin = m_ptOrigin.x + GAP_PIXELS + (horizontalSpace - nSeriesSpace * seriesCount) / 2;
1400 // Create font for labels.
1401 CFont fontLabels;
1402 int pointFontHeight = max(m_rcGraph.Height() / Y_AXIS_LABEL_DIVISOR, MIN_FONT_SIZE);
1403 VERIFY(fontLabels.CreatePointFont(pointFontHeight, _T("Arial"), &dc));
1404 CFont* pFontOld = dc.SelectObject(&fontLabels);
1405 ASSERT_VALID(pFontOld);
1407 // Draw each pie.
1408 int nPie(0);
1409 int nRadius((int) (nSeriesSpace * INTERSERIES_PERCENT_USED / 2.0));
1410 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
1412 while (pos) {
1414 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
1415 ASSERT_VALID(pSeries);
1417 // Don't leave a space for empty pies.
1418 if (0 < pSeries->GetNonZeroElementCount()) {
1420 // Locate this pie.
1421 CPoint ptCenter;
1422 ptCenter.x = xOrigin + (nSeriesSpace * nPie) + nSeriesSpace / 2;
1423 ptCenter.y = m_ptOrigin.y - m_nYAxisHeight / 2;
1425 CRect rcPie;
1426 rcPie.left = ptCenter.x - nRadius;
1427 rcPie.right = ptCenter.x + nRadius;
1428 rcPie.top = ptCenter.y - nRadius;
1429 rcPie.bottom = ptCenter.y + nRadius;
1431 // Draw series label.
1432 CSize sizPieLabel(dc.GetTextExtent(pSeries->GetLabel()));
1434 VERIFY(dc.TextOut(ptCenter.x - (sizPieLabel.cx / 2),
1435 ptCenter.y + nRadius + GAP_PIXELS, pSeries->GetLabel()));
1437 // How much do the wedges total to?
1438 double dPieTotal(pSeries->GetDataTotal());
1440 // Draw each wedge in this pie.
1441 CPoint ptStart(rcPie.left, ptCenter.y);
1442 double dRunningWedgeTotal(0.0);
1444 for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {
1446 // Ignore empty wedges.
1447 if (0 < pSeries->GetData(nGroup)) {
1449 // Get the degrees of this wedge.
1450 dRunningWedgeTotal += pSeries->GetData(nGroup);
1451 double dPercent(dRunningWedgeTotal * 100.0 / dPieTotal);
1452 double degrees(360.0 * dPercent / 100.0);
1454 // Find the location of the wedge's endpoint.
1455 CPoint ptEnd(WedgeEndFromDegrees(degrees, ptCenter, nRadius));
1457 // Special case: a wedge that takes up the whole pie would
1458 // otherwise be confused with an empty wedge.
1459 bool drawEmptyWedges = false;
1460 if (1 == pSeries->GetNonZeroElementCount()) {
1461 _ASSERTE(360 == (int)degrees && ptStart == ptEnd && "This is the problem we're correcting");
1462 --ptEnd.y;
1463 drawEmptyWedges = true;
1466 // If the wedge is zero size or very narrow, don't paint it.
1467 // If pie is small, and wedge data is small, we might get a wedges
1468 // where center and both endpoints lie on the same coordinate,
1469 // and endpoints differ only in one pixel. GDI draws such pie as whole pie,
1470 // so we just skip them instead.
1471 int distance = abs(ptStart.x-ptEnd.x) + abs(ptStart.y-ptEnd.y);
1472 if (drawEmptyWedges || distance > 1) {
1474 // Draw wedge.
1475 COLORREF crWedge(m_dwaColors.GetAt(nGroup));
1476 CBrush br(crWedge);
1477 CBrush* pBrushOld = dc.SelectObject(&br);
1478 ASSERT_VALID(pBrushOld);
1479 VERIFY(dc.Pie(rcPie, ptStart, ptEnd));
1481 // Create a region from the path we create.
1482 VERIFY(dc.BeginPath());
1483 VERIFY(dc.Pie(rcPie, ptStart, ptEnd));
1484 VERIFY(dc.EndPath());
1485 CRgn * prgnWedge = new CRgn;
1486 VERIFY(prgnWedge->CreateFromPath(&dc));
1487 pSeries->SetTipRegion(nGroup, prgnWedge);
1489 // Cleanup.
1490 dc.SelectObject(pBrushOld);
1491 br.DeleteObject();
1492 ptStart = ptEnd;
1497 ++nPie;
1501 // Draw X axis title
1502 CSize sizXLabel(dc.GetTextExtent(m_sXAxisLabel));
1503 VERIFY(dc.TextOut(xOrigin + (nSeriesSpace * nPie - sizXLabel.cx)/2,
1504 m_ptOrigin.y - m_nYAxisHeight/2 + nRadius + GAP_PIXELS*2 + sizXLabel.cy, m_sXAxisLabel));
1506 VERIFY(dc.SelectObject(pFontOld));
1507 fontLabels.DeleteObject();
1510 // Convert degrees to x and y coords.
1511 CPoint MyGraph::WedgeEndFromDegrees(double degrees, const CPoint& ptCenter,
1512 double radius) const
1514 VALIDATE;
1516 CPoint pt;
1518 double radians = degrees / 360.0 * PI * 2.0;
1520 pt.x = (int) (radius * cos(radians));
1521 pt.x = ptCenter.x - pt.x;
1523 pt.y = (int) (radius * sin(radians));
1524 pt.y = ptCenter.y + pt.y;
1526 return pt;
1529 // Spin The Message Loop: C++ version. See "Advanced Windows Programming",
1530 // M. Heller, p. 153, and the MS TechNet CD, PSS ID Number: Q99999.
1531 /* static */ UINT MyGraph::SpinTheMessageLoop(bool bNoDrawing /* = false */ ,
1532 bool bOnlyDrawing /* = false */ ,
1533 UINT uiMsgAllowed /* = WM_NULL */ )
1535 MSG msg;
1536 ::SecureZeroMemory(&msg, sizeof(msg));
1538 while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
1540 // Do painting only.
1541 if (bOnlyDrawing && WM_PAINT == msg.message) {
1542 ::TranslateMessage(&msg);
1543 ::DispatchMessage(&msg);
1545 // Update user interface.
1546 AfxGetApp()->OnIdle(0);
1548 // Do everything *but* painting.
1549 else if (bNoDrawing && WM_PAINT == msg.message) {
1550 break;
1552 // Special handling for this message.
1553 else if (WM_QUIT == msg.message) {
1554 ::PostQuitMessage(static_cast<int>(msg.wParam));
1555 break;
1557 // Allow one message (like WM_LBUTTONDOWN).
1558 else if (uiMsgAllowed == msg.message
1559 && ! AfxGetApp()->PreTranslateMessage(&msg)) {
1560 ::TranslateMessage(&msg);
1561 ::DispatchMessage(&msg);
1562 break;
1564 // This is the general case.
1565 else if (! bOnlyDrawing && ! AfxGetApp()->PreTranslateMessage(&msg)) {
1566 ::TranslateMessage(&msg);
1567 ::DispatchMessage(&msg);
1569 // Update user interface, then free temporary objects.
1570 AfxGetApp()->OnIdle(0);
1571 AfxGetApp()->OnIdle(1);
1575 return msg.message;
1579 /////////////////////////////////////////////////////////////////////////////
1580 // Conversion routines: RGB to HLS (Red-Green-Blue to Hue-Luminosity-Saturation).
1581 // See Microsoft KnowledgeBase article Q29240.
1583 #define HLSMAX 240 // H,L, and S vary over 0-HLSMAX
1584 #define RGBMAX 255 // R,G, and B vary over 0-RGBMAX
1585 // HLSMAX BEST IF DIVISIBLE BY 6
1586 // RGBMAX, HLSMAX must each fit in a byte (255).
1588 #define UNDEFINED (HLSMAX * 2 / 3) // Hue is undefined if Saturation is 0
1589 // (grey-scale). This value determines
1590 // where the Hue scrollbar is initially
1591 // set for achromatic colors.
1594 // Convert HLS to RGB.
1595 /* static */ COLORREF MyGraph::HLStoRGB(WORD wH, WORD wL, WORD wS)
1597 _ASSERTE(0 <= wH && 240 >= wH && "Illegal hue value");
1598 _ASSERTE(0 <= wL && 240 >= wL && "Illegal lum value");
1599 _ASSERTE(0 <= wS && 240 >= wS && "Illegal sat value");
1601 WORD wR(0);
1602 WORD wG(0);
1603 WORD wB(0);
1605 // Achromatic case.
1606 if (0 == wS) {
1607 wR = wG = wB = (wL * RGBMAX) / HLSMAX;
1609 if (UNDEFINED != wH) {
1610 _ASSERTE(! "ERROR");
1613 else {
1614 // Chromatic case.
1615 WORD Magic1(0);
1616 WORD Magic2(0);
1618 // Set up magic numbers.
1619 if (wL <= HLSMAX / 2) {
1620 Magic2 = (wL * (HLSMAX + wS) + (HLSMAX / 2)) / HLSMAX;
1622 else {
1623 Magic2 = wL + wS - ((wL * wS) + (HLSMAX / 2)) / HLSMAX;
1626 Magic1 = 2 * wL - Magic2;
1628 // Get RGB, change units from HLSMAX to RGBMAX.
1629 wR = (HueToRGB(Magic1, Magic2, wH + (HLSMAX / 3)) * RGBMAX + (HLSMAX / 2)) / HLSMAX;
1630 wG = (HueToRGB(Magic1, Magic2, wH) * RGBMAX + (HLSMAX / 2)) / HLSMAX;
1631 wB = (HueToRGB(Magic1, Magic2, wH - (HLSMAX / 3)) * RGBMAX + (HLSMAX / 2)) / HLSMAX;
1634 return RGB(wR,wG,wB);
1637 // Utility routine for HLStoRGB.
1638 /* static */ WORD MyGraph::HueToRGB(WORD w1, WORD w2, WORD wH)
1640 // Range check: note values passed add/subtract thirds of range.
1641 if (wH < 0) {
1642 wH += HLSMAX;
1645 if (wH > HLSMAX) {
1646 wH -= HLSMAX;
1649 // Return r, g, or b value from this tridrant.
1650 if (wH < HLSMAX / 6) {
1651 return w1 + (((w2 - w1) * wH + (HLSMAX / 12)) / (HLSMAX / 6));
1654 if (wH < HLSMAX / 2) {
1655 return w2;
1658 if (wH < (HLSMAX * 2) / 3) {
1659 return w1 + (((w2 - w1) * (((HLSMAX * 2) / 3) - wH) + (HLSMAX / 12)) / (HLSMAX / 6));
1661 else {
1662 return w1;