// Created by Larry Leonard, Definitive Solutions, Inc. #include "stdafx.h" #include "MyGraph.h" #include "Generic.h" #include "math.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif ///////////////////////////////////////////////////////////////////////////// // Constants. #define TICK_PIXELS 4 // Size of tick marks. #define GAP_PIXELS 6 // Better if an even value. #define LEGEND_COLOR_BAR_WIDTH_PIXELS 50 // Width of color bar. #define LEGEND_COLOR_BAR_GAP_PIXELS 1 // Space between color bars. #define Y_AXIS_MAX_TICK_COUNT 5 // How many ticks on y axis. #define INTERSERIES_PERCENT_USED 0.85 // How much of the graph is // used for bars/pies (the // rest is for inter-series // spacing). #define TITLE_DIVISOR 5 // Scale font to graph width. #define LEGEND_DIVISOR 8 // Scale font to graph width. #define X_AXIS_LABEL_DIVISOR 10 // Scale font to graph width. #define Y_AXIS_LABEL_DIVISOR 6 // Scale font to graph width. #define PI 3.1415926535897932384626433832795 ///////////////////////////////////////////////////////////////////////////// // MyGraphSeries // Constructor. MyGraphSeries::MyGraphSeries(const CString& sLabel /* = "" */ ) : m_sLabel(sLabel) { } // Destructor. /* virtual */ MyGraphSeries::~MyGraphSeries() { for (int nGroup = 0; nGroup < m_oaRegions.GetSize(); ++nGroup) { delete (CRgn*) m_oaRegions.GetAt(nGroup); } } // void MyGraphSeries::SetLabel(const CString& sLabel) { VALIDATE; _ASSERTE(! sLabel.IsEmpty()); _ASSERTE(m_dwaValues.GetSize() == m_oaRegions.GetSize()); m_sLabel = sLabel; } // void MyGraphSeries::SetData(int nGroup, int nValue) { VALIDATE; _ASSERTE(0 <= nGroup); m_dwaValues.SetAtGrow(nGroup, nValue); } // void MyGraphSeries::SetTipRegion(int nGroup, const CRect& rc) { VALIDATE; CRgn* prgnNew = new CRgn; ASSERT_VALID(prgnNew); VERIFY(prgnNew->CreateRectRgnIndirect(rc)); SetTipRegion(nGroup, prgnNew); } // void MyGraphSeries::SetTipRegion(int nGroup, const CRgn* prgn) { VALIDATE; _ASSERTE(0 <= nGroup); ASSERT_VALID(prgn); // If there is an existing resgion, delete it. CRgn* prgnOld = NULL; if (nGroup < m_oaRegions.GetSize()) { prgnOld = static_cast (m_oaRegions.GetAt(nGroup)); ASSERT_NULL_OR_POINTER(prgnOld, CRgn); } if (prgnOld) { delete prgnOld; prgnOld = NULL; } // Add the new region. m_oaRegions.SetAtGrow(nGroup, (CObject*) prgn); _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize()); } // CString MyGraphSeries::GetLabel() const { VALIDATE; return m_sLabel; } // int MyGraphSeries::GetData(int nGroup) const { VALIDATE; _ASSERTE(0 <= nGroup); _ASSERTE(m_dwaValues.GetSize() > nGroup); return m_dwaValues[nGroup]; } // Returns the largest data value in this series. int MyGraphSeries::GetMaxDataValue() const { VALIDATE; int nMax(0); for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) { nMax = max(nMax, static_cast (m_dwaValues.GetAt(nGroup))); } return nMax; } // Returns the number of data points that are not zero. int MyGraphSeries::GetNonZeroElementCount() const { VALIDATE; int nCount(0); for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) { if (m_dwaValues.GetAt(nGroup)) { ++nCount; } } return nCount; } // Returns the sum of the data points for this series. int MyGraphSeries::GetDataTotal() const { VALIDATE; int nTotal(0); for (int nGroup = 0; nGroup < m_dwaValues.GetSize(); ++nGroup) { nTotal += m_dwaValues.GetAt(nGroup); } return nTotal; } // Returns which group (if any) the sent point lies within in this series. int MyGraphSeries::HitTest(const CPoint& pt) const { VALIDATE; for (int nGroup = 0; nGroup < m_oaRegions.GetSize(); ++nGroup) { CRgn* prgnData = static_cast (m_oaRegions.GetAt(nGroup)); ASSERT_NULL_OR_POINTER(prgnData, CRgn); if (prgnData && prgnData->PtInRegion(pt)) { return nGroup; } } return -1; } // Get the series portion of the tip for this group in this series. CString MyGraphSeries::GetTipText(int nGroup) const { VALIDATE; _ASSERTE(0 <= nGroup); _ASSERTE(m_oaRegions.GetSize() <= m_dwaValues.GetSize()); CString sTip; sTip.Format("%d (%d%%)", m_dwaValues.GetAt(nGroup), (int) (100.0 * (double) m_dwaValues.GetAt(nGroup) / (double) GetDataTotal())); return sTip; } ///////////////////////////////////////////////////////////////////////////// // MyGraph // Constructor. MyGraph::MyGraph(GraphType eGraphType /* = MyGraph::Pie */ ) : m_nXAxisWidth(0) , m_nYAxisHeight(0) , m_eGraphType(eGraphType) { m_ptOrigin.x = m_ptOrigin.y = 0; m_rcGraph.SetRectEmpty(); m_rcLegend.SetRectEmpty(); m_rcTitle.SetRectEmpty(); } // Destructor. /* virtual */ MyGraph::~MyGraph() { } BEGIN_MESSAGE_MAP(MyGraph, CStatic) //{{AFX_MSG_MAP(MyGraph) ON_WM_PAINT() ON_WM_SIZE() //}}AFX_MSG_MAP ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, OnNeedText) ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, OnNeedText) END_MESSAGE_MAP() // Called by the framework to allow other necessary subclassing to occur // before the window is subclassed. void MyGraph::PreSubclassWindow() { VALIDATE; CStatic::PreSubclassWindow(); VERIFY(EnableToolTips(true)); } ///////////////////////////////////////////////////////////////////////////// // MyGraph message handlers // Handle the tooltip messages. Returns true to mean message was handled. bool MyGraph::OnNeedText(UINT uiId, NMHDR* pNMHDR, LRESULT* pResult) { _ASSERTE(pNMHDR && "Bad parameter passed"); _ASSERTE(pResult && "Bad parameter passed"); bool bReturn(false); UINT uiID(pNMHDR->idFrom); // Notification in NT from automatically created tooltip. if (0U != uiID) { bReturn = true; // Need to handle both ANSI and UNICODE versions of the message. TOOLTIPTEXTA* pTTTA = reinterpret_cast (pNMHDR); ASSERT_POINTER(pTTTA, TOOLTIPTEXTA); TOOLTIPTEXTW* pTTTW = reinterpret_cast (pNMHDR); ASSERT_POINTER(pTTTW, TOOLTIPTEXTW); #ifndef _UNICODE CString sTipText(GetTipText()); if (TTN_NEEDTEXTA == pNMHDR->code) { lstrcpyn(pTTTA->szText, sTipText, sizeof(pTTTA->szText)); } else { _mbstowcsz(pTTTW->szText, sTipText, sizeof(pTTTA->szText)); } #else if (pNMHDR->code == TTN_NEEDTEXTA) { _wcstombsz(pTTTA->szText, sTipText, sizeof(pTTTA->szText)); } else { lstrcpyn(pTTTW->szText, sTipText, sizeof(pTTTA->szText)); } #endif *pResult = 0; } return bReturn; } // The framework calls this member function to detemine whether a point is in // the bounding rectangle of the specified tool. int MyGraph::OnToolHitTest(CPoint point, TOOLINFO* pTI) const { _ASSERTE(pTI && "Bad parameter passed"); // This works around the problem of the tip remaining visible when you move // the mouse to various positions over this control. int nReturn(0); static bTipPopped(false); static CPoint ptPrev(-1,-1); if (point != ptPrev) { ptPrev = point; if (bTipPopped) { bTipPopped = false; nReturn = -1; } else { ::Sleep(50); bTipPopped = true; pTI->hwnd = m_hWnd; pTI->uId = (UINT) m_hWnd; pTI->lpszText = LPSTR_TEXTCALLBACK; CRect rcWnd; GetClientRect(&rcWnd); pTI->rect = rcWnd; nReturn = 1; } } else { nReturn = 1; } Generic::SpinTheMessageLoop(); return nReturn; } // Build the tip text for the part of the graph that the mouse is currently // over. CString MyGraph::GetTipText() const { VALIDATE; CString sTip; // Get the position of the mouse. CPoint pt; VERIFY(::GetCursorPos(&pt)); ScreenToClient(&pt); // Ask each part of the graph to check and see if the mouse is over it. if (m_rcLegend.PtInRect(pt)) { sTip = "Legend"; } else if (m_rcTitle.PtInRect(pt)) { sTip = "Title"; } else { POSITION pos(m_olMyGraphSeries.GetHeadPosition()); while (pos) { MyGraphSeries* pSeries = static_cast (m_olMyGraphSeries.GetNext(pos)); ASSERT_VALID(pSeries); int nGroup(pSeries->HitTest(pt)); if (-1 != nGroup) { sTip = m_saLegendLabels.GetAt(nGroup) + ": "; sTip += pSeries->GetTipText(nGroup); break; } } } return sTip; } // Handle WM_PAINT. void MyGraph::OnPaint() { VALIDATE; CPaintDC dc(this); DrawGraph(dc); } // Handle WM_SIZE. void MyGraph::OnSize(UINT nType, int cx, int cy) { VALIDATE; CStatic::OnSize(nType, cx, cy); Invalidate(); } // Change the type of the graph; the caller should call Invalidate() on this // window to make the effect of this change visible. void MyGraph::SetGraphType(GraphType e) { VALIDATE; m_eGraphType = e; } // Calculate the current max legend label length in pixels. int MyGraph::GetMaxLegendLabelLength(CDC& dc) const { VALIDATE; ASSERT_VALID(&dc); CString sMax; int nMaxChars(-1); CSize siz(-1,-1); // First get max number of characters. for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) { int nLabelLength(m_saLegendLabels.GetAt(nGroup).GetLength()); if (nMaxChars < nLabelLength) { nMaxChars = nLabelLength; sMax = m_saLegendLabels.GetAt(nGroup); } } // Now calculate the pixels. siz = dc.GetTextExtent(sMax); _ASSERTE(-1 < siz.cx); return siz.cx; } // Returns the largest number of data points in any series. int MyGraph::GetMaxSeriesSize() const { VALIDATE; int nMax(0); POSITION pos(m_olMyGraphSeries.GetHeadPosition()); while (pos) { MyGraphSeries* pSeries = static_cast (m_olMyGraphSeries.GetNext(pos)); ASSERT_VALID(pSeries); nMax = max(nMax, pSeries->m_dwaValues.GetSize()); } return nMax; } // Returns the largest number of non-zero data points in any series. int MyGraph::GetMaxNonZeroSeriesSize() const { VALIDATE; int nMax(0); POSITION pos(m_olMyGraphSeries.GetHeadPosition()); while (pos) { MyGraphSeries* pSeries = static_cast (m_olMyGraphSeries.GetNext(pos)); ASSERT_VALID(pSeries); nMax = max(nMax, pSeries->GetNonZeroElementCount()); } return nMax; } // Get the largest data value in all series. int MyGraph::GetMaxDataValue() const { VALIDATE; int nMax(0); POSITION pos(m_olMyGraphSeries.GetHeadPosition()); while (pos) { MyGraphSeries* pSeries = static_cast (m_olMyGraphSeries.GetNext(pos)); ASSERT_VALID(pSeries); nMax = max(nMax, pSeries->GetMaxDataValue()); } return nMax; } // How many series are populated? int MyGraph::GetNonZeroSeriesCount() const { VALIDATE; int nCount(0); POSITION pos(m_olMyGraphSeries.GetHeadPosition()); while (pos) { MyGraphSeries* pSeries = static_cast (m_olMyGraphSeries.GetNext(pos)); ASSERT_VALID(pSeries); if (0 < pSeries->GetNonZeroElementCount()) { ++nCount; } } return nCount; } // Returns the group number for the sent label; -1 if not found. int MyGraph::LookupLabel(const CString& sLabel) const { VALIDATE; _ASSERTE(! sLabel.IsEmpty()); for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) { if (0 == sLabel.CompareNoCase(m_saLegendLabels.GetAt(nGroup))) { return nGroup; } } return -1; } // void MyGraph::AddSeries(MyGraphSeries& rMyGraphSeries) { VALIDATE; ASSERT_VALID(&rMyGraphSeries); _ASSERTE(m_saLegendLabels.GetSize() == rMyGraphSeries.m_dwaValues.GetSize()); m_olMyGraphSeries.AddTail(&rMyGraphSeries); } // void MyGraph::SetXAxisLabel(const CString& sLabel) { VALIDATE; _ASSERTE(! sLabel.IsEmpty()); m_sXAxisLabel = sLabel; } // void MyGraph::SetYAxisLabel(const CString& sLabel) { VALIDATE; _ASSERTE(! sLabel.IsEmpty()); m_sYAxisLabel = sLabel; } // Returns the group number added. Also, makes sure that all the series have // this many elements. int MyGraph::AppendGroup(const CString& sLabel) { VALIDATE; _ASSERTE(! sLabel.IsEmpty()); // Add the group. int nGroup(m_saLegendLabels.GetSize()); SetLegend(nGroup, sLabel); // Make sure that all series have this element. POSITION pos(m_olMyGraphSeries.GetHeadPosition()); while (pos) { MyGraphSeries* pSeries = static_cast (m_olMyGraphSeries.GetNext(pos)); ASSERT_VALID(pSeries); if (nGroup >= pSeries->m_dwaValues.GetSize()) { pSeries->m_dwaValues.SetAtGrow(nGroup, 0); } } return nGroup; } // Set this value to the legend. void MyGraph::SetLegend(int nGroup, const CString& sLabel) { VALIDATE; _ASSERTE(0 <= nGroup); _ASSERTE(! sLabel.IsEmpty()); m_saLegendLabels.SetAtGrow(nGroup, sLabel); } // void MyGraph::SetGraphTitle(const CString& sTitle) { VALIDATE; _ASSERTE(! sTitle.IsEmpty()); m_sTitle = sTitle; } // void MyGraph::DrawGraph(CDC& dc) { VALIDATE; ASSERT_VALID(&dc); if (GetMaxSeriesSize()) { dc.SetBkMode(TRANSPARENT); // Populate the colors as a group of evenly spaced colors of maximum // saturation. int nColorsDelta(240 / GetMaxSeriesSize()); for (int nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup) { COLORREF cr(Generic::HLStoRGB(nColorsDelta * nGroup, 120, 240)); m_dwaColors.SetAtGrow(nGroup, cr); } // Reduce the graphable area by the frame window and status bar. We will // leave GAP_PIXELS pixels blank on all sides of the graph. So top-left // side of graph is at GAP_PIXELS,GAP_PIXELS and the bottom-right side // of graph is at (m_rcGraph.Height() - GAP_PIXELS), (m_rcGraph.Width() - // GAP_PIXELS). These settings are altered by axis labels and legends. CRect rcWnd; GetClientRect(&rcWnd); m_rcGraph.left = GAP_PIXELS; m_rcGraph.top = GAP_PIXELS; m_rcGraph.right = rcWnd.Width() - GAP_PIXELS; m_rcGraph.bottom = rcWnd.Height() - GAP_PIXELS; CBrush br; VERIFY(br.CreateSolidBrush(::GetSysColor(COLOR_WINDOW))); dc.FillRect(rcWnd, &br); // Draw graph title. DrawTitle(dc); // Set the axes and origin values. SetupAxes(dc); // Draw legend if there is one. if (m_saLegendLabels.GetSize()) { DrawLegend(dc); } // Draw axes unless it's a pie. if (m_eGraphType != MyGraph::Pie) { DrawAxes(dc); } // Draw series data and labels. switch (m_eGraphType) { case MyGraph::Bar: DrawSeriesBar(dc); break; case MyGraph::Line: DrawSeriesLine(dc); break; case MyGraph::Pie: DrawSeriesPie(dc); break; default: _ASSERTE(! "Bad default case"); break; } } } // Draw graph title; size is proportionate to width. void MyGraph::DrawTitle(CDC& dc) { VALIDATE; ASSERT_VALID(&dc); // Create the title font. CFont fontTitle; VERIFY(fontTitle.CreatePointFont(m_rcGraph.Width() / TITLE_DIVISOR, "Arial", &dc)); CFont* pFontOld = static_cast (dc.SelectObject(&fontTitle)); ASSERT_VALID(pFontOld); // Draw the title. m_rcTitle.SetRect(GAP_PIXELS, GAP_PIXELS, m_rcGraph.Width() + GAP_PIXELS, m_rcGraph.Height() + GAP_PIXELS); dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE | DT_TOP | DT_CALCRECT); m_rcTitle.right = m_rcGraph.Width() + GAP_PIXELS; dc.DrawText(m_sTitle, m_rcTitle, DT_CENTER | DT_NOPREFIX | DT_SINGLELINE | DT_TOP); VERIFY(dc.SelectObject(pFontOld)); } // Set the axes and origin values. void MyGraph::SetupAxes(CDC& dc) { VALIDATE; ASSERT_VALID(&dc); // Since pie has no axis lines, set to full size minus GAP_PIXELS on each // side. These are needed for legend to plot itself. if (MyGraph::Pie == m_eGraphType) { m_nXAxisWidth = m_rcGraph.Width() - (GAP_PIXELS * 2); m_nYAxisHeight = m_rcGraph.Height() - m_rcTitle.bottom; m_ptOrigin.x = GAP_PIXELS; m_ptOrigin.y = m_rcGraph.Height() - GAP_PIXELS; } else { // Bar and Line graphs. CString sTickLabel; sTickLabel.Format("%d", GetMaxDataValue()); CSize sizTickLabel(dc.GetTextExtent(sTickLabel)); // Determine axis specifications. Assume tick label and axes label // fonts are about the same size. m_ptOrigin.x = GAP_PIXELS + sizTickLabel.cx + GAP_PIXELS + sizTickLabel.cy + GAP_PIXELS + TICK_PIXELS; m_ptOrigin.y = m_rcGraph.Height() - sizTickLabel.cy - GAP_PIXELS - sizTickLabel.cy - GAP_PIXELS - TICK_PIXELS; m_nYAxisHeight = m_ptOrigin.y - m_rcTitle.bottom - (2 * GAP_PIXELS); m_nXAxisWidth = (m_rcGraph.Width() - GAP_PIXELS) - m_ptOrigin.x; } } // void MyGraph::DrawLegend(CDC& dc) { VALIDATE; ASSERT_VALID(&dc); // Create the legend font. CFont fontLegend; VERIFY(fontLegend.CreatePointFont(m_rcGraph.Height() / LEGEND_DIVISOR, "Arial", &dc)); CFont* pFontOld = static_cast (dc.SelectObject(&fontLegend)); ASSERT_VALID(pFontOld); // Get the height of each label. LOGFONT lf; ::ZeroMemory(&lf, sizeof(lf)); VERIFY(fontLegend.GetLogFont(&lf)); int nLabelHeight(abs(lf.lfHeight)); // Determine size of legend. A buffer of (GAP_PIXELS / 2) on each side, // plus the height of each label based on the pint size of the font. int nLegendHeight((GAP_PIXELS / 2) + (GetMaxSeriesSize() * nLabelHeight) + (GAP_PIXELS / 2)); // Draw the legend border. Allow LEGEND_COLOR_BAR_PIXELS pixels for // display of label bars. m_rcLegend.top = (m_rcGraph.Height() / 2) - (nLegendHeight / 2); m_rcLegend.bottom = m_rcLegend.top + nLegendHeight; m_rcLegend.right = m_rcGraph.Width() - GAP_PIXELS; m_rcLegend.left = m_rcLegend.right - GetMaxLegendLabelLength(dc) - LEGEND_COLOR_BAR_WIDTH_PIXELS; VERIFY(dc.Rectangle(m_rcLegend)); // Draw each group's label and bar. for (int nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup) { int nLabelTop(m_rcLegend.top + (nGroup * nLabelHeight) + (GAP_PIXELS / 2)); // Draw the label. VERIFY(dc.TextOut(m_rcLegend.left + GAP_PIXELS, nLabelTop, m_saLegendLabels.GetAt(nGroup))); // Determine the bar. CRect rcBar; rcBar.left = m_rcLegend.left + GAP_PIXELS + GetMaxLegendLabelLength(dc) + GAP_PIXELS; rcBar.top = nLabelTop + LEGEND_COLOR_BAR_GAP_PIXELS; rcBar.right = m_rcLegend.right - GAP_PIXELS; rcBar.bottom = rcBar.top + nLabelHeight - LEGEND_COLOR_BAR_GAP_PIXELS; VERIFY(dc.Rectangle(rcBar)); // Draw bar for group. COLORREF crBar(m_dwaColors.GetAt(nGroup)); CBrush br(crBar); CBrush* pBrushOld = dc.SelectObject(&br); ASSERT_VALID(pBrushOld); dc.SelectObject(&pBrushOld); rcBar.DeflateRect(LEGEND_COLOR_BAR_GAP_PIXELS, LEGEND_COLOR_BAR_GAP_PIXELS); dc.FillRect(rcBar, &br); } VERIFY(dc.SelectObject(pFontOld)); } // void MyGraph::DrawAxes(CDC& dc) const { VALIDATE; ASSERT_VALID(&dc); _ASSERTE(MyGraph::Pie != m_eGraphType); dc.SetTextColor(::GetSysColor(COLOR_WINDOWTEXT)); // Draw y axis. dc.MoveTo(m_ptOrigin); VERIFY(dc.LineTo(m_ptOrigin.x, m_ptOrigin.y - m_nYAxisHeight)); // Draw x axis. dc.MoveTo(m_ptOrigin); if (m_saLegendLabels.GetSize()) { VERIFY(dc.LineTo(m_ptOrigin.x + (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)), m_ptOrigin.y)); } else { VERIFY(dc.LineTo(m_ptOrigin.x + m_nXAxisWidth, m_ptOrigin.y)); } // Create the y-axis label font and draw it. CFont fontYAxes; VERIFY(fontYAxes.CreateFont( /* nHeight */ m_rcGraph.Width() / 10 / Y_AXIS_LABEL_DIVISOR, /* nWidth */ 0, /* nEscapement */ 90 * 10, /* nOrientation */ 0, /* nWeight */ FW_DONTCARE, /* bItalic */ false, /* bUnderline */ false, /* cStrikeOut */ 0, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, PROOF_QUALITY, VARIABLE_PITCH | FF_DONTCARE, "Arial")); CFont* pFontOld = static_cast (dc.SelectObject(&fontYAxes)); ASSERT_VALID(pFontOld); CSize sizYLabel(dc.GetTextExtent(m_sYAxisLabel)); VERIFY(dc.TextOut(GAP_PIXELS, (m_rcGraph.Height() - sizYLabel.cy) / 2, m_sYAxisLabel)); // Create the x-axis label font and draw it. CFont fontXAxes; VERIFY(fontXAxes.CreatePointFont(m_rcGraph.Width() / X_AXIS_LABEL_DIVISOR, "Arial", &dc)); VERIFY(dc.SelectObject(&fontXAxes)); CSize sizXLabel(dc.GetTextExtent(m_sXAxisLabel)); VERIFY(dc.TextOut(m_ptOrigin.x + (m_nXAxisWidth - sizXLabel.cx) / 2, m_rcGraph.Height() - GAP_PIXELS - sizXLabel.cy, m_sXAxisLabel)); // We hardwire TITLE_DIVISOR y-axis ticks here for simplicity. int nTickCount(min(Y_AXIS_MAX_TICK_COUNT, GetMaxDataValue())); int nTickSpace(m_nYAxisHeight / nTickCount); for (int nTick = 0; nTick < nTickCount; ++nTick) { int nTickYLocation(m_ptOrigin.y - (nTickSpace * (nTick + 1))); dc.MoveTo(m_ptOrigin.x - TICK_PIXELS, nTickYLocation); VERIFY(dc.LineTo(m_ptOrigin.x + TICK_PIXELS, nTickYLocation)); // Draw tick label. CString sTickLabel; sTickLabel.Format("%d", (GetMaxDataValue() * (nTick + 1)) / nTickCount); CSize sizTickLabel(dc.GetTextExtent(sTickLabel)); VERIFY(dc.TextOut(m_ptOrigin.x - GAP_PIXELS - sizTickLabel.cx - TICK_PIXELS, nTickYLocation - sizTickLabel.cy, sTickLabel)); } // Draw X axis tick marks. POSITION pos(m_olMyGraphSeries.GetHeadPosition()); int nSeries(0); while (pos) { MyGraphSeries* pSeries = static_cast (m_olMyGraphSeries.GetNext(pos)); ASSERT_VALID(pSeries); // Ignore unpopulated series if bar chart. if (m_eGraphType != MyGraph::Bar || 0 < pSeries->GetNonZeroElementCount()) { // Get the spacing of the series. _ASSERTE(GetNonZeroSeriesCount() && "Div by zero coming"); int nSeriesSpace(0); if (m_saLegendLabels.GetSize()) { nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) / (m_eGraphType == MyGraph::Bar ? GetNonZeroSeriesCount() : m_olMyGraphSeries.GetCount()); } else { nSeriesSpace = m_nXAxisWidth / (m_eGraphType == MyGraph::Bar ? GetNonZeroSeriesCount() : m_olMyGraphSeries.GetCount()); } int nTickXLocation(m_ptOrigin.x + ((nSeries + 1) * nSeriesSpace) - (nSeriesSpace / 2)); dc.MoveTo(nTickXLocation, m_ptOrigin.y - TICK_PIXELS); VERIFY(dc.LineTo(nTickXLocation, m_ptOrigin.y + TICK_PIXELS)); // Draw x-axis tick label. CString sTickLabel(pSeries->GetLabel()); CSize sizTickLabel(dc.GetTextExtent(sTickLabel)); VERIFY(dc.TextOut(nTickXLocation - (sizTickLabel.cx / 2), m_ptOrigin.y + sizTickLabel.cy, sTickLabel)); ++nSeries; } } VERIFY(dc.SelectObject(pFontOld)); } // void MyGraph::DrawSeriesBar(CDC& dc) const { VALIDATE; ASSERT_VALID(&dc); // How much space does each series get (includes interseries space)? // We ignore series whose members are all zero. int nSeriesSpace(0); if (m_saLegendLabels.GetSize()) { nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) / GetNonZeroSeriesCount(); } else { nSeriesSpace = m_nXAxisWidth / GetNonZeroSeriesCount(); } // Determine width of bars. Data points with a value of zero are assumed // to be empty. This is a bad assumption. int nBarWidth(nSeriesSpace / GetMaxNonZeroSeriesSize()); if (1 < GetNonZeroSeriesCount()) { nBarWidth = (int) ((double) nBarWidth * INTERSERIES_PERCENT_USED); } // This is the width of the largest series (no interseries space). int nMaxSeriesPlotSize(GetMaxNonZeroSeriesSize() * nBarWidth); // Iterate the series. POSITION pos(m_olMyGraphSeries.GetHeadPosition()); int nSeries(0); while (pos) { MyGraphSeries* pSeries = static_cast (m_olMyGraphSeries.GetNext(pos)); ASSERT_VALID(pSeries); // Ignore unpopulated series. if (0 < pSeries->GetNonZeroElementCount()) { // Draw each bar; empty bars are not drawn. int nRunningLeft(m_ptOrigin.x + ((nSeries + 1) * nSeriesSpace) - nMaxSeriesPlotSize); for (int nGroup = 0; nGroup < GetMaxSeriesSize(); ++nGroup) { if (pSeries->GetData(nGroup)) { CRect rcBar; rcBar.left = nRunningLeft; rcBar.top = m_ptOrigin.y - (m_nYAxisHeight * pSeries->GetData(nGroup)) / GetMaxDataValue(); rcBar.right = rcBar.left + nBarWidth; rcBar.bottom = m_ptOrigin.y; pSeries->SetTipRegion(nGroup, rcBar); COLORREF crBar(m_dwaColors.GetAt(nGroup)); CBrush br(crBar); CBrush* pBrushOld = dc.SelectObject(&br); ASSERT_VALID(pBrushOld); VERIFY(dc.Rectangle(rcBar)); dc.SelectObject(&pBrushOld); nRunningLeft += nBarWidth; } } ++nSeries; } } } // void MyGraph::DrawSeriesLine(CDC& dc) const { VALIDATE; ASSERT_VALID(&dc); // Iterate the groups. CPoint ptLastLoc(0,0); for (int nGroup = 0; nGroup < GetMaxSeriesSize(); nGroup++) { // How much space does each series get (includes interseries space)? int nSeriesSpace(0); if (m_saLegendLabels.GetSize()) { nSeriesSpace = (m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) / m_olMyGraphSeries.GetCount(); } else { nSeriesSpace = m_nXAxisWidth / m_olMyGraphSeries.GetCount(); } // Determine width of bars. int nBarWidth(nSeriesSpace / GetMaxSeriesSize()); if (1 < m_olMyGraphSeries.GetCount()) { nBarWidth = (int) ((double) nBarWidth * INTERSERIES_PERCENT_USED); } // This is the width of the largest series (no interseries space). int nMaxSeriesPlotSize(GetMaxSeriesSize() * nBarWidth); // Iterate the series. POSITION pos(m_olMyGraphSeries.GetHeadPosition()); for (int nSeries = 0; nSeries < m_olMyGraphSeries.GetCount(); ++nSeries) { MyGraphSeries* pSeries = static_cast (m_olMyGraphSeries.GetNext(pos)); ASSERT_VALID(pSeries); // Get x and y location of center of ellipse. CPoint ptLoc(0,0); ptLoc.x = m_ptOrigin.x + (((nSeries + 1) * nSeriesSpace) - (nSeriesSpace / 2)); double dLineHeight(pSeries->GetData(nGroup) * m_nYAxisHeight / GetMaxDataValue()); ptLoc.y = (int) ((double) m_ptOrigin.y - dLineHeight); // Build objects. COLORREF crLine(m_dwaColors.GetAt(nGroup)); CBrush br(crLine); CBrush* pBrushOld = dc.SelectObject(&br); ASSERT_VALID(pBrushOld); // Draw line back to last data member. if (nSeries > 0) { CPen penLine(PS_SOLID, 1, crLine); CPen* pPenOld = dc.SelectObject(&penLine); ASSERT_VALID(pPenOld); dc.MoveTo(ptLastLoc.x + 2, ptLastLoc.y - 1); VERIFY(dc.LineTo(ptLoc.x - 3, ptLoc.y - 1)); VERIFY(dc.SelectObject(pPenOld)); } // Now draw ellipse. CRect rcEllipse(ptLoc.x - 3, ptLoc.y - 3, ptLoc.x + 3, ptLoc.y + 3); VERIFY(dc.Ellipse(rcEllipse)); pSeries->SetTipRegion(nGroup, rcEllipse); dc.SelectObject(&pBrushOld); ptLastLoc = ptLoc; } } } // void MyGraph::DrawSeriesPie(CDC& dc) const { VALIDATE; ASSERT_VALID(&dc); _ASSERTE(0 < GetNonZeroSeriesCount() && "Div by zero"); // Determine width of pie display area (pie and space). int nSeriesSpace(0); if (m_saLegendLabels.GetSize()) { int nPieAndSpaceWidth((m_nXAxisWidth - m_rcLegend.Width() - (GAP_PIXELS * 2)) / GetNonZeroSeriesCount()); // Height is limiting factor. if (nPieAndSpaceWidth > m_nYAxisHeight - (GAP_PIXELS * 2)) { nSeriesSpace = (m_nYAxisHeight - (GAP_PIXELS * 2)) / GetNonZeroSeriesCount(); } else { // Width is limiting factor. nSeriesSpace = nPieAndSpaceWidth; } } else { // No legend box. // Height is limiting factor. if (m_nXAxisWidth > m_nYAxisHeight) { nSeriesSpace = m_nYAxisHeight / GetNonZeroSeriesCount(); } else { // Width is limiting factor. nSeriesSpace = m_nXAxisWidth / GetNonZeroSeriesCount(); } } // Draw each pie. int nPie(0); int nRadius((int) (nSeriesSpace * INTERSERIES_PERCENT_USED / 2.0)); POSITION pos(m_olMyGraphSeries.GetHeadPosition()); while (pos) { MyGraphSeries* pSeries = static_cast (m_olMyGraphSeries.GetNext(pos)); ASSERT_VALID(pSeries); // Don't leave a space for empty pies. if (0 < pSeries->GetNonZeroElementCount()) { // Locate this pie. CRect rcPie; rcPie.left = m_ptOrigin.x + GAP_PIXELS + (nSeriesSpace * nPie); rcPie.right = rcPie.left + (2 * nRadius); rcPie.top = (m_nYAxisHeight / 2) - nRadius; rcPie.bottom = (m_nYAxisHeight / 2) + nRadius; CPoint ptCenter((rcPie.left + rcPie.right) / 2, (rcPie.top + rcPie.bottom) / 2); // Draw series label. CSize sizPieLabel(dc.GetTextExtent(pSeries->GetLabel())); VERIFY(dc.TextOut((rcPie.left + nRadius) - (sizPieLabel.cx / 2), ptCenter.y + nRadius + GAP_PIXELS, pSeries->GetLabel())); // How much do the wedges total to? double dPieTotal(pSeries->GetDataTotal()); // Draw each wedge in this pie. CPoint ptStart(rcPie.left, ptCenter.y); double dRunningWedgeTotal(0.0); for (int nGroup = 0; nGroup < m_saLegendLabels.GetSize(); ++nGroup) { // Ignore empty wedges. if (0 < pSeries->GetData(nGroup)) { // Get the degrees of this wedge. dRunningWedgeTotal += pSeries->GetData(nGroup); double dPercent(dRunningWedgeTotal * 100.0 / dPieTotal); int nDegrees((int) (360.0 * dPercent / 100.0)); // Find the location of the wedge's endpoint. CPoint ptEnd(WedgeEndFromDegrees(nDegrees, ptCenter, nRadius)); // Special case: a wedge that takes up the whole pie would // otherwise be confused with an empty wedge. if (1 == pSeries->GetNonZeroElementCount()) { _ASSERTE(360 == nDegrees && ptStart == ptEnd && "This is the problem we're correcting"); --ptEnd.y; } // If the wedge is of zero size, don't paint it! if (ptStart != ptEnd) { // Draw wedge. COLORREF crWedge(m_dwaColors.GetAt(nGroup)); CBrush br(crWedge); CBrush* pBrushOld = dc.SelectObject(&br); ASSERT_VALID(pBrushOld); VERIFY(dc.Pie(rcPie, ptStart, ptEnd)); // Now "psuedo-draw" the wedge to create a region from the // path we create during the draw. This region is used to // tell the tip when to pop. VERIFY(dc.BeginPath()); CreateTipRgn(dc, rcPie, ptCenter, ptStart, ptEnd); VERIFY(dc.EndPath()); CRgn* prgnWedge = new CRgn; VERIFY(prgnWedge->CreateFromPath(&dc)); pSeries->SetTipRegion(nGroup, prgnWedge); // Cleanup. dc.SelectObject(pBrushOld); ptStart = ptEnd; } } } ++nPie; } } } // We want to create the region from the path we describe. In WinNT and // Win2000, the CDC::Pie() method can be called after BeginPath(), but in // Win95/Win98 it cannot. So, we have to determine which OS we're running, // and then either call Pie(), or draw a triangle that approximates the // pie wedge. void MyGraph::CreateTipRgn(CDC& dc, const CRect& rcPie, const CPoint& ptCenter, const CPoint& ptStart, const CPoint& ptEnd) const { VALIDATE; // We only want to determine the OS version one time, for efficiency. static bool bIsWin95OrWin98(false); static OSVERSIONINFO osvi; if (osvi.dwOSVersionInfoSize != sizeof(osvi)) { osvi.dwOSVersionInfoSize = sizeof(osvi); VERIFY(::GetVersionEx(&osvi)); if (VER_PLATFORM_WIN32_WINDOWS == osvi.dwPlatformId && (0 == osvi.dwMinorVersion || 10 == osvi.dwMinorVersion)) { bIsWin95OrWin98 = true; } } // Now create the path, one way or the other. if (bIsWin95OrWin98) { dc.MoveTo(ptCenter); VERIFY(dc.LineTo(ptStart)); VERIFY(dc.Arc(rcPie, ptStart, ptEnd)); VERIFY(dc.LineTo(ptEnd)); VERIFY(dc.LineTo(ptCenter)); } else { if (dc.Pie(rcPie, ptStart, ptEnd)) { ; } else { DOMYLOG ("CDC::Pie() failed!\n"); } } } // Convert degrees to x and y coords. CPoint MyGraph::WedgeEndFromDegrees(int nDegrees, const CPoint& ptCenter, int nRadius) const { VALIDATE; CPoint pt; pt.x = (int) ((double) nRadius * cos((double) nDegrees / 360.0 * PI * 2.0)); pt.x = ptCenter.x - pt.x; pt.y = (int) ((double) nRadius * sin((double) nDegrees / 360.0 * PI * 2.0)); pt.y = ptCenter.y + pt.y; return pt; }