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