| 1 | /*************************************************************************** |
|---|
| 2 | * Copyright (C) 2009 by Guy Rutenberg * |
|---|
| 3 | * guyrutenberg@gmail.com * |
|---|
| 4 | * * |
|---|
| 5 | * This program is free software; you can redistribute it and/or modify * |
|---|
| 6 | * it under the terms of the GNU General Public License as published by * |
|---|
| 7 | * the Free Software Foundation; either version 2 of the License, or * |
|---|
| 8 | * (at your option) any later version. * |
|---|
| 9 | * * |
|---|
| 10 | * This program is distributed in the hope that it will be useful, * |
|---|
| 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of * |
|---|
| 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * |
|---|
| 13 | * GNU General Public License for more details. * |
|---|
| 14 | * * |
|---|
| 15 | * You should have received a copy of the GNU General Public License * |
|---|
| 16 | * along with this program; if not, write to the * |
|---|
| 17 | * Free Software Foundation, Inc., * |
|---|
| 18 | * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * |
|---|
| 19 | ***************************************************************************/ |
|---|
| 20 | |
|---|
| 21 | #include "simple_pie_plot.h" |
|---|
| 22 | #include <boost/foreach.hpp> |
|---|
| 23 | #include <memory> |
|---|
| 24 | #include <algorithm> |
|---|
| 25 | #include <cmath> |
|---|
| 26 | using namespace std; |
|---|
| 27 | using namespace simple_pie_plot; |
|---|
| 28 | |
|---|
| 29 | const double PI = 4.0 * atan(1.0); |
|---|
| 30 | |
|---|
| 31 | /** |
|---|
| 32 | * Draws a pie slice with origin in (\a x,\a y), radius (\a r) from |
|---|
| 33 | * from \a start_angle to \a end_angle, where angles are in radians measured |
|---|
| 34 | * from the x-axis. The figure is drawn using \a dc. |
|---|
| 35 | */ |
|---|
| 36 | void DrawPieSlice(double x, double y, double r, double start_angle, |
|---|
| 37 | double end_angle, wxGraphicsContext *dc) |
|---|
| 38 | { |
|---|
| 39 | wxGraphicsPath path = dc->CreatePath(); |
|---|
| 40 | path.MoveToPoint(x,y); |
|---|
| 41 | path.AddArc(x, y, r, start_angle, end_angle, true); |
|---|
| 42 | dc->DrawPath(path); |
|---|
| 43 | } |
|---|
| 44 | |
|---|
| 45 | SimplePiePlot::SimplePiePlot(wxWindow* parent, wxWindowID id, |
|---|
| 46 | const wxPoint& pos, const wxSize& size, |
|---|
| 47 | long style, const wxString& name) |
|---|
| 48 | : wxPanel(parent, id, pos, size, style, name) |
|---|
| 49 | { |
|---|
| 50 | m_highlight = -1; |
|---|
| 51 | |
|---|
| 52 | m_max_legend_width = 0; |
|---|
| 53 | m_max_legend_height = 0; |
|---|
| 54 | m_legend_width = 0; |
|---|
| 55 | |
|---|
| 56 | Connect(this->GetId(), wxEVT_PAINT, wxPaintEventHandler(SimplePiePlot::OnPaint)); |
|---|
| 57 | Connect(this->GetId(), wxEVT_SIZE, wxSizeEventHandler(SimplePiePlot::OnResize)); |
|---|
| 58 | |
|---|
| 59 | Connect(this->GetId(), wxEVT_MOTION, wxMouseEventHandler(SimplePiePlot::OnMouseMove)); |
|---|
| 60 | Connect(this->GetId(), wxEVT_LEAVE_WINDOW, wxMouseEventHandler(SimplePiePlot::OnMouseLeaveWindow)); |
|---|
| 61 | } |
|---|
| 62 | |
|---|
| 63 | void SimplePiePlot::OnPaint(wxPaintEvent& event) |
|---|
| 64 | { |
|---|
| 65 | wxPaintDC pdc(this); |
|---|
| 66 | auto_ptr<wxGraphicsContext> dc(wxGraphicsContext::Create(pdc)); |
|---|
| 67 | wxBrush color_brush; |
|---|
| 68 | |
|---|
| 69 | if (!m_max_legend_width || !m_max_legend_height) { |
|---|
| 70 | CalculateLegendDimensions(dc.get()); |
|---|
| 71 | // This might have caused a new minimum size to occure, tell |
|---|
| 72 | // parent about it by refitting. See also comment in |
|---|
| 73 | // GetMinSize() |
|---|
| 74 | if (this->GetParent() && this->GetParent()->GetSizer()) |
|---|
| 75 | this->GetParent()->GetSizer()->SetSizeHints(this->GetParent()); |
|---|
| 76 | } |
|---|
| 77 | CalculatePiePlotLocation(); |
|---|
| 78 | |
|---|
| 79 | |
|---|
| 80 | dc->SetPen(*wxTRANSPARENT_PEN); |
|---|
| 81 | for (int i = 1; i<m_angles.size(); i++) { |
|---|
| 82 | // create brush |
|---|
| 83 | color_brush.SetColour(GetSegmentColor(i-1)); |
|---|
| 84 | dc->SetBrush(color_brush); |
|---|
| 85 | |
|---|
| 86 | DrawPieSlice(m_pie_x, m_pie_y, m_radius, m_angles[i-1], |
|---|
| 87 | m_angles[i], dc.get()); |
|---|
| 88 | } |
|---|
| 89 | |
|---|
| 90 | DrawLegend(dc.get()); |
|---|
| 91 | } |
|---|
| 92 | |
|---|
| 93 | void SimplePiePlot::CalculatePiePlotLocation() |
|---|
| 94 | { |
|---|
| 95 | double pie_width; |
|---|
| 96 | int width, height; |
|---|
| 97 | |
|---|
| 98 | assert(m_legend_width); |
|---|
| 99 | |
|---|
| 100 | GetClientSize(&width, &height); |
|---|
| 101 | // m_max_legend_height* for the color box and it's right padding |
|---|
| 102 | pie_width = width - m_legend_width - m_legend_left_padding; |
|---|
| 103 | m_pie_x = pie_width/2.0; |
|---|
| 104 | m_pie_y = height/2.0; |
|---|
| 105 | m_radius = min(pie_width,(double)height)/2.0; |
|---|
| 106 | } |
|---|
| 107 | |
|---|
| 108 | void SimplePiePlot::OnResize(wxSizeEvent& event) |
|---|
| 109 | { |
|---|
| 110 | Refresh(); |
|---|
| 111 | event.Skip(); |
|---|
| 112 | } |
|---|
| 113 | |
|---|
| 114 | void SimplePiePlot::SetData(vector<double> d, vector<wxString> labels) |
|---|
| 115 | { |
|---|
| 116 | // number of values should match number of labels |
|---|
| 117 | assert(labels.size() == d.size()); |
|---|
| 118 | |
|---|
| 119 | m_data.clear(); |
|---|
| 120 | m_data_total = 0; |
|---|
| 121 | BOOST_FOREACH(double tmp, d) { |
|---|
| 122 | // we can't plot negative values |
|---|
| 123 | assert(tmp >= 0); |
|---|
| 124 | m_data_total += tmp; |
|---|
| 125 | m_data.push_back(tmp); |
|---|
| 126 | } |
|---|
| 127 | m_labels.assign(labels.begin(), labels.end()); |
|---|
| 128 | CalculateAngles(); |
|---|
| 129 | |
|---|
| 130 | // this will cause the legend size to be re-calculated |
|---|
| 131 | m_max_legend_width = 0; |
|---|
| 132 | m_max_legend_height = 0; |
|---|
| 133 | } |
|---|
| 134 | |
|---|
| 135 | void SimplePiePlot::CalculateAngles() |
|---|
| 136 | { |
|---|
| 137 | double new_angle = 0; |
|---|
| 138 | |
|---|
| 139 | m_angles.clear(); |
|---|
| 140 | m_angles.push_back(new_angle); |
|---|
| 141 | BOOST_FOREACH(double tmp, m_data) { |
|---|
| 142 | new_angle = new_angle + 2 * PI * (tmp/m_data_total); |
|---|
| 143 | m_angles.push_back(new_angle); |
|---|
| 144 | } |
|---|
| 145 | } |
|---|
| 146 | |
|---|
| 147 | void SimplePiePlot::CalculateLegendDimensions(wxGraphicsContext *dc) |
|---|
| 148 | { |
|---|
| 149 | double tmp_height; |
|---|
| 150 | double tmp_width; |
|---|
| 151 | m_max_legend_width = 0; |
|---|
| 152 | m_max_legend_height = 0; |
|---|
| 153 | |
|---|
| 154 | dc->SetFont(*wxNORMAL_FONT, *wxBLACK); |
|---|
| 155 | |
|---|
| 156 | BOOST_FOREACH(wxString label, m_labels) { |
|---|
| 157 | dc->GetTextExtent(label, &tmp_width, &tmp_height, NULL, NULL); |
|---|
| 158 | m_max_legend_width = max(m_max_legend_width, tmp_width); |
|---|
| 159 | m_max_legend_height = max(m_max_legend_height, tmp_height); |
|---|
| 160 | } |
|---|
| 161 | |
|---|
| 162 | m_legend_width = m_max_legend_width + 2*m_max_legend_height; |
|---|
| 163 | m_legend_line_height = 1.5 * m_max_legend_height; |
|---|
| 164 | } |
|---|
| 165 | |
|---|
| 166 | void SimplePiePlot::DrawLegend(wxGraphicsContext *dc) |
|---|
| 167 | { |
|---|
| 168 | wxBrush color_brush; |
|---|
| 169 | int width, height; |
|---|
| 170 | GetClientSize(&width, &height); |
|---|
| 171 | |
|---|
| 172 | assert(m_legend_width); |
|---|
| 173 | double legend_x = width - m_legend_width; |
|---|
| 174 | |
|---|
| 175 | dc->SetPen(*wxTRANSPARENT_PEN); |
|---|
| 176 | dc->SetFont(*wxNORMAL_FONT, *wxBLACK); |
|---|
| 177 | |
|---|
| 178 | for (int i = 0; i<m_labels.size(); i++) { |
|---|
| 179 | color_brush.SetColour(GetSegmentColor(i)); |
|---|
| 180 | dc->SetBrush(color_brush); |
|---|
| 181 | |
|---|
| 182 | dc->DrawRectangle(legend_x, i * m_legend_line_height, |
|---|
| 183 | m_max_legend_height, m_max_legend_height); |
|---|
| 184 | dc->DrawText(m_labels[i], legend_x + 2*m_max_legend_height, |
|---|
| 185 | i * m_legend_line_height); |
|---|
| 186 | } |
|---|
| 187 | |
|---|
| 188 | } |
|---|
| 189 | |
|---|
| 190 | wxColour SimplePiePlot::GetSegmentColor(int i) |
|---|
| 191 | { |
|---|
| 192 | double hue_step = 1.0/(m_data.size()); |
|---|
| 193 | double hue = i * hue_step; |
|---|
| 194 | double value = m_highlight == i ? 1.0 : 0.9; |
|---|
| 195 | wxColour color; |
|---|
| 196 | wxImage::RGBValue rgb; |
|---|
| 197 | wxImage::HSVValue hsv(hue, 1, value); |
|---|
| 198 | rgb = wxImage::HSVtoRGB(hsv); |
|---|
| 199 | color.Set(rgb.red, rgb.green, rgb.blue); |
|---|
| 200 | |
|---|
| 201 | return color; |
|---|
| 202 | } |
|---|
| 203 | |
|---|
| 204 | void SimplePiePlot::OnMouseMove(wxMouseEvent& event) |
|---|
| 205 | { |
|---|
| 206 | //check if the move is even in the plot |
|---|
| 207 | int width, height; |
|---|
| 208 | GetClientSize(&width, &height); |
|---|
| 209 | |
|---|
| 210 | const double dist_mouse = (m_pie_x-event.m_x)*(m_pie_x-event.m_x) + |
|---|
| 211 | (m_pie_y-event.m_y)*(m_pie_y-event.m_y); |
|---|
| 212 | if (dist_mouse <= (m_radius*m_radius)) { |
|---|
| 213 | //angles are clockwise from the x-axis |
|---|
| 214 | double angle = acos((event.m_x-m_pie_x)/sqrt(dist_mouse)); |
|---|
| 215 | if (event.m_y < m_pie_y) |
|---|
| 216 | angle = 2*PI - angle; |
|---|
| 217 | int i; |
|---|
| 218 | for (i = 1; i<m_angles.size(); i++) { |
|---|
| 219 | if (angle<=m_angles[i]) |
|---|
| 220 | break; |
|---|
| 221 | } |
|---|
| 222 | Highlight(i-1); |
|---|
| 223 | return; |
|---|
| 224 | } else if (event.m_x >= width-m_legend_width){ |
|---|
| 225 | int i = (int) (event.m_y / m_legend_line_height); |
|---|
| 226 | if (i<m_labels.size()){ |
|---|
| 227 | Highlight(i); |
|---|
| 228 | return; |
|---|
| 229 | } |
|---|
| 230 | } |
|---|
| 231 | |
|---|
| 232 | ClearHighlight(); |
|---|
| 233 | } |
|---|
| 234 | |
|---|
| 235 | void SimplePiePlot::OnMouseLeaveWindow(wxMouseEvent& event) |
|---|
| 236 | { |
|---|
| 237 | ClearHighlight(); |
|---|
| 238 | } |
|---|
| 239 | |
|---|
| 240 | void SimplePiePlot::Highlight(int i) |
|---|
| 241 | { |
|---|
| 242 | m_highlight = i; |
|---|
| 243 | Refresh(); |
|---|
| 244 | } |
|---|
| 245 | |
|---|
| 246 | void SimplePiePlot::ClearHighlight() |
|---|
| 247 | { |
|---|
| 248 | if (m_highlight != -1) { |
|---|
| 249 | m_highlight = -1; |
|---|
| 250 | Refresh(); |
|---|
| 251 | } |
|---|
| 252 | } |
|---|
| 253 | |
|---|
| 254 | wxSize SimplePiePlot::GetMinSize() const |
|---|
| 255 | { |
|---|
| 256 | // In the first call to GetMinSize, m_legend_line_height and |
|---|
| 257 | // m_legend_width may not be populated yet (set to zero), this |
|---|
| 258 | // is a result of not being able to call CalculateLegendDimensions() |
|---|
| 259 | // at this point due to constness. In order to workaround this issue, |
|---|
| 260 | // if CalculateLegendDimensions() gets called in OnPaint() (this |
|---|
| 261 | // happens also after setting new data), OnPaint() also notifies the |
|---|
| 262 | // parent's sizer (is such exist) to update the size hints |
|---|
| 263 | // accordingly. |
|---|
| 264 | const double total_legend_height = m_labels.size() * m_legend_line_height; |
|---|
| 265 | const double total_width = total_legend_height + m_legend_width + |
|---|
| 266 | m_legend_left_padding; |
|---|
| 267 | return wxSize((int)ceil(total_width),(int)ceil(total_legend_height)); |
|---|
| 268 | } |
|---|
| 269 | |
|---|