OSDN Git Service

Show Ignore Sub Menu
[tortoisegit/TortoiseGitJp.git] / Utils / MiscUI / MyGraph.cpp
1 // MyGraph.cpp\r
2 \r
3 #include "stdafx.h"\r
4 #include "MyGraph.h"\r
5 #include "BufferDC.h"\r
6 \r
7 #include <cmath>\r
8 #include <memory>\r
9 using namespace std;\r
10 \r
11 #ifdef _DEBUG\r
12 #define new DEBUG_NEW\r
13 #undef THIS_FILE\r
14 static char THIS_FILE[] = __FILE__;\r
15 #endif\r
16 \r
17 \r
18 /////////////////////////////////////////////////////////////////////////////\r
19 // This macro can be called at the beginning and ending of every\r
20 // method.  It is identical to saying "ASSERT_VALID(); ASSERT_KINDOF();"\r
21 // but is written like this so that VALIDATE can be a macro.  It is useful\r
22 // as an "early warning" that something has gone wrong with "this" object.\r
23 #ifndef VALIDATE\r
24         #ifdef _DEBUG\r
25                 #define VALIDATE                ::AfxAssertValidObject(this, __FILE__ , __LINE__ ); \\r
26                                                                           _ASSERTE(IsKindOf(GetRuntimeClass()));\r
27         #else\r
28                 #define VALIDATE\r
29         #endif\r
30 #endif\r
31 \r
32 \r
33 /////////////////////////////////////////////////////////////////////////////\r
34 // Constants.\r
35 \r
36 #define TICK_PIXELS                                                               4                     // Size of tick marks.\r
37 #define GAP_PIXELS                                                                6                     // Better if an even value.\r
38 #define LEGEND_COLOR_BAR_WIDTH_PIXELS                    50                     // Width of color bar.\r
39 #define LEGEND_COLOR_BAR_GAP_PIXELS                               1                     // Space between color bars.\r
40 #define Y_AXIS_MAX_TICK_COUNT                                     5                     // How many ticks on y axis.\r
41 #define MIN_FONT_SIZE                                                    70                     // The minimum font-size in pt*10.\r
42 #define LEGEND_VISIBILITY_THRESHOLD                             300                     // The width of the graph in pixels when the legend gets hidden.\r
43 \r
44 #define INTERSERIES_PERCENT_USED                                0.85            // How much of the graph is \r
45                                                                                                                         // used for bars/pies (the \r
46                                                                                                                         // rest is for inter-series\r
47                                                                                                                         // spacing).\r
48 \r
49 #define TITLE_DIVISOR                                                    5                      // Scale font to graph width.\r
50 #define LEGEND_DIVISOR                                                   8                      // Scale font to graph height.\r
51 #define Y_AXIS_LABEL_DIVISOR                                     6                      // Scale font to graph height.\r
52 \r
53 const double PI = 3.1415926535897932384626433832795;\r
54 \r
55 /////////////////////////////////////////////////////////////////////////////\r
56 // MyGraphSeries\r
57 \r
58 // Constructor.\r
59 MyGraphSeries::MyGraphSeries(const CString& sLabel /* = "" */ )\r
60         : m_sLabel(sLabel)\r
61 {\r
62 }\r
63 \r
64 // Destructor.\r
65 /* virtual */ MyGraphSeries::~MyGraphSeries()\r
66 {\r
67         for (int nGroup = 0; nGroup < m_oaRegions.GetSize(); ++nGroup) {\r
68                 delete m_oaRegions.GetAt(nGroup);\r
69         }\r
70 }\r
71 \r
72 //\r
73 void MyGraphSeries::SetLabel(const CString& sLabel)\r
74 {\r
75         VALIDATE;\r
76 \r
77         m_sLabel = sLabel;\r
78 }\r
79 \r
80 //\r
81 void MyGraphSeries::SetData(int nGroup, int nValue)\r
82 {\r
83         VALIDATE;\r
84         _ASSERTE(0 <= nGroup);\r
85 \r
86         m_dwaValues.SetAtGrow(nGroup, nValue);\r
87 }\r
88 \r
89 //\r
90 void MyGraphSeries::SetTipRegion(int nGroup, const CRect& rc)\r
91 {\r
92         VALIDATE;\r
93         \r
94         CRgn* prgnNew = new CRgn;\r
95         ASSERT_VALID(prgnNew);\r
96 \r
97         VERIFY(prgnNew->CreateRectRgnIndirect(rc));\r
98         SetTipRegion(nGroup, prgnNew);\r
99 }\r
100 \r
101 //\r
102 void MyGraphSeries::SetTipRegion(int nGroup, CRgn* prgn)\r
103 {\r
104         VALIDATE;\r
105         _ASSERTE(0 <= nGroup);\r
106         ASSERT_VALID(prgn);\r
107 \r
108         // If there is an existing region, delete it.\r
109         CRgn* prgnOld = NULL;\r
110 \r
111         if (nGroup < m_oaRegions.GetSize()) \r
112         {\r
113                 prgnOld = m_oaRegions.GetAt(nGroup);\r
114                 ASSERT_NULL_OR_POINTER(prgnOld, CRgn);\r
115         }\r
116 \r
117         if (prgnOld) {\r
118                 delete prgnOld;\r
119                 prgnOld = NULL;\r
120         }\r
121 \r
122         // Add the new region.\r
123         m_oaRegions.SetAtGrow(nGroup, prgn);\r
124 \r
125         _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());\r
126 }\r
127 \r
128 //\r
129 CString MyGraphSeries::GetLabel() const\r
130 {\r
131         VALIDATE;\r
132 \r
133         return m_sLabel;\r
134 }\r
135 \r
136 //\r
137 int MyGraphSeries::GetData(int nGroup) const\r
138 {\r
139         VALIDATE;\r
140         _ASSERTE(0 <= nGroup);\r
141         _ASSERTE(m_dwaValues.GetSize() > nGroup);\r
142 \r
143         return m_dwaValues[nGroup];\r
144 }\r
145 \r
146 // Returns the largest data value in this series.\r
147 int MyGraphSeries::GetMaxDataValue(bool bStackedGraph) const\r
148 {\r
149         VALIDATE;\r
150 \r
151         int nMax(0);\r
152 \r
153         for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {\r
154                 if(!bStackedGraph){\r
155                         nMax = max(nMax, static_cast<int> (m_dwaValues.GetAt(nGroup)));\r
156                 }\r
157                 else{\r
158                         nMax += static_cast<int> (m_dwaValues.GetAt(nGroup));\r
159                 }\r
160         }\r
161 \r
162         return nMax;\r
163 }\r
164 \r
165 // Returns the number of data points that are not zero.\r
166 int MyGraphSeries::GetNonZeroElementCount() const\r
167 {\r
168         VALIDATE;\r
169 \r
170         int nCount(0);\r
171 \r
172         for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {\r
173                 \r
174                 if (m_dwaValues.GetAt(nGroup)) {\r
175                         ++nCount;\r
176                 }\r
177         }\r
178 \r
179         return nCount;\r
180 }\r
181 \r
182 // Returns the sum of the data points for this series.\r
183 int MyGraphSeries::GetDataTotal() const\r
184 {\r
185         VALIDATE;\r
186 \r
187         int nTotal(0);\r
188 \r
189         for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {\r
190                 nTotal += m_dwaValues.GetAt(nGroup);\r
191         }\r
192 \r
193         return nTotal;\r
194 }\r
195 \r
196 // Returns which group (if any) the sent point lies within in this series.\r
197 INT_PTR MyGraphSeries::HitTest(const CPoint& pt, int searchStart = 0) const\r
198 {\r
199         VALIDATE;\r
200 \r
201         for (int nGroup = searchStart; nGroup < m_oaRegions.GetSize(); ++nGroup) {\r
202                 CRgn* prgnData = m_oaRegions.GetAt(nGroup);\r
203                 ASSERT_NULL_OR_POINTER(prgnData, CRgn);\r
204 \r
205                 if (prgnData  &&  prgnData->PtInRegion(pt)) {\r
206                         return nGroup;\r
207                 }\r
208         }\r
209 \r
210         return -1;\r
211 }\r
212 \r
213 // Get the series portion of the tip for this group in this series.\r
214 CString MyGraphSeries::GetTipText(int nGroup) const\r
215 {\r
216         VALIDATE;\r
217         _ASSERTE(0 <= nGroup);\r
218         _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());\r
219         \r
220         CString sTip;\r
221 \r
222         sTip.Format(_T("%d (%d%%)"), m_dwaValues.GetAt(nGroup), \r
223                 GetDataTotal() ? (int) (100.0 * (double) m_dwaValues.GetAt(nGroup) / \r
224                 (double) GetDataTotal()) : 0);\r
225 \r
226         return sTip;\r
227 }\r
228 \r
229 \r
230 /////////////////////////////////////////////////////////////////////////////\r
231 // MyGraph\r
232 \r
233 // Constructor.\r
234 MyGraph::MyGraph(GraphType eGraphType /* = MyGraph::Pie */ , bool bStackedGraph /* = false */)\r
235         : m_nXAxisWidth(0)\r
236         , m_nYAxisHeight(0)\r
237         , m_eGraphType(eGraphType)\r
238         , m_bStackedGraph(bStackedGraph)\r
239 {\r
240         m_ptOrigin.x = m_ptOrigin.y = 0;\r
241         m_rcGraph.SetRectEmpty();\r
242         m_rcLegend.SetRectEmpty();\r
243         m_rcTitle.SetRectEmpty();\r
244 }\r
245 \r
246 // Destructor.\r
247 /* virtual */ MyGraph::~MyGraph()\r
248 {\r
249 }\r
250 \r
251 BEGIN_MESSAGE_MAP(MyGraph, CStatic)\r
252         //{{AFX_MSG_MAP(MyGraph)\r
253         ON_WM_PAINT()\r
254         ON_WM_SIZE()\r
255         //}}AFX_MSG_MAP\r
256         ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnNeedText)\r
257         ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnNeedText)\r
258 END_MESSAGE_MAP()\r
259 \r
260 // Called by the framework to allow other necessary sub classing to occur \r
261 // before the window is sub classed.\r
262 void MyGraph::PreSubclassWindow() \r
263 {\r
264         VALIDATE;\r
265 \r
266         CStatic::PreSubclassWindow();\r
267 \r
268         VERIFY(EnableToolTips(true));\r
269 }\r
270 \r
271 \r
272 /////////////////////////////////////////////////////////////////////////////\r
273 // MyGraph message handlers\r
274 \r
275 // Handle the tooltip messages.  Returns true to mean message was handled.\r
276 BOOL MyGraph::OnNeedText(UINT /*uiId*/, NMHDR* pNMHDR, LRESULT* pResult) \r
277 {\r
278         _ASSERTE(pNMHDR  &&  "Bad parameter passed");\r
279         _ASSERTE(pResult  &&  "Bad parameter passed");\r
280 \r
281         bool bReturn(false);\r
282         UINT_PTR uiID(pNMHDR->idFrom);\r
283 \r
284         // Notification in NT from automatically created tooltip.\r
285         if (0U != uiID) {\r
286                 bReturn = true;\r
287 \r
288                 // Need to handle both ANSI and UNICODE versions of the message.\r
289                 TOOLTIPTEXTA* pTTTA = reinterpret_cast<TOOLTIPTEXTA*> (pNMHDR);\r
290                 ASSERT_POINTER(pTTTA, TOOLTIPTEXTA);\r
291 \r
292                 TOOLTIPTEXTW* pTTTW = reinterpret_cast<TOOLTIPTEXTW*> (pNMHDR);\r
293                 ASSERT_POINTER(pTTTW, TOOLTIPTEXTW);\r
294 \r
295                 CString sTipText(GetTipText());\r
296 \r
297 #ifndef _UNICODE        \r
298                 if (TTN_NEEDTEXTA == pNMHDR->code) {\r
299                         lstrcpyn(pTTTA->szText, sTipText, sizeof(pTTTA->szText));\r
300                 }\r
301                 else {\r
302                         _mbstowcsz(pTTTW->szText, sTipText, sizeof(pTTTA->szText));\r
303                 }\r
304 #else\r
305                 if (pNMHDR->code == TTN_NEEDTEXTA) {\r
306                         _wcstombsz(pTTTA->szText, sTipText, sizeof(pTTTA->szText));\r
307                 }\r
308                 else {\r
309                         lstrcpyn(pTTTW->szText, sTipText, sizeof(pTTTA->szText));\r
310                 }\r
311 #endif\r
312  \r
313                 *pResult = 0;\r
314         }\r
315 \r
316         return bReturn;\r
317 }\r
318 \r
319 // The framework calls this member function to determine whether a point is in\r
320 // the bounding rectangle of the specified tool.\r
321 INT_PTR MyGraph::OnToolHitTest(CPoint point, TOOLINFO* pTI) const\r
322 {\r
323         _ASSERTE(pTI  &&  "Bad parameter passed");\r
324 \r
325         // This works around the problem of the tip remaining visible when you move \r
326         // the mouse to various positions over this control.\r
327         INT_PTR nReturn(0);\r
328         static bool bTipPopped(false);\r
329         static CPoint ptPrev(-1,-1);\r
330 \r
331         if (point != ptPrev) {\r
332                 ptPrev = point;\r
333 \r
334                 if (bTipPopped) {\r
335                         bTipPopped = false;\r
336                         nReturn = -1;\r
337                 }\r
338                 else {\r
339                         ::Sleep(50);\r
340                         bTipPopped = true;\r
341 \r
342                         pTI->hwnd = m_hWnd;\r
343                         pTI->uId = (UINT) m_hWnd;\r
344                         pTI->lpszText = LPSTR_TEXTCALLBACK;\r
345 \r
346                         CRect rcWnd;\r
347                         GetClientRect(&rcWnd);\r
348                         pTI->rect = rcWnd;\r
349                         nReturn = 1;\r
350                 }\r
351         }\r
352         else {\r
353                 nReturn = 1;\r
354         }\r
355 \r
356         MyGraph::SpinTheMessageLoop();\r
357 \r
358         return nReturn;\r
359 }\r
360 \r
361 // Build the tip text for the part of the graph that the mouse is currently\r
362 // over.\r
363 CString MyGraph::GetTipText() const\r
364 {\r
365         VALIDATE;\r
366 \r
367         CString sTip("");\r
368 \r
369         // Get the position of the mouse.\r
370         CPoint pt;\r
371         VERIFY(::GetCursorPos(&pt));\r
372         ScreenToClient(&pt);\r
373 \r
374         // Ask each part of the graph to check and see if the mouse is over it.\r
375         if (m_rcLegend.PtInRect(pt)) {\r
376                 sTip = "Legend";\r
377         }\r
378         else if (m_rcTitle.PtInRect(pt)) {\r
379                 sTip = "Title";\r
380         }\r
381         else {\r
382                 POSITION pos(m_olMyGraphSeries.GetHeadPosition());\r
383 \r
384                 while (pos && sTip=="") {\r
385                         MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);\r
386                         ASSERT_VALID(pSeries);\r
387 \r
388                         INT_PTR nGroup(0);\r
389                         do{\r
390                                 nGroup = pSeries->HitTest(pt,nGroup);\r
391 \r
392                                 if (-1 != nGroup) {\r
393                                         if("" != sTip){\r
394                                                 sTip += _T(", ");\r
395                                         }\r
396                                         sTip += m_saLegendLabels.GetAt(nGroup) + _T(": ");\r
397                                         sTip += pSeries->GetTipText(nGroup);\r
398                                         nGroup++;\r
399                                 }\r
400                         }while(-1 != nGroup);\r
401                 }\r
402         }\r
403 \r
404         return sTip;\r
405 }\r
406  \r
407 // Handle WM_PAINT.\r
408 void MyGraph::OnPaint() \r
409 {\r
410         VALIDATE;\r
411 \r
412         CBufferDC dc(this);\r
413         DrawGraph(dc);\r
414 }\r
415 \r
416 // Handle WM_SIZE.\r
417 void MyGraph::OnSize(UINT nType, int cx, int cy) \r
418 {\r
419         VALIDATE;\r
420 \r
421         CStatic::OnSize(nType, cx, cy);\r
422         \r
423         Invalidate();   \r
424 }\r
425 \r
426 // Change the type of the graph; the caller should call Invalidate() on this\r
427 // window to make the effect of this change visible.\r
428 void MyGraph::SetGraphType(GraphType e, bool bStackedGraph)\r
429 {\r
430         VALIDATE;\r
431 \r
432         m_eGraphType = e;\r
433         m_bStackedGraph = bStackedGraph;\r
434 }\r
435 \r
436 // Calculate the current max legend label length in pixels.\r
437 int MyGraph::GetMaxLegendLabelLength(CDC& dc) const\r
438 {\r
439         VALIDATE;\r
440         ASSERT_VALID(&dc);\r
441 \r
442         CString sMax;\r
443         int nMaxChars(-1);\r
444         CSize siz(-1,-1);\r
445 \r
446         // First get max number of characters.\r
447         for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {\r
448                 int nLabelLength(m_saLegendLabels.GetAt(nGroup).GetLength());\r
449 \r
450                 if (nMaxChars < nLabelLength) {\r
451                         nMaxChars = nLabelLength;\r
452                         sMax = m_saLegendLabels.GetAt(nGroup);\r
453                 }\r
454         }\r
455 \r
456         // Now calculate the pixels.\r
457         siz = dc.GetTextExtent(sMax);\r
458 \r
459         _ASSERTE(-1 < siz.cx);\r
460 \r
461         return siz.cx;\r
462 }\r
463 \r
464 // Returns the largest number of data points in any series.\r
465 int MyGraph::GetMaxSeriesSize() const\r
466 {\r
467         VALIDATE;\r
468 \r
469         int nMax(0);\r
470         POSITION pos(m_olMyGraphSeries.GetHeadPosition());\r
471 \r
472         while (pos) {\r
473                 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);\r
474                 ASSERT_VALID(pSeries);\r
475 \r
476                 nMax = max(nMax, pSeries->m_dwaValues.GetSize());\r
477         }\r
478 \r
479         return nMax;\r
480 }\r
481 \r
482 // Returns the largest number of non-zero data points in any series.\r
483 int MyGraph::GetMaxNonZeroSeriesSize() const\r
484 {\r
485         VALIDATE;\r
486 \r
487         int nMax(0);\r
488         POSITION pos(m_olMyGraphSeries.GetHeadPosition());\r
489 \r
490         while (pos) {\r
491                 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);\r
492                 ASSERT_VALID(pSeries);\r
493 \r
494                 nMax = max(nMax, pSeries->GetNonZeroElementCount());\r
495         }\r
496 \r
497         return nMax;\r
498 }\r
499 \r
500 // Get the largest data value in all series.\r
501 int MyGraph::GetMaxDataValue() const\r
502 {\r
503         VALIDATE;\r
504 \r
505         int nMax(0);\r
506         POSITION pos(m_olMyGraphSeries.GetHeadPosition());\r
507 \r
508         while (pos) {\r
509                 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);\r
510                 ASSERT_VALID(pSeries);\r
511 \r
512                 nMax = max(nMax, pSeries->GetMaxDataValue(m_bStackedGraph));\r
513         }\r
514 \r
515         return nMax;\r
516 }\r
517 \r
518 // How many series are populated?\r
519 int MyGraph::GetNonZeroSeriesCount() const\r
520 {\r
521         VALIDATE;\r
522 \r
523         int nCount(0);\r
524         POSITION pos(m_olMyGraphSeries.GetHeadPosition());\r
525 \r
526         while (pos) {\r
527                 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);\r
528                 ASSERT_VALID(pSeries);\r
529 \r
530                 if (0 < pSeries->GetNonZeroElementCount()) {\r
531                         ++nCount;\r
532                 }\r
533         }\r
534 \r
535         return nCount;\r
536 }\r
537 \r
538 // Returns the group number for the sent label; -1 if not found.\r
539 int MyGraph::LookupLabel(const CString& sLabel) const\r
540 {\r
541         VALIDATE;\r
542         _ASSERTE(! sLabel.IsEmpty());\r
543         \r
544         for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {\r
545 \r
546                 if (0 == sLabel.CompareNoCase(m_saLegendLabels.GetAt(nGroup))) {\r
547                         return nGroup;\r
548                 }\r
549         }\r
550 \r
551         return -1;\r
552 }\r
553 \r
554 void MyGraph::Clear()\r
555 {\r
556         m_dwaColors.RemoveAll();\r
557         m_saLegendLabels.RemoveAll();\r
558         m_olMyGraphSeries.RemoveAll();\r
559 }\r
560 \r
561 //\r
562 void MyGraph::AddSeries(MyGraphSeries& rMyGraphSeries)\r
563 {\r
564         VALIDATE;\r
565         ASSERT_VALID(&rMyGraphSeries);\r
566         _ASSERTE(m_saLegendLabels.GetSize() == rMyGraphSeries.m_dwaValues.GetSize());\r
567         \r
568         m_olMyGraphSeries.AddTail(&rMyGraphSeries);\r
569 }\r
570 \r
571 //\r
572 void MyGraph::SetXAxisLabel(const CString& sLabel)\r
573 {\r
574         VALIDATE;\r
575         _ASSERTE(! sLabel.IsEmpty());\r
576 \r
577         m_sXAxisLabel = sLabel;\r
578 }\r
579 \r
580 //\r
581 void MyGraph::SetYAxisLabel(const CString& sLabel)\r
582 {\r
583         VALIDATE;\r
584         _ASSERTE(! sLabel.IsEmpty());\r
585 \r
586         m_sYAxisLabel = sLabel;\r
587 }\r
588 \r
589 // Returns the group number added.  Also, makes sure that all the series have \r
590 // this many elements.\r
591 int MyGraph::AppendGroup(const CString& sLabel)\r
592 {\r
593         VALIDATE;\r
594 \r
595         // Add the group.\r
596         int nGroup(m_saLegendLabels.GetSize());\r
597         SetLegend(nGroup, sLabel);\r
598 \r
599         // Make sure that all series have this element.\r
600         POSITION pos(m_olMyGraphSeries.GetHeadPosition());\r
601 \r
602         while (pos) {\r
603 \r
604                 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);\r
605                 ASSERT_VALID(pSeries);\r
606 \r
607                 if (nGroup >= pSeries->m_dwaValues.GetSize()) {\r
608                         pSeries->m_dwaValues.SetAtGrow(nGroup, 0);\r
609                 }\r
610         }\r
611 \r
612         return nGroup;\r
613 }\r
614 \r
615 // Set this value to the legend.\r
616 void MyGraph::SetLegend(int nGroup, const CString& sLabel)\r
617 {\r
618         VALIDATE;\r
619         _ASSERTE(0 <= nGroup);\r
620 \r
621         m_saLegendLabels.SetAtGrow(nGroup, sLabel);\r
622 }\r
623 \r
624 //\r
625 void MyGraph::SetGraphTitle(const CString& sTitle)\r
626 {\r
627         VALIDATE;\r
628         _ASSERTE(! sTitle.IsEmpty());\r
629 \r
630         m_sTitle = sTitle;\r
631 }\r
632 \r
633 //\r
634 void MyGraph::DrawGraph(CDC& dc)\r
635 {\r
636         VALIDATE;\r
637         ASSERT_VALID(&dc);\r
638 \r
639         if (GetMaxSeriesSize()) {\r
640                 dc.SetBkMode(TRANSPARENT);\r
641 \r
642                 // Populate the colors as a group of evenly spaced colors of maximum\r
643                 // saturation.\r
644                 int nColorsDelta(240 / GetMaxSeriesSize());\r
645 \r
646                 int baseColorL = 120;\r
647                 int diffColorL = 60;\r
648                 DWORD backgroundColor = ::GetSysColor(COLOR_WINDOW);\r
649                 // If graph is a non-stacked line graph, use darker colors if system window color is light.\r
650                 if (m_eGraphType == MyGraph::Line && !m_bStackedGraph) {\r
651                         int backgroundLuma = (GetRValue(backgroundColor) + GetGValue(backgroundColor) + GetBValue(backgroundColor)) / 3;\r
652                         if (backgroundLuma > 128) {\r
653                                 baseColorL = 70;\r
654                                 diffColorL = 50;\r
655                         }\r
656                 }\r
657                 \r
658                 for (WORD nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup) {\r
659                         WORD colorH = (WORD)(nColorsDelta * nGroup);\r
660                         WORD colorL = (WORD)(baseColorL+(diffColorL*(nGroup%2)));\r
661                         WORD colorS = (WORD)(180)+(30*((1-nGroup%2)*(nGroup%3)));\r
662                         COLORREF cr(MyGraph::HLStoRGB(colorH, colorL, colorS)); // Populate colors cleverly\r
663                         m_dwaColors.SetAtGrow(nGroup, cr);\r
664                 }\r
665 \r
666                 // Reduce the graphable area by the frame window and status bar.  We will\r
667                 // leave GAP_PIXELS pixels blank on all sides of the graph.  So top-left \r
668                 // side of graph is at GAP_PIXELS,GAP_PIXELS and the bottom-right side \r
669                 // of graph is at (m_rcGraph.Height() - GAP_PIXELS), (m_rcGraph.Width() -\r
670                 // GAP_PIXELS).  These settings are altered by axis labels and legends.\r
671                 CRect rcWnd;\r
672                 GetClientRect(&rcWnd);\r
673                 m_rcGraph.left = GAP_PIXELS;\r
674                 m_rcGraph.top = GAP_PIXELS;\r
675                 m_rcGraph.right = rcWnd.Width() - GAP_PIXELS;\r
676                 m_rcGraph.bottom = rcWnd.Height() - GAP_PIXELS;\r
677 \r
678                 CBrush br;\r
679                 VERIFY(br.CreateSolidBrush(backgroundColor));\r
680                 dc.FillRect(rcWnd, &br);\r
681                 br.DeleteObject();\r
682 \r
683                 // Draw graph title.\r
684                 DrawTitle(dc);\r
685 \r
686                 // Set the axes and origin values.\r
687                 SetupAxes(dc);\r
688 \r
689                 // Draw legend if there is one and there's enough space.\r
690                 if (m_saLegendLabels.GetSize() && m_rcGraph.right-m_rcGraph.left > LEGEND_VISIBILITY_THRESHOLD) {\r
691                         DrawLegend(dc);\r
692                 }\r
693                 else{\r
694                         m_rcLegend.SetRectEmpty();\r
695                 }\r
696 \r
697                 // Draw axes unless it's a pie.\r
698                 if (m_eGraphType != MyGraph::PieChart) {\r
699                         DrawAxes(dc);\r
700                 }\r
701 \r
702                 // Draw series data and labels.\r
703                 switch (m_eGraphType) {\r
704                         case MyGraph::Bar:  DrawSeriesBar(dc);  break;\r
705                         case MyGraph::Line: if (m_bStackedGraph) DrawSeriesLineStacked(dc); else DrawSeriesLine(dc); break;\r
706                         case MyGraph::PieChart:  DrawSeriesPie(dc);  break;\r
707                         default: _ASSERTE(! "Bad default case"); break;\r
708                 }\r
709         }\r
710 }\r
711 \r
712 // Draw graph title; size is proportionate to width.\r
713 void MyGraph::DrawTitle(CDC& dc)\r
714 {\r
715         VALIDATE;\r
716         ASSERT_VALID(&dc);\r
717 \r
718         // Create the title font.\r
719         CFont fontTitle;\r
720         VERIFY(fontTitle.CreatePointFont(max(m_rcGraph.Width() / TITLE_DIVISOR, MIN_FONT_SIZE),\r
721                 _T("Arial"), &dc));\r
722         CFont* pFontOld = dc.SelectObject(&fontTitle);\r
723         ASSERT_VALID(pFontOld);\r
724 \r
725         // Draw the title.\r
726         m_rcTitle.SetRect(GAP_PIXELS, GAP_PIXELS, m_rcGraph.Width() + GAP_PIXELS,\r
727                 m_rcGraph.Height() + GAP_PIXELS);\r
728 \r
729         dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE |\r
730                 DT_TOP | DT_CALCRECT);\r
731 \r
732         m_rcTitle.right = m_rcGraph.Width() + GAP_PIXELS;\r
733 \r
734         dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE | \r
735                 DT_TOP);\r
736 \r
737         VERIFY(dc.SelectObject(pFontOld));\r
738         fontTitle.DeleteObject();\r
739 }\r
740 \r
741 // Set the axes and origin values.\r
742 void MyGraph::SetupAxes(CDC& dc)\r
743 {\r
744         VALIDATE;\r
745         ASSERT_VALID(&dc);\r
746 \r
747         // Since pie has no axis lines, set to full size minus GAP_PIXELS on each \r
748         // side.  These are needed for legend to plot itself.\r
749         if (MyGraph::PieChart == m_eGraphType) {\r
750                 m_nXAxisWidth = m_rcGraph.Width() - (GAP_PIXELS * 2);\r
751                 m_nYAxisHeight = m_rcGraph.Height() - m_rcTitle.bottom;\r
752                 m_ptOrigin.x = GAP_PIXELS;\r
753                 m_ptOrigin.y = m_rcGraph.Height() - GAP_PIXELS;\r
754         }\r
755         else {\r
756                 // Bar and Line graphs.\r
757 \r
758                 // Need to find out how wide the biggest Y-axis tick label is\r
759 \r
760                 // Get and store height of axis label font.\r
761                 m_nAxisLabelHeight = max(m_rcGraph.Height() / Y_AXIS_LABEL_DIVISOR, MIN_FONT_SIZE);\r
762                 // Get and store height of tick label font.\r
763                 m_nAxisTickLabelHeight = max(int(m_nAxisLabelHeight*0.8), MIN_FONT_SIZE);\r
764 \r
765                 CFont fontTickLabels;\r
766                 VERIFY(fontTickLabels.CreatePointFont(m_nAxisTickLabelHeight, _T("Arial"), &dc));\r
767                 // Select font and store the old.\r
768                 CFont* pFontOld = dc.SelectObject(&fontTickLabels);\r
769                 ASSERT_VALID(pFontOld);\r
770 \r
771                 // Obtain tick label dimensions.\r
772                 CString sTickLabel;\r
773                 sTickLabel.Format(_T("%d"), GetMaxDataValue());\r
774                 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));\r
775 \r
776                 // Set old font object again and delete temporary font object.\r
777                 VERIFY(dc.SelectObject(pFontOld));\r
778                 fontTickLabels.DeleteObject();          \r
779 \r
780                 // Determine axis specifications.\r
781                 m_ptOrigin.x = m_rcGraph.left + m_nAxisLabelHeight/10 + 2*GAP_PIXELS \r
782                         + sizTickLabel.cx + GAP_PIXELS + TICK_PIXELS;\r
783                 m_ptOrigin.y = m_rcGraph.bottom - m_nAxisLabelHeight/10 - 2*GAP_PIXELS - \r
784                         sizTickLabel.cy - GAP_PIXELS - TICK_PIXELS;\r
785                 m_nYAxisHeight = m_ptOrigin.y - m_rcTitle.bottom - (2 * GAP_PIXELS);\r
786                 m_nXAxisWidth = (m_rcGraph.Width() - GAP_PIXELS) - m_ptOrigin.x;\r
787         }\r
788 }\r
789 \r
790 //\r
791 void MyGraph::DrawLegend(CDC& dc)\r
792 {\r
793         VALIDATE;\r
794         ASSERT_VALID(&dc);\r
795 \r
796         // Create the legend font.\r
797         CFont fontLegend;\r
798         int pointFontHeight = max(m_rcGraph.Height() / LEGEND_DIVISOR, MIN_FONT_SIZE);\r
799         VERIFY(fontLegend.CreatePointFont(pointFontHeight, _T("Arial"), &dc));\r
800 \r
801         // Get the height of each label.\r
802         LOGFONT lf;\r
803         ::SecureZeroMemory(&lf, sizeof(lf));\r
804         VERIFY(fontLegend.GetLogFont(&lf));\r
805         int nLabelHeight(abs(lf.lfHeight));\r
806 \r
807         // Get number of legend entries\r
808         int nLegendEntries = max(1, GetMaxSeriesSize());\r
809 \r
810         // Calculate optimal label height = AvailableLegendHeight/AllAuthors\r
811         // Use a buffer of (GAP_PIXELS / 2) on each side inside the legend, and in addition the same\r
812         // gab above and below the legend frame, so in total 2*GAP_PIXELS\r
813         double optimalLabelHeight = double(m_rcGraph.Height() - 2*GAP_PIXELS)/nLegendEntries;\r
814 \r
815         // Now relate the LabelHeight to the PointFontHeight\r
816         int optimalPointFontHeight = int(pointFontHeight*optimalLabelHeight/nLabelHeight);\r
817 \r
818         // Limit the optimal PointFontHeight to the available range\r
819         optimalPointFontHeight = min( max(optimalPointFontHeight, MIN_FONT_SIZE), pointFontHeight);\r
820 \r
821         // If the optimalPointFontHeight is different from the initial one, create a new legend font\r
822         if (optimalPointFontHeight != pointFontHeight) {\r
823                 fontLegend.DeleteObject();\r
824                 VERIFY(fontLegend.CreatePointFont(optimalPointFontHeight, _T("Arial"), &dc));\r
825                 VERIFY(fontLegend.GetLogFont(&lf));\r
826                 nLabelHeight = abs(lf.lfHeight);\r
827         }\r
828 \r
829         // Calculate maximum number of authors that can be shown with the current label height\r
830         int nShownAuthors = (m_rcGraph.Height() - 2*GAP_PIXELS)/nLabelHeight - 1;\r
831         // Fix rounding errors.\r
832         if (nShownAuthors+1 == GetMaxSeriesSize()) \r
833                 ++nShownAuthors;\r
834 \r
835         // Get number of authors to be shown.\r
836         nShownAuthors = min(nShownAuthors, GetMaxSeriesSize());\r
837         // nShownAuthors contains now the number of authors\r
838 \r
839         CFont* pFontOld = dc.SelectObject(&fontLegend);\r
840         ASSERT_VALID(pFontOld);\r
841 \r
842         // Determine actual size of legend.  A buffer of (GAP_PIXELS / 2) on each side, \r
843         // plus the height of each label based on the pint size of the font.\r
844         int nLegendHeight = (GAP_PIXELS / 2) + (nShownAuthors * nLabelHeight) + (GAP_PIXELS / 2);\r
845         // Draw the legend border.  Allow LEGEND_COLOR_BAR_PIXELS pixels for\r
846         // display of label bars.\r
847         m_rcLegend.top = (m_rcGraph.Height() - nLegendHeight) / 2;\r
848         m_rcLegend.bottom = m_rcLegend.top + nLegendHeight;\r
849         m_rcLegend.right = m_rcGraph.Width() - GAP_PIXELS;\r
850         m_rcLegend.left = m_rcLegend.right - GetMaxLegendLabelLength(dc) - \r
851                 LEGEND_COLOR_BAR_WIDTH_PIXELS;\r
852         VERIFY(dc.Rectangle(m_rcLegend));\r
853 \r
854         int skipped_row = -1; // if != -1, this is the row that we show the ... in\r
855         if (nShownAuthors < GetMaxSeriesSize())\r
856                 skipped_row = nShownAuthors-2;\r
857         // Draw each group's label and bar.\r
858         for (int nGroup = 0; nGroup < nShownAuthors; ++nGroup) {\r
859 \r
860                 int nLabelTop(m_rcLegend.top + (nGroup * nLabelHeight) +\r
861                         (GAP_PIXELS / 2));\r
862 \r
863                 int nShownGroup = nGroup; // introduce helper variable to avoid code duplication\r
864 \r
865                 // Do we have a skipped row?\r
866                 if (skipped_row != -1) \r
867                 {\r
868                         if (nGroup == skipped_row) {\r
869                                 // draw the dots\r
870                                 VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop, _T("...") ));\r
871                                 continue;\r
872                         }\r
873                         if (nGroup == nShownAuthors-1) {\r
874                                 // we show the last group instead of the scheduled group\r
875                                 nShownGroup = GetMaxSeriesSize()-1;\r
876                         }\r
877                 }\r
878                 // Draw the label.\r
879                 VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop,\r
880                         m_saLegendLabels.GetAt(nShownGroup)));\r
881 \r
882                 // Determine the bar.\r
883                 CRect rcBar;\r
884                 rcBar.left = m_rcLegend.left + GAP_PIXELS + GetMaxLegendLabelLength(dc) + GAP_PIXELS;\r
885                 rcBar.top = nLabelTop + LEGEND_COLOR_BAR_GAP_PIXELS;\r
886                 rcBar.right = m_rcLegend.right - GAP_PIXELS;\r
887                 rcBar.bottom = rcBar.top + nLabelHeight - LEGEND_COLOR_BAR_GAP_PIXELS;\r
888                 VERIFY(dc.Rectangle(rcBar));\r
889 \r
890                 // Draw bar for group.\r
891                 COLORREF crBar(m_dwaColors.GetAt(nShownGroup));\r
892                 CBrush br(crBar);\r
893 \r
894                 CBrush* pBrushOld = dc.SelectObject(&br);\r
895                 ASSERT_VALID(pBrushOld);\r
896 \r
897                 rcBar.DeflateRect(LEGEND_COLOR_BAR_GAP_PIXELS, LEGEND_COLOR_BAR_GAP_PIXELS);\r
898                 dc.FillRect(rcBar, &br);\r
899 \r
900                 dc.SelectObject(pBrushOld);\r
901                 br.DeleteObject();\r
902         }\r
903 \r
904         VERIFY(dc.SelectObject(pFontOld));\r
905         fontLegend.DeleteObject();\r
906 }\r
907 \r
908 //\r
909 void MyGraph::DrawAxes(CDC& dc) const\r
910 {\r
911         VALIDATE;\r
912         ASSERT_VALID(&dc);\r
913         _ASSERTE(MyGraph::PieChart != m_eGraphType);\r
914 \r
915         dc.SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));\r
916 \r
917         // Draw y axis.\r
918         dc.MoveTo(m_ptOrigin);  \r
919         VERIFY(dc.LineTo(m_ptOrigin.x, m_ptOrigin.y - m_nYAxisHeight));\r
920 \r
921         // Draw x axis.\r
922         dc.MoveTo(m_ptOrigin);  \r
923 \r
924         if (m_saLegendLabels.GetSize()) {\r
925 \r
926                 VERIFY(dc.LineTo(m_ptOrigin.x + \r
927                         (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)), \r
928                         m_ptOrigin.y));\r
929         }\r
930         else {\r
931                 VERIFY(dc.LineTo(m_ptOrigin.x + m_nXAxisWidth, m_ptOrigin.y));\r
932         }\r
933 \r
934         // Note: m_nAxisLabelHeight and m_nAxisTickLabelHeight have been calculated in SetupAxis()\r
935 \r
936         // Create the x-axis label font.\r
937         CFont fontXAxis;\r
938         VERIFY(fontXAxis.CreatePointFont(m_nAxisLabelHeight, _T("Arial"), &dc));\r
939 \r
940         // Obtain the height of the font in device coordinates.\r
941         LOGFONT pLF;\r
942         VERIFY(fontXAxis.GetLogFont(&pLF));\r
943         int fontHeightDC = pLF.lfHeight;\r
944 \r
945         // Create the y-axis label font.\r
946         CFont fontYAxis;\r
947         VERIFY(fontYAxis.CreateFont( \r
948                 /* nHeight */ fontHeightDC,\r
949                 /* nWidth */ 0, \r
950                 /* nEscapement */ 90 * 10, \r
951                 /* nOrientation */ 0,\r
952                 /* nWeight */ FW_DONTCARE,      \r
953                 /* bItalic */ false, \r
954                 /* bUnderline */ false,\r
955                 /* cStrikeOut */ 0, \r
956                 ANSI_CHARSET, \r
957                 OUT_DEFAULT_PRECIS,\r
958                 CLIP_DEFAULT_PRECIS, \r
959                 PROOF_QUALITY, \r
960                 VARIABLE_PITCH | FF_DONTCARE, \r
961                 _T("Arial"))\r
962         );\r
963 \r
964         // Set the y-axis label font and draw the label.\r
965         CFont* pFontOld = dc.SelectObject(&fontYAxis);\r
966         ASSERT_VALID(pFontOld);\r
967         CSize sizYLabel(dc.GetTextExtent(m_sYAxisLabel));\r
968         VERIFY(dc.TextOut(GAP_PIXELS, (m_rcGraph.Height() - sizYLabel.cy) / 2,\r
969                 m_sYAxisLabel));\r
970 \r
971         // Set the x-axis label font and draw the label.\r
972         VERIFY(dc.SelectObject(&fontXAxis));\r
973         CSize sizXLabel(dc.GetTextExtent(m_sXAxisLabel));\r
974         VERIFY(dc.TextOut(m_ptOrigin.x + (m_nXAxisWidth - sizXLabel.cx) / 2,\r
975                 m_rcGraph.bottom - GAP_PIXELS - sizXLabel.cy, m_sXAxisLabel));\r
976 \r
977         // We hardwire TITLE_DIVISOR y-axis ticks here for simplicity.\r
978         int nTickCount(min(Y_AXIS_MAX_TICK_COUNT, GetMaxDataValue()));\r
979         int nTickSpace(m_nYAxisHeight / nTickCount);\r
980 \r
981         // create tick label font and set it in the device context\r
982         CFont fontTickLabels;\r
983         VERIFY(fontTickLabels.CreatePointFont(m_nAxisTickLabelHeight, _T("Arial"), &dc));\r
984         VERIFY(dc.SelectObject(&fontTickLabels));\r
985 \r
986         for (int nTick = 0; nTick < nTickCount; ++nTick) {\r
987                 int nTickYLocation(m_ptOrigin.y - (nTickSpace * (nTick + 1)));\r
988                 dc.MoveTo(m_ptOrigin.x - TICK_PIXELS, nTickYLocation);\r
989                 VERIFY(dc.LineTo(m_ptOrigin.x + TICK_PIXELS, nTickYLocation));\r
990 \r
991                 // Draw tick label.\r
992                 CString sTickLabel;\r
993                 sTickLabel.Format(_T("%d"), (GetMaxDataValue() * (nTick + 1)) / nTickCount);\r
994                 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));\r
995                 \r
996                 VERIFY(dc.TextOut(m_ptOrigin.x - GAP_PIXELS - sizTickLabel.cx - TICK_PIXELS,\r
997                         nTickYLocation - sizTickLabel.cy/2, sTickLabel));\r
998         }\r
999 \r
1000         // Draw X axis tick marks.\r
1001         POSITION pos(m_olMyGraphSeries.GetHeadPosition());\r
1002         int nSeries(0);\r
1003 \r
1004         while (pos) {\r
1005                 \r
1006                 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);\r
1007                 ASSERT_VALID(pSeries);\r
1008 \r
1009                 // Ignore unpopulated series if bar chart.\r
1010                 if (m_eGraphType != MyGraph::Bar  || \r
1011                         0 < pSeries->GetNonZeroElementCount()) {\r
1012 \r
1013                         // Get the spacing of the series.\r
1014                         _ASSERTE(GetNonZeroSeriesCount()  &&  "Div by zero coming");\r
1015                         int nSeriesSpace(0);\r
1016 \r
1017                         if (m_saLegendLabels.GetSize()) {\r
1018 \r
1019                                 nSeriesSpace =\r
1020                                         (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /\r
1021                                         (m_eGraphType == MyGraph::Bar ?\r
1022                                         GetNonZeroSeriesCount() : m_olMyGraphSeries.GetCount());\r
1023                         }\r
1024                         else {\r
1025                                 nSeriesSpace = m_nXAxisWidth / (m_eGraphType == MyGraph::Bar ?\r
1026                                         GetNonZeroSeriesCount() : m_olMyGraphSeries.GetCount());\r
1027                         }\r
1028 \r
1029                         int nTickXLocation(m_ptOrigin.x + ((nSeries + 1) * nSeriesSpace) -\r
1030                                 (nSeriesSpace / 2));\r
1031 \r
1032                         dc.MoveTo(nTickXLocation, m_ptOrigin.y - TICK_PIXELS);\r
1033                         VERIFY(dc.LineTo(nTickXLocation, m_ptOrigin.y + TICK_PIXELS));\r
1034 \r
1035                         // Draw x-axis tick label.\r
1036                         CString sTickLabel(pSeries->GetLabel());\r
1037                         CSize sizTickLabel(dc.GetTextExtent(sTickLabel));\r
1038 \r
1039                         VERIFY(dc.TextOut(nTickXLocation - (sizTickLabel.cx / 2),\r
1040                                 m_ptOrigin.y + TICK_PIXELS + GAP_PIXELS, sTickLabel));\r
1041 \r
1042                         ++nSeries;\r
1043                 }\r
1044         }\r
1045 \r
1046         VERIFY(dc.SelectObject(pFontOld));\r
1047         fontXAxis.DeleteObject();\r
1048         fontYAxis.DeleteObject();\r
1049         fontTickLabels.DeleteObject();\r
1050 }\r
1051 \r
1052 //\r
1053 void MyGraph::DrawSeriesBar(CDC& dc) const\r
1054 {\r
1055         VALIDATE;\r
1056         ASSERT_VALID(&dc);\r
1057 \r
1058         // How much space does each series get (includes inter series space)?\r
1059         // We ignore series whose members are all zero.\r
1060         int nSeriesSpace(0);\r
1061 \r
1062         if (m_saLegendLabels.GetSize()) {\r
1063 \r
1064                 nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /\r
1065                         GetNonZeroSeriesCount();\r
1066         }\r
1067         else {\r
1068                 nSeriesSpace = m_nXAxisWidth / GetNonZeroSeriesCount();\r
1069         }\r
1070 \r
1071         // Determine width of bars.  Data points with a value of zero are assumed \r
1072         // to be empty.  This is a bad assumption.\r
1073         int nBarWidth(0);\r
1074 \r
1075         // This is the width of the largest series (no inter series space).\r
1076         int nMaxSeriesPlotSize(0);\r
1077 \r
1078         if(!m_bStackedGraph){\r
1079                 nBarWidth = nSeriesSpace / GetMaxNonZeroSeriesSize();\r
1080                 if (1 < GetNonZeroSeriesCount()) {\r
1081                         nBarWidth = (int) ((double) nBarWidth * INTERSERIES_PERCENT_USED);\r
1082                 }\r
1083                 nMaxSeriesPlotSize = GetMaxNonZeroSeriesSize() * nBarWidth;\r
1084         }\r
1085         else{\r
1086                 nBarWidth = (int) ((double) nSeriesSpace * INTERSERIES_PERCENT_USED);\r
1087                 nMaxSeriesPlotSize = nBarWidth;\r
1088         }\r
1089 \r
1090         // Iterate the series.\r
1091         POSITION pos(m_olMyGraphSeries.GetHeadPosition());\r
1092         int nSeries(0);\r
1093 \r
1094         while (pos) {\r
1095                 \r
1096                 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);\r
1097                 ASSERT_VALID(pSeries);\r
1098 \r
1099                 // Ignore unpopulated series.\r
1100                 if (0 < pSeries->GetNonZeroElementCount()) {\r
1101 \r
1102                         // Draw each bar; empty bars are not drawn.\r
1103                         int nRunningLeft(m_ptOrigin.x + ((nSeries + 1) * nSeriesSpace) - \r
1104                                 nMaxSeriesPlotSize);\r
1105 \r
1106                         int stackAccumulator(0);\r
1107 \r
1108                         for (int nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup) {\r
1109 \r
1110                                 if (pSeries->GetData(nGroup)) {\r
1111 \r
1112                                         CRect rcBar;\r
1113                                         rcBar.left = nRunningLeft; \r
1114                                         rcBar.top = (m_ptOrigin.y - (m_nYAxisHeight *\r
1115                                                 pSeries->GetData(nGroup)) / GetMaxDataValue()) - stackAccumulator;\r
1116                                         // Make adjacent bar borders overlap, so there's only one pixel border line between them.\r
1117                                         rcBar.right = rcBar.left + nBarWidth + 1;\r
1118                                         rcBar.bottom = m_ptOrigin.y - stackAccumulator + 1;\r
1119 \r
1120                                         if(m_bStackedGraph){\r
1121                                                 stackAccumulator = (m_ptOrigin.y - rcBar.top);\r
1122                                         }\r
1123 \r
1124                                         pSeries->SetTipRegion(nGroup, rcBar);\r
1125 \r
1126                                         COLORREF crBar(m_dwaColors.GetAt(nGroup));\r
1127                                         CBrush br(crBar);\r
1128                                         CBrush* pBrushOld = dc.SelectObject(&br);\r
1129                                         ASSERT_VALID(pBrushOld);\r
1130 \r
1131                                         VERIFY(dc.Rectangle(rcBar));\r
1132                                         dc.SelectObject(pBrushOld);\r
1133                                         br.DeleteObject();\r
1134 \r
1135                                         if(!m_bStackedGraph){\r
1136                                                 nRunningLeft += nBarWidth;\r
1137                                         }\r
1138                                 }\r
1139                         }\r
1140 \r
1141                         ++nSeries;\r
1142                 }\r
1143         }\r
1144 }\r
1145 \r
1146 //\r
1147 void MyGraph::DrawSeriesLine(CDC& dc) const\r
1148 {\r
1149         VALIDATE;\r
1150         ASSERT_VALID(&dc);\r
1151         _ASSERTE(!m_bStackedGraph);\r
1152 \r
1153         // Iterate the groups.\r
1154         CPoint ptLastLoc(0,0);\r
1155         int dataLastLoc(0);\r
1156 \r
1157         for (int nGroup = 0; nGroup < GetMaxSeriesSize(); nGroup++) {\r
1158 \r
1159                 // How much space does each series get (includes inter series space)?\r
1160                 int nSeriesSpace(0);\r
1161 \r
1162                 if (m_saLegendLabels.GetSize()) {\r
1163 \r
1164                         nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /\r
1165                                 m_olMyGraphSeries.GetCount();\r
1166                 }\r
1167                 else {\r
1168                         nSeriesSpace = m_nXAxisWidth / m_olMyGraphSeries.GetCount();\r
1169                 }\r
1170 \r
1171                 // Determine width of bars.\r
1172                 int nBarWidth(nSeriesSpace / GetMaxSeriesSize());\r
1173 \r
1174                 if (1 < m_olMyGraphSeries.GetCount()) {\r
1175                         nBarWidth = (int) ((double) nBarWidth * INTERSERIES_PERCENT_USED);\r
1176                 }\r
1177 \r
1178                 // This is the width of the largest series (no inter series space).\r
1179                 //int nMaxSeriesPlotSize(GetMaxSeriesSize() * nBarWidth);\r
1180 \r
1181                 // Iterate the series.\r
1182                 POSITION pos(m_olMyGraphSeries.GetHeadPosition());\r
1183         \r
1184                 // Build objects.\r
1185                 COLORREF crLine(m_dwaColors.GetAt(nGroup));\r
1186                 CBrush br(crLine);\r
1187                 CBrush* pBrushOld = dc.SelectObject(&br);\r
1188                 ASSERT_VALID(pBrushOld);\r
1189                 CPen penLine(PS_SOLID, 1, crLine);\r
1190                 CPen* pPenOld = dc.SelectObject(&penLine);\r
1191                 ASSERT_VALID(pPenOld);\r
1192 \r
1193                 for (int nSeries = 0; nSeries < m_olMyGraphSeries.GetCount(); ++nSeries) {\r
1194 \r
1195                         MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);\r
1196                         ASSERT_VALID(pSeries);\r
1197                         \r
1198                         // Get x and y location of center of ellipse.\r
1199                         CPoint ptLoc(0,0);\r
1200                         \r
1201                         ptLoc.x = m_ptOrigin.x + (((nSeries + 1) * nSeriesSpace) - \r
1202                                 (nSeriesSpace / 2));\r
1203                         \r
1204                         double dLineHeight(pSeries->GetData(nGroup) * m_nYAxisHeight /\r
1205                                 GetMaxDataValue());\r
1206 \r
1207                         ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);\r
1208                         \r
1209 \r
1210                         // Draw line back to last data member.\r
1211                         if (nSeries > 0 && (pSeries->GetData(nGroup)!=0 || dataLastLoc != 0)) {\r
1212 \r
1213                                 dc.MoveTo(ptLastLoc.x, ptLastLoc.y - 1);\r
1214                                 VERIFY(dc.LineTo(ptLoc.x - 1, ptLoc.y - 1));\r
1215                         }\r
1216 \r
1217                         // Now draw ellipse.\r
1218                         CRect rcEllipse(ptLoc.x - 3, ptLoc.y - 3, ptLoc.x + 3, ptLoc.y + 3);\r
1219                         if(pSeries->GetData(nGroup)!=0){\r
1220                                 VERIFY(dc.Ellipse(rcEllipse));\r
1221                         }\r
1222                         if (m_olMyGraphSeries.GetCount() < 40)\r
1223                         {\r
1224                                 pSeries->SetTipRegion(nGroup, rcEllipse);\r
1225                         }\r
1226 \r
1227                         // Save last pt and data\r
1228                         ptLastLoc = ptLoc;\r
1229                         dataLastLoc = pSeries->GetData(nGroup);\r
1230                 }\r
1231                 VERIFY(dc.SelectObject(pPenOld));\r
1232                 penLine.DeleteObject();\r
1233                 VERIFY(dc.SelectObject(pBrushOld));\r
1234                 br.DeleteObject();\r
1235         }\r
1236 }\r
1237 \r
1238 //\r
1239 void MyGraph::DrawSeriesLineStacked(CDC& dc) const\r
1240 {\r
1241         VALIDATE;\r
1242         ASSERT_VALID(&dc);\r
1243         _ASSERTE(m_bStackedGraph);\r
1244 \r
1245         CArray<int> stackAccumulator;\r
1246         stackAccumulator.SetSize(m_olMyGraphSeries.GetCount());\r
1247 \r
1248         CArray<CPoint> polygon;\r
1249         polygon.SetSize(m_olMyGraphSeries.GetCount() * 2);\r
1250 \r
1251         // How much space does each series get (includes inter series space)?\r
1252         int nSeriesSpace(0);\r
1253         if (m_saLegendLabels.GetSize()) {\r
1254                 nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /\r
1255                         m_olMyGraphSeries.GetCount();\r
1256         }\r
1257         else {\r
1258                 nSeriesSpace = m_nXAxisWidth / m_olMyGraphSeries.GetCount();\r
1259         }\r
1260 \r
1261         // Determine width of bars.\r
1262         int nBarWidth(nSeriesSpace / GetMaxSeriesSize());\r
1263         if (1 < m_olMyGraphSeries.GetCount()) {\r
1264                 nBarWidth = (int) ((double) nBarWidth * INTERSERIES_PERCENT_USED);\r
1265         }\r
1266 \r
1267         double dYScaling = double(m_nYAxisHeight) / GetMaxDataValue();\r
1268         int nSeriesCount = m_olMyGraphSeries.GetCount();\r
1269 \r
1270         // Iterate the groups.\r
1271         for (int nGroup = 0; nGroup < GetMaxSeriesSize(); nGroup++) {\r
1272         \r
1273                 // Build objects.\r
1274                 COLORREF crGroup(m_dwaColors.GetAt(nGroup));\r
1275                 CBrush br(crGroup);\r
1276                 CBrush* pBrushOld = dc.SelectObject(&br);\r
1277                 ASSERT_VALID(pBrushOld);\r
1278                 // For polygon outline, use average of this and previous color, and darken it.\r
1279                 COLORREF crPrevGroup(nGroup > 0 ? m_dwaColors.GetAt(nGroup-1) : crGroup);\r
1280                 COLORREF crOutline = RGB(\r
1281                         (GetRValue(crGroup)+GetRValue(crPrevGroup))/3,\r
1282                         (GetGValue(crGroup)+GetGValue(crPrevGroup))/3,\r
1283                         (GetBValue(crGroup)+GetBValue(crPrevGroup))/3);\r
1284                 CPen penLine(PS_SOLID, 1, crOutline);\r
1285                 CPen* pPenOld = dc.SelectObject(&penLine);\r
1286                 ASSERT_VALID(pPenOld);\r
1287 \r
1288                 // Construct bottom part of polygon from current stack accumulator\r
1289                 for (int nPolyBottom = 0; nPolyBottom < nSeriesCount; ++nPolyBottom) {\r
1290                         CPoint ptLoc;\r
1291                         ptLoc.x = m_ptOrigin.x + (((nPolyBottom + 1) * nSeriesSpace) - (nSeriesSpace / 2));\r
1292                         double dLineHeight((stackAccumulator[nPolyBottom]) * dYScaling);\r
1293                         ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);\r
1294                         polygon[nSeriesCount-nPolyBottom-1] = ptLoc;\r
1295                 }\r
1296 \r
1297                 // Iterate the series, construct upper part of polygon and upadte stack accumulator\r
1298                 POSITION pos(m_olMyGraphSeries.GetHeadPosition());\r
1299                 for (int nSeries = 0; nSeries < nSeriesCount; ++nSeries) {\r
1300 \r
1301                         MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);\r
1302                         ASSERT_VALID(pSeries);\r
1303                         \r
1304                         CPoint ptLoc;                   \r
1305                         ptLoc.x = m_ptOrigin.x + (((nSeries + 1) * nSeriesSpace) - \r
1306                                 (nSeriesSpace / 2));\r
1307                         double dLineHeight((pSeries->GetData(nGroup) + stackAccumulator[nSeries]) * dYScaling);                 \r
1308                         ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);\r
1309                         polygon[nSeriesCount+nSeries] = ptLoc;\r
1310 \r
1311                         stackAccumulator[nSeries] += pSeries->GetData(nGroup);\r
1312                 }\r
1313 \r
1314                 // Draw polygon\r
1315                 VERIFY(dc.Polygon(polygon.GetData(), polygon.GetSize()));\r
1316 \r
1317                 VERIFY(dc.SelectObject(pPenOld));\r
1318                 penLine.DeleteObject();\r
1319                 VERIFY(dc.SelectObject(pBrushOld));\r
1320                 br.DeleteObject();\r
1321         }\r
1322 }\r
1323 \r
1324 //\r
1325 void MyGraph::DrawSeriesPie(CDC& dc) const\r
1326 {\r
1327         VALIDATE;\r
1328         ASSERT_VALID(&dc);\r
1329         _ASSERTE(0 < GetNonZeroSeriesCount()  &&  "Div by zero");\r
1330 \r
1331         // Determine width of pie display area (pie and space).\r
1332         int nSeriesSpace(0);\r
1333 \r
1334         if (m_saLegendLabels.GetSize()) {\r
1335                 \r
1336                 int nPieAndSpaceWidth((m_nXAxisWidth - m_rcLegend.Width() - \r
1337                         (GAP_PIXELS * 2)) / GetNonZeroSeriesCount());\r
1338 \r
1339                 // Height is limiting factor.\r
1340                 if (nPieAndSpaceWidth > m_nYAxisHeight - (GAP_PIXELS * 2)) {\r
1341                         nSeriesSpace = (m_nYAxisHeight - (GAP_PIXELS * 2)) /\r
1342                                 GetNonZeroSeriesCount();\r
1343                 }\r
1344                 else {\r
1345                         // Width is limiting factor.\r
1346                         nSeriesSpace = nPieAndSpaceWidth;\r
1347                 }\r
1348         }\r
1349         else {\r
1350                 // No legend box.\r
1351 \r
1352                 // Height is limiting factor.\r
1353                 if (m_nXAxisWidth > m_nYAxisHeight) {\r
1354                         nSeriesSpace = m_nYAxisHeight / GetNonZeroSeriesCount();\r
1355                 }\r
1356                 else {\r
1357                         // Width is limiting factor.\r
1358                         nSeriesSpace = m_nXAxisWidth / GetNonZeroSeriesCount();\r
1359                 }\r
1360         }\r
1361 \r
1362         // Create font for labels.\r
1363         CFont fontLabels;\r
1364         int pointFontHeight = max(m_rcGraph.Height() / Y_AXIS_LABEL_DIVISOR, MIN_FONT_SIZE);\r
1365         VERIFY(fontLabels.CreatePointFont(pointFontHeight, _T("Arial"), &dc));\r
1366         CFont* pFontOld = dc.SelectObject(&fontLabels);\r
1367         ASSERT_VALID(pFontOld);\r
1368 \r
1369         // Draw each pie.\r
1370         int nPie(0);\r
1371         int nRadius((int) (nSeriesSpace * INTERSERIES_PERCENT_USED / 2.0));\r
1372         POSITION pos(m_olMyGraphSeries.GetHeadPosition());\r
1373 \r
1374         while (pos) {\r
1375 \r
1376                 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);\r
1377                 ASSERT_VALID(pSeries);\r
1378 \r
1379                 // Don't leave a space for empty pies.\r
1380                 if (0 < pSeries->GetNonZeroElementCount()) {\r
1381 \r
1382                         // Locate this pie.\r
1383                         CRect rcPie;\r
1384                         rcPie.left = m_ptOrigin.x + GAP_PIXELS + (nSeriesSpace * nPie);\r
1385                         rcPie.right = rcPie.left + (2 * nRadius);\r
1386                         rcPie.top = (m_nYAxisHeight / 2) - nRadius;\r
1387                         rcPie.bottom = (m_nYAxisHeight / 2) + nRadius;\r
1388 \r
1389                         CPoint ptCenter((rcPie.left + rcPie.right) / 2,\r
1390                                 (rcPie.top + rcPie.bottom) / 2);\r
1391 \r
1392                         // Draw series label.\r
1393                         CSize sizPieLabel(dc.GetTextExtent(pSeries->GetLabel()));\r
1394                         \r
1395                         VERIFY(dc.TextOut((rcPie.left + nRadius) - (sizPieLabel.cx / 2),\r
1396                           ptCenter.y + nRadius + GAP_PIXELS, pSeries->GetLabel()));\r
1397                         \r
1398                         // How much do the wedges total to?\r
1399                         double dPieTotal(pSeries->GetDataTotal());\r
1400 \r
1401                         // Draw each wedge in this pie.\r
1402                         CPoint ptStart(rcPie.left, ptCenter.y);\r
1403                         double dRunningWedgeTotal(0.0);\r
1404                         \r
1405                         for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {\r
1406 \r
1407                                 // Ignore empty wedges.\r
1408                                 if (0 < pSeries->GetData(nGroup)) {\r
1409 \r
1410                                         // Get the degrees of this wedge.\r
1411                                         dRunningWedgeTotal += pSeries->GetData(nGroup);\r
1412                                         double dPercent(dRunningWedgeTotal * 100.0 / dPieTotal);\r
1413                                         int nDegrees((int) (360.0 * dPercent / 100.0));\r
1414 \r
1415                                         // Find the location of the wedge's endpoint.\r
1416                                         CPoint ptEnd(WedgeEndFromDegrees(nDegrees, ptCenter, nRadius));\r
1417 \r
1418                                         // Special case: a wedge that takes up the whole pie would\r
1419                                         // otherwise be confused with an empty wedge.\r
1420                                         if (1 == pSeries->GetNonZeroElementCount()) {\r
1421                                                 _ASSERTE(360 == nDegrees  &&  ptStart == ptEnd  &&  "This is the problem we're correcting");\r
1422                                                 --ptEnd.y;\r
1423                                         }\r
1424 \r
1425                                         // If the wedge is of zero size, don't paint it!\r
1426                                         if (ptStart != ptEnd) {\r
1427 \r
1428                                                 // Draw wedge.\r
1429                                                 COLORREF crWedge(m_dwaColors.GetAt(nGroup));\r
1430                                                 CBrush br(crWedge);\r
1431                                                 CBrush* pBrushOld = dc.SelectObject(&br);\r
1432                                                 ASSERT_VALID(pBrushOld);\r
1433                                                 VERIFY(dc.Pie(rcPie, ptStart, ptEnd));\r
1434 \r
1435                                                 // Create a region from the path we create.\r
1436                                                 VERIFY(dc.BeginPath());\r
1437                                                 VERIFY(dc.Pie(rcPie, ptStart, ptEnd));\r
1438                                                 VERIFY(dc.EndPath());\r
1439                                                 CRgn * prgnWedge = new CRgn;\r
1440                                                 VERIFY(prgnWedge->CreateFromPath(&dc));\r
1441                                                 pSeries->SetTipRegion(nGroup, prgnWedge);\r
1442 \r
1443                                                 // Cleanup.\r
1444                                                 dc.SelectObject(pBrushOld);\r
1445                                                 br.DeleteObject();\r
1446                                                 ptStart = ptEnd;\r
1447                                         }\r
1448                                 }\r
1449                         }\r
1450 \r
1451                         ++nPie;\r
1452                 }\r
1453         }\r
1454 \r
1455         // Draw X axis title after we know how many pies we actually have\r
1456         CSize sizXLabel(dc.GetTextExtent(m_sXAxisLabel));\r
1457         int nTotalSpaceOfPies = nSeriesSpace * nPie - (nSeriesSpace - nRadius*2);\r
1458         VERIFY(dc.TextOut(m_ptOrigin.x + GAP_PIXELS + (nTotalSpaceOfPies - sizXLabel.cx)/2,\r
1459                 m_nYAxisHeight/2 + nRadius + GAP_PIXELS*2 + sizXLabel.cy, m_sXAxisLabel));\r
1460 \r
1461         VERIFY(dc.SelectObject(pFontOld));\r
1462         fontLabels.DeleteObject();\r
1463 }\r
1464 \r
1465 // Convert degrees to x and y coords.\r
1466 CPoint MyGraph::WedgeEndFromDegrees(int nDegrees, const CPoint& ptCenter,\r
1467                                                                                                 int nRadius) const\r
1468 {\r
1469         VALIDATE;\r
1470 \r
1471         CPoint pt;\r
1472 \r
1473         pt.x = (int) ((double) nRadius * cos((double) nDegrees / 360.0 * PI * 2.0));\r
1474         pt.x = ptCenter.x - pt.x;\r
1475 \r
1476         pt.y = (int) ((double) nRadius * sin((double) nDegrees / 360.0 * PI * 2.0));\r
1477         pt.y = ptCenter.y + pt.y;\r
1478 \r
1479         return pt;\r
1480 }\r
1481 \r
1482 // Spin The Message Loop: C++ version.  See "Advanced Windows Programming", \r
1483 // M. Heller, p. 153, and the MS TechNet CD, PSS ID Number: Q99999.\r
1484 /* static */ UINT MyGraph::SpinTheMessageLoop(bool bNoDrawing /* = false */ ,\r
1485                                                                                                                          bool bOnlyDrawing /* = false */ ,\r
1486                                                                                                                          UINT uiMsgAllowed /* = WM_NULL */ )\r
1487 {\r
1488         MSG msg;\r
1489         ::SecureZeroMemory(&msg, sizeof(msg));\r
1490 \r
1491         while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {\r
1492 \r
1493                 // Do painting only.\r
1494                 if (bOnlyDrawing  &&  WM_PAINT == msg.message)  {\r
1495                         ::TranslateMessage(&msg);\r
1496                         ::DispatchMessage(&msg);\r
1497 \r
1498                         // Update user interface.\r
1499                         AfxGetApp()->OnIdle(0);\r
1500                 }\r
1501                 // Do everything *but* painting.\r
1502                 else if (bNoDrawing  &&  WM_PAINT == msg.message)  {\r
1503                         break;\r
1504                 }\r
1505                 // Special handling for this message.\r
1506                 else if (WM_QUIT == msg.message) {\r
1507                         ::PostQuitMessage(static_cast<int>(msg.wParam));\r
1508                         break;\r
1509                 }\r
1510                 // Allow one message (like WM_LBUTTONDOWN).\r
1511                 else if (uiMsgAllowed == msg.message\r
1512                   &&  ! AfxGetApp()->PreTranslateMessage(&msg)) {\r
1513                         ::TranslateMessage(&msg);\r
1514                         ::DispatchMessage(&msg);\r
1515                         break;\r
1516                 }\r
1517                 // This is the general case.\r
1518                 else if (! bOnlyDrawing  &&  ! AfxGetApp()->PreTranslateMessage(&msg)) {\r
1519                         ::TranslateMessage(&msg);\r
1520                         ::DispatchMessage(&msg);\r
1521 \r
1522                         // Update user interface, then free temporary objects.\r
1523                         AfxGetApp()->OnIdle(0);\r
1524                         AfxGetApp()->OnIdle(1);\r
1525                 }\r
1526         }\r
1527 \r
1528         return msg.message;\r
1529 }\r
1530 \r
1531 \r
1532 /////////////////////////////////////////////////////////////////////////////\r
1533 // Conversion routines: RGB to HLS (Red-Green-Blue to Hue-Luminosity-Saturation).\r
1534 // See Microsoft KnowledgeBase article Q29240.\r
1535 \r
1536 #define  HLSMAX   240           // H,L, and S vary over 0-HLSMAX\r
1537 #define  RGBMAX   255           // R,G, and B vary over 0-RGBMAX\r
1538                                                                         // HLSMAX BEST IF DIVISIBLE BY 6\r
1539                                                                         // RGBMAX, HLSMAX must each fit in a byte (255).\r
1540 \r
1541 #define  UNDEFINED  (HLSMAX * 2 / 3)            // Hue is undefined if Saturation is 0 \r
1542                                                                                                                 // (grey-scale).  This value determines \r
1543                                                                                                                 // where the Hue scrollbar is initially \r
1544                                                                                                                 // set for achromatic colors.\r
1545 \r
1546 \r
1547 // Convert HLS to RGB.\r
1548 /* static */ COLORREF MyGraph::HLStoRGB(WORD wH, WORD wL, WORD wS)\r
1549 {\r
1550         _ASSERTE(0 <= wH  &&  240 >= wH  &&  "Illegal hue value");\r
1551         _ASSERTE(0 <= wL  &&  240 >= wL  &&  "Illegal lum value");\r
1552         _ASSERTE(0 <= wS  &&  240 >= wS  &&  "Illegal sat value");\r
1553 \r
1554         WORD wR(0);\r
1555         WORD wG(0);\r
1556         WORD wB(0);\r
1557         \r
1558         // Achromatic case.\r
1559         if (0 == wS) {\r
1560                 wR = wG = wB = (wL * RGBMAX) / HLSMAX;\r
1561 \r
1562                 if (UNDEFINED != wH) {\r
1563                         _ASSERTE(! "ERROR");\r
1564                 }\r
1565         }\r
1566         else {\r
1567                 // Chromatic case.\r
1568                 WORD Magic1(0);\r
1569                 WORD Magic2(0);\r
1570 \r
1571                 // Set up magic numbers.\r
1572                 if (wL <= HLSMAX / 2) {\r
1573                         Magic2 = (wL * (HLSMAX + wS) + (HLSMAX / 2)) / HLSMAX;\r
1574                 }\r
1575                 else {\r
1576                         Magic2 = wL + wS - ((wL * wS) + (HLSMAX / 2)) / HLSMAX;\r
1577                 }\r
1578 \r
1579                 Magic1 = 2 * wL - Magic2;\r
1580                 \r
1581                 // Get RGB, change units from HLSMAX to RGBMAX.\r
1582                 wR = (HueToRGB(Magic1, Magic2, wH + (HLSMAX / 3)) * RGBMAX + (HLSMAX / 2)) / HLSMAX;\r
1583                 wG = (HueToRGB(Magic1, Magic2, wH)                * RGBMAX + (HLSMAX / 2)) / HLSMAX;\r
1584                 wB = (HueToRGB(Magic1, Magic2, wH - (HLSMAX / 3)) * RGBMAX + (HLSMAX / 2)) / HLSMAX;\r
1585         }\r
1586         \r
1587         return RGB(wR,wG,wB);\r
1588 }\r
1589 \r
1590 // Utility routine for HLStoRGB.\r
1591 /* static */ WORD MyGraph::HueToRGB(WORD w1, WORD w2, WORD wH)\r
1592 {\r
1593         // Range check: note values passed add/subtract thirds of range.\r
1594         if (wH < 0) {\r
1595                 wH += HLSMAX;\r
1596         }\r
1597 \r
1598         if (wH > HLSMAX) {\r
1599                 wH -= HLSMAX;\r
1600         }\r
1601 \r
1602         // Return r, g, or b value from this tridrant.\r
1603         if (wH < HLSMAX / 6) {\r
1604                 return w1 + (((w2 - w1) * wH + (HLSMAX / 12)) / (HLSMAX / 6));\r
1605         }\r
1606 \r
1607         if (wH < HLSMAX / 2) {\r
1608                 return w2;\r
1609         }\r
1610 \r
1611         if (wH < (HLSMAX * 2) / 3) {\r
1612                 return w1 + (((w2 - w1) * (((HLSMAX * 2) / 3) - wH) + (HLSMAX / 12)) / (HLSMAX / 6));\r
1613         }\r
1614         else {\r
1615                 return w1;\r
1616         }\r
1617 }\r
1618 \r