// MyGraph.h
#if !defined(MYGRAPHH__9DB68B4D_3C7C_47E2_9F72_EEDA5D2CDBB0__INCLUDED_)#define MYGRAPHH__9DB68B4D_3C7C_47E2_9F72_EEDA5D2CDBB0__INCLUDED_#pragma once
/// MyGraphSeries
class MyGraphSeries : public CObject{ friend class MyGraph;
// Construction.public: MyGraphSeries(const CString& sLabel = ""); virtual ~MyGraphSeries();
// Declared but not defined.private: MyGraphSeries(const MyGraphSeries& rhs); MyGraphSeries& operator=(const MyGraphSeries& rhs);
// Operations.public: void SetLabel(const CString& sLabel); CString GetLabel() const; void SetData(int nGroup, int nValue); int GetData(int nGroup) const;
// Implementation.private: int GetMaxDataValue() const; int GetNonZeroElementCount() const; int GetDataTotal() const; void SetTipRegion(int nGroup, const CRect& rc); void SetTipRegion(int nGroup, const CRgn* prgn); int HitTest(const CPoint& pt) const; CString GetTipText(int nGroup) const;
// Data.private: CString m_sLabel; // Series label. CDWordArray m_dwaValues; // Values array. CObArray m_oaRegions; // Tooltip regions.};
/// MyGraph
class MyGraph : public CStatic{// Enum.public: enum GraphType { Bar, Line, Pie };
// Construction.public: MyGraph(GraphType eGraphType = MyGraph::Pie); virtual ~MyGraph();
// Declared but not defined.private: MyGraph(const MyGraph& rhs); MyGraph& operator=(const MyGraph& rhs);
// Operations.public: void AddSeries(MyGraphSeries& rMyGraphSeries); void SetXAxisLabel(const CString& sLabel); void SetYAxisLabel(const CString& sLabel); int AppendGroup(const CString& sLabel); void SetLegend(int nGroup, const CString& sLabel); void SetGraphType(GraphType eType); void SetGraphTitle(const CString& sTitle); int LookupLabel(const CString& sLabel) const;
// Implementation.private: void DrawGraph(CDC& dc); void DrawTitle(CDC& dc); void SetupAxes(CDC& dc); void DrawAxes(CDC& dc) const; void DrawLegend(CDC& dc); void DrawSeriesBar(CDC& dc) const; void DrawSeriesLine(CDC& dc) const; void DrawSeriesPie(CDC& dc) const;
int GetMaxLegendLabelLength(CDC& dc) const; int GetMaxSeriesSize() const; int GetMaxNonZeroSeriesSize() const; int GetMaxDataValue() const; int GetNonZeroSeriesCount() const;
CString GetTipText() const;
int OnToolHitTest(CPoint point, TOOLINFO* pTI) const;
CPoint WedgeEndFromDegrees(int nDegrees, const CPoint& ptCenter, int nRadius) const;
static UINT SpinTheMessageLoop(bool bNoDrawing = false, bool bOnlyDrawing = false, UINT uiMsgAllowed = WM_NULL);
static void RGBtoHLS(COLORREF crRGB, WORD& wH, WORD& wL, WORD& wS); static COLORREF HLStoRGB(WORD wH, WORD wL, WORD wS); static WORD HueToRGB(WORD w1, WORD w2, WORD wH);
// Overrides // ClassWizard generated virtual function overrides //{{AFX_VIRTUAL(MyGraph) protected: virtual void PreSubclassWindow(); //}}AFX_VIRTUAL
// Generated message map functionsprotected: //{{AFX_MSG(MyGraph) afx_msg void OnPaint(); afx_msg void OnSize(UINT nType, int cx, int cy); //}}AFX_MSG afx_msg bool OnNeedText(UINT uiId, NMHDR* pNMHDR, LRESULT* pResult); DECLARE_MESSAGE_MAP()
// Data.private: int m_nXAxisWidth; int m_nYAxisHeight; CPoint m_ptOrigin; CRect m_rcGraph; CRect m_rcLegend; CRect m_rcTitle; CString m_sXAxisLabel; CString m_sYAxisLabel; CString m_sTitle; CDWordArray m_dwaColors; CStringArray m_saLegendLabels; CObList m_olMyGraphSeries; GraphType m_eGraphType;};
//{{AFX_INSERT_LOCATION}}// Microsoft Visual C++ will insert additional declarations immediately before the previous line.
#endif // !defined(MYGRAPHH__9DB68B4D_3C7C_47E2_9F72_EEDA5D2CDBB0__INCLUDED_)
// MyGraph.cpp
#include "stdafx.h"#include "MyGraph.h"
#include "math.h"
#ifdef _DEBUG#define new DEBUG_NEW#undef THIS_FILEstatic char THIS_FILE[] = __FILE__;#endif
/// This macro can be called at the beginning and ending of every// method. It is identical to saying "ASSERT_VALID(); ASSERT_KINDOF();"// but is written like this so that VALIDATE can be a macro. It is useful// as an "early warning" that something has gone wrong with "this" object.#ifndef VALIDATE #ifdef _DEBUG #define VALIDATE ::AfxAssertValidObject(this, __FILE__ , __LINE__ ); / _ASSERTE(IsKindOf(GetRuntimeClass())); #else #define VALIDATE #endif#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<CRgn*> (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<int> (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<CRgn*> (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<TOOLTIPTEXTA*> (pNMHDR); ASSERT_POINTER(pTTTA, TOOLTIPTEXTA);
TOOLTIPTEXTW* pTTTW = reinterpret_cast<TOOLTIPTEXTW*> (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; }
MyGraph::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<MyGraphSeries*> (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<MyGraphSeries*> (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<MyGraphSeries*> (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<MyGraphSeries*> (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<MyGraphSeries*> (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<MyGraphSeries*> (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(MyGraph::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<CFont*> (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<CFont*> (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<CFont*> (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<MyGraphSeries*> (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<MyGraphSeries*> (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<MyGraphSeries*> (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<MyGraphSeries*> (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));
// Create a region from the path we create. VERIFY(dc.BeginPath()); VERIFY(dc.Pie(rcPie, ptStart, ptEnd)); VERIFY(dc.EndPath()); CRgn* prgnWedge = new CRgn; VERIFY(prgnWedge->CreateFromPath(&dc)); pSeries->SetTipRegion(nGroup, prgnWedge);
// Cleanup. dc.SelectObject(pBrushOld); ptStart = ptEnd; } } }
++nPie; } }}
// 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;}
// Spin The Message Loop: C++ version. See "Advanced Windows Programming", // M. Heller, p. 153, and the MS TechNet CD, PSS ID Number: Q99999./* static */ UINT MyGraph::SpinTheMessageLoop(bool bNoDrawing /* = false */ , bool bOnlyDrawing /* = false */ , UINT uiMsgAllowed /* = WM_NULL */ ){ MSG msg; ::ZeroMemory(&msg, sizeof(msg));
while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
// Do painting only. if (bOnlyDrawing && WM_PAINT == msg.message) { ::TranslateMessage(&msg); ::DispatchMessage(&msg);
// Update user interface. AfxGetApp()->OnIdle(0); } // Do everything *but* painting. else if (bNoDrawing && WM_PAINT == msg.message) { break; } // Special handling for this message. else if (WM_QUIT == msg.message) { ::PostQuitMessage(msg.wParam); break; } // Allow one message (like WM_LBUTTONDOWN). else if (uiMsgAllowed == msg.message && ! AfxGetApp()->PreTranslateMessage(&msg)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg); break; } // This is the general case. else if (! bOnlyDrawing && ! AfxGetApp()->PreTranslateMessage(&msg)) { ::TranslateMessage(&msg); ::DispatchMessage(&msg);
// Update user interface, then free temporary objects. AfxGetApp()->OnIdle(0); AfxGetApp()->OnIdle(1); } }
return msg.message;}
/// Conversion routines: RGB to HLS (Red-Green-Blue to Hue-Luminosity-Saturation).// See Microsoft KnowledgeBase article Q29240.
#define HLSMAX 240 // H,L, and S vary over 0-HLSMAX#define RGBMAX 255 // R,G, and B vary over 0-RGBMAX // HLSMAX BEST IF DIVISIBLE BY 6 // RGBMAX, HLSMAX must each fit in a byte (255).
#define UNDEFINED (HLSMAX * 2 / 3) // Hue is undefined if Saturation is 0 // (grey-scale). This value determines // where the Hue scrollbar is initially // set for achromatic colors.
// Convert HLS to RGB./* static */ COLORREF MyGraph::HLStoRGB(WORD wH, WORD wL, WORD wS){ _ASSERTE(0 <= wH && 240 >= wH && "Illegal hue value"); _ASSERTE(0 <= wL && 240 >= wL && "Illegal lum value"); _ASSERTE(0 <= wS && 240 >= wS && "Illegal sat value");
WORD wR(0); WORD wG(0); WORD wB(0); // Achromatic case. if (0 == wS) { wR = wG = wB = (wL * RGBMAX) / HLSMAX;
if (UNDEFINED != wH) { _ASSERTE(! "ERROR"); } } else { // Chromatic case. WORD Magic1(0); WORD Magic2(0);
// Set up magic numbers. if (wL <= HLSMAX / 2) { Magic2 = (wL * (HLSMAX + wS) + (HLSMAX / 2)) / HLSMAX; } else { Magic2 = wL + wS - ((wL * wS) + (HLSMAX / 2)) / HLSMAX; }
Magic1 = 2 * wL - Magic2; // Get RGB, change units from HLSMAX to RGBMAX. wR = (HueToRGB(Magic1, Magic2, wH + (HLSMAX / 3)) * RGBMAX + (HLSMAX / 2)) / HLSMAX; wG = (HueToRGB(Magic1, Magic2, wH) * RGBMAX + (HLSMAX / 2)) / HLSMAX; wB = (HueToRGB(Magic1, Magic2, wH - (HLSMAX / 3)) * RGBMAX + (HLSMAX / 2)) / HLSMAX; } return RGB(wR,wG,wB);}
// Utility routine for HLStoRGB./* static */ WORD MyGraph::HueToRGB(WORD w1, WORD w2, WORD wH){ // Range check: note values passed add/subtract thirds of range. if (wH < 0) { wH += HLSMAX; }
if (wH > HLSMAX) { wH -= HLSMAX; }
// Return r, g, or b value from this tridrant. if (wH < HLSMAX / 6) { return w1 + (((w2 - w1) * wH + (HLSMAX / 12)) / (HLSMAX / 6)); }
if (wH < HLSMAX / 2) { return w2; }
if (wH < (HLSMAX * 2) / 3) { return w1 + (((w2 - w1) * (((HLSMAX * 2) / 3) - wH) + (HLSMAX / 12)) / (HLSMAX / 6)); } else { return w1; }}