5 #include "BufferDC.h"
\r
12 #define new DEBUG_NEW
\r
14 static char THIS_FILE[] = __FILE__;
\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
25 #define VALIDATE ::AfxAssertValidObject(this, __FILE__ , __LINE__ ); \
\r
26 _ASSERTE(IsKindOf(GetRuntimeClass()));
\r
33 /////////////////////////////////////////////////////////////////////////////
\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
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
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
53 const double PI = 3.1415926535897932384626433832795;
\r
55 /////////////////////////////////////////////////////////////////////////////
\r
59 MyGraphSeries::MyGraphSeries(const CString& sLabel /* = "" */ )
\r
65 /* virtual */ MyGraphSeries::~MyGraphSeries()
\r
67 for (int nGroup = 0; nGroup < m_oaRegions.GetSize(); ++nGroup) {
\r
68 delete m_oaRegions.GetAt(nGroup);
\r
73 void MyGraphSeries::SetLabel(const CString& sLabel)
\r
81 void MyGraphSeries::SetData(int nGroup, int nValue)
\r
84 _ASSERTE(0 <= nGroup);
\r
86 m_dwaValues.SetAtGrow(nGroup, nValue);
\r
90 void MyGraphSeries::SetTipRegion(int nGroup, const CRect& rc)
\r
94 CRgn* prgnNew = new CRgn;
\r
95 ASSERT_VALID(prgnNew);
\r
97 VERIFY(prgnNew->CreateRectRgnIndirect(rc));
\r
98 SetTipRegion(nGroup, prgnNew);
\r
102 void MyGraphSeries::SetTipRegion(int nGroup, CRgn* prgn)
\r
105 _ASSERTE(0 <= nGroup);
\r
106 ASSERT_VALID(prgn);
\r
108 // If there is an existing region, delete it.
\r
109 CRgn* prgnOld = NULL;
\r
111 if (nGroup < m_oaRegions.GetSize())
\r
113 prgnOld = m_oaRegions.GetAt(nGroup);
\r
114 ASSERT_NULL_OR_POINTER(prgnOld, CRgn);
\r
122 // Add the new region.
\r
123 m_oaRegions.SetAtGrow(nGroup, prgn);
\r
125 _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());
\r
129 CString MyGraphSeries::GetLabel() const
\r
137 int MyGraphSeries::GetData(int nGroup) const
\r
140 _ASSERTE(0 <= nGroup);
\r
141 _ASSERTE(m_dwaValues.GetSize() > nGroup);
\r
143 return m_dwaValues[nGroup];
\r
146 // Returns the largest data value in this series.
\r
147 int MyGraphSeries::GetMaxDataValue(bool bStackedGraph) const
\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
158 nMax += static_cast<int> (m_dwaValues.GetAt(nGroup));
\r
165 // Returns the number of data points that are not zero.
\r
166 int MyGraphSeries::GetNonZeroElementCount() const
\r
172 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
\r
174 if (m_dwaValues.GetAt(nGroup)) {
\r
182 // Returns the sum of the data points for this series.
\r
183 int MyGraphSeries::GetDataTotal() const
\r
189 for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) {
\r
190 nTotal += m_dwaValues.GetAt(nGroup);
\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
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
205 if (prgnData && prgnData->PtInRegion(pt)) {
\r
213 // Get the series portion of the tip for this group in this series.
\r
214 CString MyGraphSeries::GetTipText(int nGroup) const
\r
217 _ASSERTE(0 <= nGroup);
\r
218 _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize());
\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
230 /////////////////////////////////////////////////////////////////////////////
\r
234 MyGraph::MyGraph(GraphType eGraphType /* = MyGraph::Pie */ , bool bStackedGraph /* = false */)
\r
236 , m_nYAxisHeight(0)
\r
237 , m_eGraphType(eGraphType)
\r
238 , m_bStackedGraph(bStackedGraph)
\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
247 /* virtual */ MyGraph::~MyGraph()
\r
251 BEGIN_MESSAGE_MAP(MyGraph, CStatic)
\r
252 //{{AFX_MSG_MAP(MyGraph)
\r
256 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnNeedText)
\r
257 ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnNeedText)
\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
266 CStatic::PreSubclassWindow();
\r
268 VERIFY(EnableToolTips(true));
\r
272 /////////////////////////////////////////////////////////////////////////////
\r
273 // MyGraph message handlers
\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
278 _ASSERTE(pNMHDR && "Bad parameter passed");
\r
279 _ASSERTE(pResult && "Bad parameter passed");
\r
281 bool bReturn(false);
\r
282 UINT_PTR uiID(pNMHDR->idFrom);
\r
284 // Notification in NT from automatically created tooltip.
\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
292 TOOLTIPTEXTW* pTTTW = reinterpret_cast<TOOLTIPTEXTW*> (pNMHDR);
\r
293 ASSERT_POINTER(pTTTW, TOOLTIPTEXTW);
\r
295 CString sTipText(GetTipText());
\r
298 if (TTN_NEEDTEXTA == pNMHDR->code) {
\r
299 lstrcpyn(pTTTA->szText, sTipText, sizeof(pTTTA->szText));
\r
302 _mbstowcsz(pTTTW->szText, sTipText, sizeof(pTTTA->szText));
\r
305 if (pNMHDR->code == TTN_NEEDTEXTA) {
\r
306 _wcstombsz(pTTTA->szText, sTipText, sizeof(pTTTA->szText));
\r
309 lstrcpyn(pTTTW->szText, sTipText, sizeof(pTTTA->szText));
\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
323 _ASSERTE(pTI && "Bad parameter passed");
\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
331 if (point != ptPrev) {
\r
335 bTipPopped = false;
\r
342 pTI->hwnd = m_hWnd;
\r
343 pTI->uId = (UINT) m_hWnd;
\r
344 pTI->lpszText = LPSTR_TEXTCALLBACK;
\r
347 GetClientRect(&rcWnd);
\r
356 MyGraph::SpinTheMessageLoop();
\r
361 // Build the tip text for the part of the graph that the mouse is currently
\r
363 CString MyGraph::GetTipText() const
\r
369 // Get the position of the mouse.
\r
371 VERIFY(::GetCursorPos(&pt));
\r
372 ScreenToClient(&pt);
\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
378 else if (m_rcTitle.PtInRect(pt)) {
\r
382 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
\r
384 while (pos && sTip=="") {
\r
385 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
\r
386 ASSERT_VALID(pSeries);
\r
390 nGroup = pSeries->HitTest(pt,nGroup);
\r
392 if (-1 != nGroup) {
\r
396 sTip += m_saLegendLabels.GetAt(nGroup) + _T(": ");
\r
397 sTip += pSeries->GetTipText(nGroup);
\r
400 }while(-1 != nGroup);
\r
407 // Handle WM_PAINT.
\r
408 void MyGraph::OnPaint()
\r
412 CBufferDC dc(this);
\r
417 void MyGraph::OnSize(UINT nType, int cx, int cy)
\r
421 CStatic::OnSize(nType, cx, cy);
\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
433 m_bStackedGraph = bStackedGraph;
\r
436 // Calculate the current max legend label length in pixels.
\r
437 int MyGraph::GetMaxLegendLabelLength(CDC& dc) const
\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
450 if (nMaxChars < nLabelLength) {
\r
451 nMaxChars = nLabelLength;
\r
452 sMax = m_saLegendLabels.GetAt(nGroup);
\r
456 // Now calculate the pixels.
\r
457 siz = dc.GetTextExtent(sMax);
\r
459 _ASSERTE(-1 < siz.cx);
\r
464 // Returns the largest number of data points in any series.
\r
465 int MyGraph::GetMaxSeriesSize() const
\r
470 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
\r
473 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
\r
474 ASSERT_VALID(pSeries);
\r
476 nMax = max(nMax, pSeries->m_dwaValues.GetSize());
\r
482 // Returns the largest number of non-zero data points in any series.
\r
483 int MyGraph::GetMaxNonZeroSeriesSize() const
\r
488 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
\r
491 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
\r
492 ASSERT_VALID(pSeries);
\r
494 nMax = max(nMax, pSeries->GetNonZeroElementCount());
\r
500 // Get the largest data value in all series.
\r
501 int MyGraph::GetMaxDataValue() const
\r
506 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
\r
509 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
\r
510 ASSERT_VALID(pSeries);
\r
512 nMax = max(nMax, pSeries->GetMaxDataValue(m_bStackedGraph));
\r
518 // How many series are populated?
\r
519 int MyGraph::GetNonZeroSeriesCount() const
\r
524 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
\r
527 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
\r
528 ASSERT_VALID(pSeries);
\r
530 if (0 < pSeries->GetNonZeroElementCount()) {
\r
538 // Returns the group number for the sent label; -1 if not found.
\r
539 int MyGraph::LookupLabel(const CString& sLabel) const
\r
542 _ASSERTE(! sLabel.IsEmpty());
\r
544 for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {
\r
546 if (0 == sLabel.CompareNoCase(m_saLegendLabels.GetAt(nGroup))) {
\r
554 void MyGraph::Clear()
\r
556 m_dwaColors.RemoveAll();
\r
557 m_saLegendLabels.RemoveAll();
\r
558 m_olMyGraphSeries.RemoveAll();
\r
562 void MyGraph::AddSeries(MyGraphSeries& rMyGraphSeries)
\r
565 ASSERT_VALID(&rMyGraphSeries);
\r
566 _ASSERTE(m_saLegendLabels.GetSize() == rMyGraphSeries.m_dwaValues.GetSize());
\r
568 m_olMyGraphSeries.AddTail(&rMyGraphSeries);
\r
572 void MyGraph::SetXAxisLabel(const CString& sLabel)
\r
575 _ASSERTE(! sLabel.IsEmpty());
\r
577 m_sXAxisLabel = sLabel;
\r
581 void MyGraph::SetYAxisLabel(const CString& sLabel)
\r
584 _ASSERTE(! sLabel.IsEmpty());
\r
586 m_sYAxisLabel = sLabel;
\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
596 int nGroup(m_saLegendLabels.GetSize());
\r
597 SetLegend(nGroup, sLabel);
\r
599 // Make sure that all series have this element.
\r
600 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
\r
604 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
\r
605 ASSERT_VALID(pSeries);
\r
607 if (nGroup >= pSeries->m_dwaValues.GetSize()) {
\r
608 pSeries->m_dwaValues.SetAtGrow(nGroup, 0);
\r
615 // Set this value to the legend.
\r
616 void MyGraph::SetLegend(int nGroup, const CString& sLabel)
\r
619 _ASSERTE(0 <= nGroup);
\r
621 m_saLegendLabels.SetAtGrow(nGroup, sLabel);
\r
625 void MyGraph::SetGraphTitle(const CString& sTitle)
\r
628 _ASSERTE(! sTitle.IsEmpty());
\r
634 void MyGraph::DrawGraph(CDC& dc)
\r
639 if (GetMaxSeriesSize()) {
\r
640 dc.SetBkMode(TRANSPARENT);
\r
642 // Populate the colors as a group of evenly spaced colors of maximum
\r
644 int nColorsDelta(240 / GetMaxSeriesSize());
\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
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
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
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
679 VERIFY(br.CreateSolidBrush(backgroundColor));
\r
680 dc.FillRect(rcWnd, &br);
\r
683 // Draw graph title.
\r
686 // Set the axes and origin values.
\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
694 m_rcLegend.SetRectEmpty();
\r
697 // Draw axes unless it's a pie.
\r
698 if (m_eGraphType != MyGraph::PieChart) {
\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
712 // Draw graph title; size is proportionate to width.
\r
713 void MyGraph::DrawTitle(CDC& dc)
\r
718 // Create the title font.
\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
726 m_rcTitle.SetRect(GAP_PIXELS, GAP_PIXELS, m_rcGraph.Width() + GAP_PIXELS,
\r
727 m_rcGraph.Height() + GAP_PIXELS);
\r
729 dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE |
\r
730 DT_TOP | DT_CALCRECT);
\r
732 m_rcTitle.right = m_rcGraph.Width() + GAP_PIXELS;
\r
734 dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE |
\r
737 VERIFY(dc.SelectObject(pFontOld));
\r
738 fontTitle.DeleteObject();
\r
741 // Set the axes and origin values.
\r
742 void MyGraph::SetupAxes(CDC& dc)
\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
756 // Bar and Line graphs.
\r
758 // Need to find out how wide the biggest Y-axis tick label is
\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
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
771 // Obtain tick label dimensions.
\r
772 CString sTickLabel;
\r
773 sTickLabel.Format(_T("%d"), GetMaxDataValue());
\r
774 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));
\r
776 // Set old font object again and delete temporary font object.
\r
777 VERIFY(dc.SelectObject(pFontOld));
\r
778 fontTickLabels.DeleteObject();
\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
791 void MyGraph::DrawLegend(CDC& dc)
\r
796 // Create the legend font.
\r
798 int pointFontHeight = max(m_rcGraph.Height() / LEGEND_DIVISOR, MIN_FONT_SIZE);
\r
799 VERIFY(fontLegend.CreatePointFont(pointFontHeight, _T("Arial"), &dc));
\r
801 // Get the height of each label.
\r
803 ::SecureZeroMemory(&lf, sizeof(lf));
\r
804 VERIFY(fontLegend.GetLogFont(&lf));
\r
805 int nLabelHeight(abs(lf.lfHeight));
\r
807 // Get number of legend entries
\r
808 int nLegendEntries = max(1, GetMaxSeriesSize());
\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
815 // Now relate the LabelHeight to the PointFontHeight
\r
816 int optimalPointFontHeight = int(pointFontHeight*optimalLabelHeight/nLabelHeight);
\r
818 // Limit the optimal PointFontHeight to the available range
\r
819 optimalPointFontHeight = min( max(optimalPointFontHeight, MIN_FONT_SIZE), pointFontHeight);
\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
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
835 // Get number of authors to be shown.
\r
836 nShownAuthors = min(nShownAuthors, GetMaxSeriesSize());
\r
837 // nShownAuthors contains now the number of authors
\r
839 CFont* pFontOld = dc.SelectObject(&fontLegend);
\r
840 ASSERT_VALID(pFontOld);
\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
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
860 int nLabelTop(m_rcLegend.top + (nGroup * nLabelHeight) +
\r
863 int nShownGroup = nGroup; // introduce helper variable to avoid code duplication
\r
865 // Do we have a skipped row?
\r
866 if (skipped_row != -1)
\r
868 if (nGroup == skipped_row) {
\r
870 VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop, _T("...") ));
\r
873 if (nGroup == nShownAuthors-1) {
\r
874 // we show the last group instead of the scheduled group
\r
875 nShownGroup = GetMaxSeriesSize()-1;
\r
879 VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop,
\r
880 m_saLegendLabels.GetAt(nShownGroup)));
\r
882 // Determine the bar.
\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
890 // Draw bar for group.
\r
891 COLORREF crBar(m_dwaColors.GetAt(nShownGroup));
\r
894 CBrush* pBrushOld = dc.SelectObject(&br);
\r
895 ASSERT_VALID(pBrushOld);
\r
897 rcBar.DeflateRect(LEGEND_COLOR_BAR_GAP_PIXELS, LEGEND_COLOR_BAR_GAP_PIXELS);
\r
898 dc.FillRect(rcBar, &br);
\r
900 dc.SelectObject(pBrushOld);
\r
904 VERIFY(dc.SelectObject(pFontOld));
\r
905 fontLegend.DeleteObject();
\r
909 void MyGraph::DrawAxes(CDC& dc) const
\r
913 _ASSERTE(MyGraph::PieChart != m_eGraphType);
\r
915 dc.SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));
\r
918 dc.MoveTo(m_ptOrigin);
\r
919 VERIFY(dc.LineTo(m_ptOrigin.x, m_ptOrigin.y - m_nYAxisHeight));
\r
922 dc.MoveTo(m_ptOrigin);
\r
924 if (m_saLegendLabels.GetSize()) {
\r
926 VERIFY(dc.LineTo(m_ptOrigin.x +
\r
927 (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)),
\r
931 VERIFY(dc.LineTo(m_ptOrigin.x + m_nXAxisWidth, m_ptOrigin.y));
\r
934 // Note: m_nAxisLabelHeight and m_nAxisTickLabelHeight have been calculated in SetupAxis()
\r
936 // Create the x-axis label font.
\r
938 VERIFY(fontXAxis.CreatePointFont(m_nAxisLabelHeight, _T("Arial"), &dc));
\r
940 // Obtain the height of the font in device coordinates.
\r
942 VERIFY(fontXAxis.GetLogFont(&pLF));
\r
943 int fontHeightDC = pLF.lfHeight;
\r
945 // Create the y-axis label font.
\r
947 VERIFY(fontYAxis.CreateFont(
\r
948 /* nHeight */ fontHeightDC,
\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
957 OUT_DEFAULT_PRECIS,
\r
958 CLIP_DEFAULT_PRECIS,
\r
960 VARIABLE_PITCH | FF_DONTCARE,
\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
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
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
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
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
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
996 VERIFY(dc.TextOut(m_ptOrigin.x - GAP_PIXELS - sizTickLabel.cx - TICK_PIXELS,
\r
997 nTickYLocation - sizTickLabel.cy/2, sTickLabel));
\r
1000 // Draw X axis tick marks.
\r
1001 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
\r
1006 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
\r
1007 ASSERT_VALID(pSeries);
\r
1009 // Ignore unpopulated series if bar chart.
\r
1010 if (m_eGraphType != MyGraph::Bar ||
\r
1011 0 < pSeries->GetNonZeroElementCount()) {
\r
1013 // Get the spacing of the series.
\r
1014 _ASSERTE(GetNonZeroSeriesCount() && "Div by zero coming");
\r
1015 int nSeriesSpace(0);
\r
1017 if (m_saLegendLabels.GetSize()) {
\r
1020 (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
\r
1021 (m_eGraphType == MyGraph::Bar ?
\r
1022 GetNonZeroSeriesCount() : m_olMyGraphSeries.GetCount());
\r
1025 nSeriesSpace = m_nXAxisWidth / (m_eGraphType == MyGraph::Bar ?
\r
1026 GetNonZeroSeriesCount() : m_olMyGraphSeries.GetCount());
\r
1029 int nTickXLocation(m_ptOrigin.x + ((nSeries + 1) * nSeriesSpace) -
\r
1030 (nSeriesSpace / 2));
\r
1032 dc.MoveTo(nTickXLocation, m_ptOrigin.y - TICK_PIXELS);
\r
1033 VERIFY(dc.LineTo(nTickXLocation, m_ptOrigin.y + TICK_PIXELS));
\r
1035 // Draw x-axis tick label.
\r
1036 CString sTickLabel(pSeries->GetLabel());
\r
1037 CSize sizTickLabel(dc.GetTextExtent(sTickLabel));
\r
1039 VERIFY(dc.TextOut(nTickXLocation - (sizTickLabel.cx / 2),
\r
1040 m_ptOrigin.y + TICK_PIXELS + GAP_PIXELS, sTickLabel));
\r
1046 VERIFY(dc.SelectObject(pFontOld));
\r
1047 fontXAxis.DeleteObject();
\r
1048 fontYAxis.DeleteObject();
\r
1049 fontTickLabels.DeleteObject();
\r
1053 void MyGraph::DrawSeriesBar(CDC& dc) const
\r
1056 ASSERT_VALID(&dc);
\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
1062 if (m_saLegendLabels.GetSize()) {
\r
1064 nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
\r
1065 GetNonZeroSeriesCount();
\r
1068 nSeriesSpace = m_nXAxisWidth / GetNonZeroSeriesCount();
\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
1075 // This is the width of the largest series (no inter series space).
\r
1076 int nMaxSeriesPlotSize(0);
\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
1083 nMaxSeriesPlotSize = GetMaxNonZeroSeriesSize() * nBarWidth;
\r
1086 nBarWidth = (int) ((double) nSeriesSpace * INTERSERIES_PERCENT_USED);
\r
1087 nMaxSeriesPlotSize = nBarWidth;
\r
1090 // Iterate the series.
\r
1091 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
\r
1096 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
\r
1097 ASSERT_VALID(pSeries);
\r
1099 // Ignore unpopulated series.
\r
1100 if (0 < pSeries->GetNonZeroElementCount()) {
\r
1102 // Draw each bar; empty bars are not drawn.
\r
1103 int nRunningLeft(m_ptOrigin.x + ((nSeries + 1) * nSeriesSpace) -
\r
1104 nMaxSeriesPlotSize);
\r
1106 int stackAccumulator(0);
\r
1108 for (int nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup) {
\r
1110 if (pSeries->GetData(nGroup)) {
\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
1120 if(m_bStackedGraph){
\r
1121 stackAccumulator = (m_ptOrigin.y - rcBar.top);
\r
1124 pSeries->SetTipRegion(nGroup, rcBar);
\r
1126 COLORREF crBar(m_dwaColors.GetAt(nGroup));
\r
1128 CBrush* pBrushOld = dc.SelectObject(&br);
\r
1129 ASSERT_VALID(pBrushOld);
\r
1131 VERIFY(dc.Rectangle(rcBar));
\r
1132 dc.SelectObject(pBrushOld);
\r
1133 br.DeleteObject();
\r
1135 if(!m_bStackedGraph){
\r
1136 nRunningLeft += nBarWidth;
\r
1147 void MyGraph::DrawSeriesLine(CDC& dc) const
\r
1150 ASSERT_VALID(&dc);
\r
1151 _ASSERTE(!m_bStackedGraph);
\r
1153 // Iterate the groups.
\r
1154 CPoint ptLastLoc(0,0);
\r
1155 int dataLastLoc(0);
\r
1157 for (int nGroup = 0; nGroup < GetMaxSeriesSize(); nGroup++) {
\r
1159 // How much space does each series get (includes inter series space)?
\r
1160 int nSeriesSpace(0);
\r
1162 if (m_saLegendLabels.GetSize()) {
\r
1164 nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) /
\r
1165 m_olMyGraphSeries.GetCount();
\r
1168 nSeriesSpace = m_nXAxisWidth / m_olMyGraphSeries.GetCount();
\r
1171 // Determine width of bars.
\r
1172 int nBarWidth(nSeriesSpace / GetMaxSeriesSize());
\r
1174 if (1 < m_olMyGraphSeries.GetCount()) {
\r
1175 nBarWidth = (int) ((double) nBarWidth * INTERSERIES_PERCENT_USED);
\r
1178 // This is the width of the largest series (no inter series space).
\r
1179 //int nMaxSeriesPlotSize(GetMaxSeriesSize() * nBarWidth);
\r
1181 // Iterate the series.
\r
1182 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
\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
1193 for (int nSeries = 0; nSeries < m_olMyGraphSeries.GetCount(); ++nSeries) {
\r
1195 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
\r
1196 ASSERT_VALID(pSeries);
\r
1198 // Get x and y location of center of ellipse.
\r
1199 CPoint ptLoc(0,0);
\r
1201 ptLoc.x = m_ptOrigin.x + (((nSeries + 1) * nSeriesSpace) -
\r
1202 (nSeriesSpace / 2));
\r
1204 double dLineHeight(pSeries->GetData(nGroup) * m_nYAxisHeight /
\r
1205 GetMaxDataValue());
\r
1207 ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight);
\r
1210 // Draw line back to last data member.
\r
1211 if (nSeries > 0 && (pSeries->GetData(nGroup)!=0 || dataLastLoc != 0)) {
\r
1213 dc.MoveTo(ptLastLoc.x, ptLastLoc.y - 1);
\r
1214 VERIFY(dc.LineTo(ptLoc.x - 1, ptLoc.y - 1));
\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
1222 if (m_olMyGraphSeries.GetCount() < 40)
\r
1224 pSeries->SetTipRegion(nGroup, rcEllipse);
\r
1227 // Save last pt and data
\r
1228 ptLastLoc = ptLoc;
\r
1229 dataLastLoc = pSeries->GetData(nGroup);
\r
1231 VERIFY(dc.SelectObject(pPenOld));
\r
1232 penLine.DeleteObject();
\r
1233 VERIFY(dc.SelectObject(pBrushOld));
\r
1234 br.DeleteObject();
\r
1239 void MyGraph::DrawSeriesLineStacked(CDC& dc) const
\r
1242 ASSERT_VALID(&dc);
\r
1243 _ASSERTE(m_bStackedGraph);
\r
1245 CArray<int> stackAccumulator;
\r
1246 stackAccumulator.SetSize(m_olMyGraphSeries.GetCount());
\r
1248 CArray<CPoint> polygon;
\r
1249 polygon.SetSize(m_olMyGraphSeries.GetCount() * 2);
\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
1258 nSeriesSpace = m_nXAxisWidth / m_olMyGraphSeries.GetCount();
\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
1267 double dYScaling = double(m_nYAxisHeight) / GetMaxDataValue();
\r
1268 int nSeriesCount = m_olMyGraphSeries.GetCount();
\r
1270 // Iterate the groups.
\r
1271 for (int nGroup = 0; nGroup < GetMaxSeriesSize(); nGroup++) {
\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
1288 // Construct bottom part of polygon from current stack accumulator
\r
1289 for (int nPolyBottom = 0; nPolyBottom < nSeriesCount; ++nPolyBottom) {
\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
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
1301 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
\r
1302 ASSERT_VALID(pSeries);
\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
1311 stackAccumulator[nSeries] += pSeries->GetData(nGroup);
\r
1315 VERIFY(dc.Polygon(polygon.GetData(), polygon.GetSize()));
\r
1317 VERIFY(dc.SelectObject(pPenOld));
\r
1318 penLine.DeleteObject();
\r
1319 VERIFY(dc.SelectObject(pBrushOld));
\r
1320 br.DeleteObject();
\r
1325 void MyGraph::DrawSeriesPie(CDC& dc) const
\r
1328 ASSERT_VALID(&dc);
\r
1329 _ASSERTE(0 < GetNonZeroSeriesCount() && "Div by zero");
\r
1331 // Determine width of pie display area (pie and space).
\r
1332 int nSeriesSpace(0);
\r
1334 if (m_saLegendLabels.GetSize()) {
\r
1336 int nPieAndSpaceWidth((m_nXAxisWidth - m_rcLegend.Width() -
\r
1337 (GAP_PIXELS * 2)) / GetNonZeroSeriesCount());
\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
1345 // Width is limiting factor.
\r
1346 nSeriesSpace = nPieAndSpaceWidth;
\r
1352 // Height is limiting factor.
\r
1353 if (m_nXAxisWidth > m_nYAxisHeight) {
\r
1354 nSeriesSpace = m_nYAxisHeight / GetNonZeroSeriesCount();
\r
1357 // Width is limiting factor.
\r
1358 nSeriesSpace = m_nXAxisWidth / GetNonZeroSeriesCount();
\r
1362 // Create font for labels.
\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
1371 int nRadius((int) (nSeriesSpace * INTERSERIES_PERCENT_USED / 2.0));
\r
1372 POSITION pos(m_olMyGraphSeries.GetHeadPosition());
\r
1376 MyGraphSeries* pSeries = m_olMyGraphSeries.GetNext(pos);
\r
1377 ASSERT_VALID(pSeries);
\r
1379 // Don't leave a space for empty pies.
\r
1380 if (0 < pSeries->GetNonZeroElementCount()) {
\r
1382 // Locate this pie.
\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
1389 CPoint ptCenter((rcPie.left + rcPie.right) / 2,
\r
1390 (rcPie.top + rcPie.bottom) / 2);
\r
1392 // Draw series label.
\r
1393 CSize sizPieLabel(dc.GetTextExtent(pSeries->GetLabel()));
\r
1395 VERIFY(dc.TextOut((rcPie.left + nRadius) - (sizPieLabel.cx / 2),
\r
1396 ptCenter.y + nRadius + GAP_PIXELS, pSeries->GetLabel()));
\r
1398 // How much do the wedges total to?
\r
1399 double dPieTotal(pSeries->GetDataTotal());
\r
1401 // Draw each wedge in this pie.
\r
1402 CPoint ptStart(rcPie.left, ptCenter.y);
\r
1403 double dRunningWedgeTotal(0.0);
\r
1405 for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) {
\r
1407 // Ignore empty wedges.
\r
1408 if (0 < pSeries->GetData(nGroup)) {
\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
1415 // Find the location of the wedge's endpoint.
\r
1416 CPoint ptEnd(WedgeEndFromDegrees(nDegrees, ptCenter, nRadius));
\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
1425 // If the wedge is of zero size, don't paint it!
\r
1426 if (ptStart != ptEnd) {
\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
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
1444 dc.SelectObject(pBrushOld);
\r
1445 br.DeleteObject();
\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
1461 VERIFY(dc.SelectObject(pFontOld));
\r
1462 fontLabels.DeleteObject();
\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
1473 pt.x = (int) ((double) nRadius * cos((double) nDegrees / 360.0 * PI * 2.0));
\r
1474 pt.x = ptCenter.x - pt.x;
\r
1476 pt.y = (int) ((double) nRadius * sin((double) nDegrees / 360.0 * PI * 2.0));
\r
1477 pt.y = ptCenter.y + pt.y;
\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
1489 ::SecureZeroMemory(&msg, sizeof(msg));
\r
1491 while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
\r
1493 // Do painting only.
\r
1494 if (bOnlyDrawing && WM_PAINT == msg.message) {
\r
1495 ::TranslateMessage(&msg);
\r
1496 ::DispatchMessage(&msg);
\r
1498 // Update user interface.
\r
1499 AfxGetApp()->OnIdle(0);
\r
1501 // Do everything *but* painting.
\r
1502 else if (bNoDrawing && WM_PAINT == msg.message) {
\r
1505 // Special handling for this message.
\r
1506 else if (WM_QUIT == msg.message) {
\r
1507 ::PostQuitMessage(static_cast<int>(msg.wParam));
\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
1517 // This is the general case.
\r
1518 else if (! bOnlyDrawing && ! AfxGetApp()->PreTranslateMessage(&msg)) {
\r
1519 ::TranslateMessage(&msg);
\r
1520 ::DispatchMessage(&msg);
\r
1522 // Update user interface, then free temporary objects.
\r
1523 AfxGetApp()->OnIdle(0);
\r
1524 AfxGetApp()->OnIdle(1);
\r
1528 return msg.message;
\r
1532 /////////////////////////////////////////////////////////////////////////////
\r
1533 // Conversion routines: RGB to HLS (Red-Green-Blue to Hue-Luminosity-Saturation).
\r
1534 // See Microsoft KnowledgeBase article Q29240.
\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
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
1547 // Convert HLS to RGB.
\r
1548 /* static */ COLORREF MyGraph::HLStoRGB(WORD wH, WORD wL, WORD wS)
\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
1558 // Achromatic case.
\r
1560 wR = wG = wB = (wL * RGBMAX) / HLSMAX;
\r
1562 if (UNDEFINED != wH) {
\r
1563 _ASSERTE(! "ERROR");
\r
1567 // Chromatic case.
\r
1571 // Set up magic numbers.
\r
1572 if (wL <= HLSMAX / 2) {
\r
1573 Magic2 = (wL * (HLSMAX + wS) + (HLSMAX / 2)) / HLSMAX;
\r
1576 Magic2 = wL + wS - ((wL * wS) + (HLSMAX / 2)) / HLSMAX;
\r
1579 Magic1 = 2 * wL - Magic2;
\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
1587 return RGB(wR,wG,wB);
\r
1590 // Utility routine for HLStoRGB.
\r
1591 /* static */ WORD MyGraph::HueToRGB(WORD w1, WORD w2, WORD wH)
\r
1593 // Range check: note values passed add/subtract thirds of range.
\r
1598 if (wH > HLSMAX) {
\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
1607 if (wH < HLSMAX / 2) {
\r
1611 if (wH < (HLSMAX * 2) / 3) {
\r
1612 return w1 + (((w2 - w1) * (((HLSMAX * 2) / 3) - wH) + (HLSMAX / 12)) / (HLSMAX / 6));
\r