From ce51c7061a35f4071ba91acc5a5e54c0b257f119 Mon Sep 17 00:00:00 2001 From: twanvl Date: Mon, 14 May 2007 15:37:04 +0000 Subject: [PATCH] Legends for graphs; pie scatter graphs git-svn-id: svn://svn.code.sf.net/p/magicseteditor/code/trunk@369 0fc631ac-6414-0410-93d0-97cfa31319b6 --- src/data/graph_type.hpp | 1 + src/data/statistics.cpp | 9 +- src/gui/control/graph.cpp | 182 ++++++++++++++++++++++++++++++++++++-- src/gui/control/graph.hpp | 40 ++++++++- src/util/real_point.hpp | 15 ++++ 5 files changed, 231 insertions(+), 16 deletions(-) diff --git a/src/data/graph_type.hpp b/src/data/graph_type.hpp index 5a4b7d21..1ab96af9 100644 --- a/src/data/graph_type.hpp +++ b/src/data/graph_type.hpp @@ -19,6 +19,7 @@ enum GraphType , GRAPH_TYPE_PIE , GRAPH_TYPE_STACK , GRAPH_TYPE_SCATTER +, GRAPH_TYPE_SCATTER_PIE }; // ----------------------------------------------------------------------------- : EOF diff --git a/src/data/statistics.cpp b/src/data/statistics.cpp index 5dbff95f..8d4bca25 100644 --- a/src/data/statistics.cpp +++ b/src/data/statistics.cpp @@ -119,8 +119,9 @@ void StatsCategory::find_dimensions(const vector& available) { // ----------------------------------------------------------------------------- : GraphType (from graph_type.hpp) IMPLEMENT_REFLECTION_ENUM(GraphType) { - VALUE_N("bar", GRAPH_TYPE_BAR); - VALUE_N("pie", GRAPH_TYPE_PIE); - VALUE_N("stack", GRAPH_TYPE_STACK); - VALUE_N("scatter", GRAPH_TYPE_SCATTER); + VALUE_N("bar", GRAPH_TYPE_BAR); + VALUE_N("pie", GRAPH_TYPE_PIE); + VALUE_N("stack", GRAPH_TYPE_STACK); + VALUE_N("scatter", GRAPH_TYPE_SCATTER); + VALUE_N("scatter pie", GRAPH_TYPE_SCATTER_PIE); } diff --git a/src/gui/control/graph.cpp b/src/gui/control/graph.cpp index f78d91c5..4584cab6 100644 --- a/src/gui/control/graph.cpp +++ b/src/gui/control/graph.cpp @@ -152,6 +152,20 @@ void GraphData::crossAxis(size_t axis1, size_t axis2, vector& out) const { } } +void GraphData::crossAxis(size_t axis1, size_t axis2, size_t axis3, vector& out) const { + size_t a1_size = axes[axis1]->groups.size(); + size_t a2_size = axes[axis2]->groups.size(); + size_t a3_size = axes[axis3]->groups.size(); + out.clear(); + out.resize(a1_size * a2_size * a3_size, 0); + FOR_EACH_CONST(v, values) { + int v1 = v[axis1], v2 = v[axis2], v3 = v[axis3]; + if (v1 >= 0 && v2 >= 0 && v3 >= 0) { + out[a3_size * (a2_size * v1 + v2) + v3]++; + } + } +} + // ----------------------------------------------------------------------------- : Graph1D @@ -399,7 +413,7 @@ int PieGraph::findItem(const RealPoint& pos, const RealRect& rect) const { void ScatterGraph::draw(RotatedDC& dc, const vector& current, DrawLayer layer) const { if (!data || data->axes.size() <= max(axis1,axis2)) return; - // Rectangle for bars + // Rectangle for drawing RealRect rect = dc.getInternalRect(); GraphAxis& axis1 = axis1_data(); // the major axis GraphAxis& axis2 = axis2_data(); // the stacked axis @@ -424,7 +438,7 @@ void ScatterGraph::draw(RotatedDC& dc, const vector& current, DrawLayer lay dc.SetBrush(lerp(bg,axis2.groups[cur2].color,0.3)); dc.DrawRectangle(RealRect(rect.x, rect.bottom() - (cur2+1) * size.height, rect.width, size.height)); } - } else { + } else if (layer == LAYER_VALUES) { Color fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); dc.SetPen(fg); size_t i = 0; @@ -466,9 +480,114 @@ void ScatterGraph::setData(const GraphDataP& d) { } } +// ----------------------------------------------------------------------------- : Scatter Plot plus + +void ScatterGraphPlus::setData(const GraphDataP& d) { + ScatterGraph::setData(d); + if (data->axes.size() <= max(max(axis1,axis2),axis3)) return; + d->crossAxis(axis1,axis2,axis3,values3D); +} + +// ----------------------------------------------------------------------------- : Scatter Pie graph + +void ScatterPieGraph::draw(RotatedDC& dc, const vector& current, DrawLayer layer) const { + if (data->axes.size() <= max(max(axis1,axis2),axis3)) return; + if (layer == LAYER_SELECTION) { + ScatterGraph::draw(dc, current, layer); + } else if (layer == LAYER_VALUES) { + // Rectangle for drawing + RealRect rect = dc.getInternalRect(); + GraphAxis& axis1 = axis1_data(); // the major axis + GraphAxis& axis2 = axis2_data(); // the stacked axis + GraphAxis& axis3 = axis3_data(); // the pie axis + RealSize size(rect.width / axis1.groups.size(), rect.height / axis2.groups.size()); // size for a single cell + double step = min(size.width, size.height) / sqrt((double)max_value) / 2.01; + // Draw pies + Color fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); + dc.SetPen(fg); + for (size_t x = 0 ; x < axis1.groups.size() ; ++x) { + for (size_t y = 0 ; y < axis2.groups.size() ; ++y) { + size_t i = x * axis2.groups.size() + y; + UInt value = values[i]; + double radius = sqrt((double)value) * step; + RealSize radius_s(radius*2+1,radius*2+1); + RealPoint center(rect.left() + (x+0.5) * size.width + 0.5, rect.bottom() - (y+0.5) * size.height + 0.5); + // draw pie slices + double angle = 0; + size_t j = 0; + FOR_EACH(g, axis3.groups) { + dc.SetBrush(g.color); + UInt val = values3D[i * axis3.groups.size() + j++]; + if (val > 0) { + double end_angle = angle + 2 * M_PI * (double)val / value; + dc.DrawEllipticArc(center, radius_s, angle, end_angle); + angle = end_angle; + } + } + // draw spokes? + } + } + } +} + + // ----------------------------------------------------------------------------- : Graph Legend +RealSize GraphLegend::determineSize(RotatedDC& dc) const { + if (!data) return RealSize(-1,-1); + GraphAxis& axis = axis_data(); + dc.SetFont(*wxNORMAL_FONT); + item_size = RealSize(0,0); + FOR_EACH(g, axis.groups) { + RealSize this_item_size = dc.GetTextExtent(g.name); + this_item_size = RealSize(this_item_size.width + 34, this_item_size.height + 5); + item_size = piecewise_max(item_size, this_item_size); + } + size = RealSize(item_size.width + 2, axis.groups.size() * item_size.height + 3); // margins + return size; +} + +void GraphLegend::draw(RotatedDC& dc, int current, DrawLayer layer) const { + if (!size.width) determineSize(dc); + if (layer == LAYER_VALUES) { + RealRect rect = dc.getInternalRect(); + RealPoint pos = align_in_rect(alignment, size, rect); + GraphAxis& axis = axis_data(); + Color fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); + Color bg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW); + // draw border + dc.SetBrush(bg); + dc.DrawRectangle(RealRect(pos,size)); + // draw items + dc.SetFont(*wxNORMAL_FONT); + dc.SetPen(fg); + double y = pos.y + 1; + for (int j = 0 ; j < (int)axis.groups.size() ; ++j) { + int i = reverse ? (int)axis.groups.size() - j - 1 : j; + const GraphGroup& g = axis.groups[i]; + if (i == current) { + dc.SetBrush(lerp(bg,g.color,0.5)); + dc.SetPen(*wxTRANSPARENT_PEN); + dc.DrawRectangle(RealRect(pos.x+1, y, item_size.width, item_size.height + 1)); + dc.SetPen(fg); + } + dc.SetBrush(g.color); + dc.DrawRectangle(RealRect(pos.x+3, y + 2, 26, item_size.height - 3)); + dc.DrawText(g.name, RealPoint(pos.x + 32, y + 2)); + y += item_size.height; + } + } +} +int GraphLegend::findItem(const RealPoint& pos, const RealRect& rect) const { + RealPoint mypos = align_in_rect(alignment, size, rect); + RealPoint pos2(pos.x - mypos.x, pos.y - mypos.y); + if (pos2.x < 0 || pos2.y < 0 || pos2.x >= size.width || pos2.y >= size.height) return -1; + int col = floor((pos2.y-1) / item_size.height); + if (col < 0 || col >= (int)axis_data().groups.size()) return -1; + return reverse ? (int)axis_data().groups.size() - col - 1 : col; +} + // ----------------------------------------------------------------------------- : Graph label axis void GraphLabelAxis::draw(RotatedDC& dc, int current, DrawLayer layer) const { @@ -580,7 +699,7 @@ void GraphValueAxis::draw(RotatedDC& dc, int current, DrawLayer layer) const { Color fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT); // Draw backlines (horizontal) and value labels dc.SetPen(lerp(bg, fg, 0.3)); - int highlight = current >= 0 ? (int)axis.groups[current].size : -1; + int highlight = (highlight_value && current >= 0) ? (int)axis.groups[current].size : -1; for (int i = 0 ; i <= (int)axis.max ; ++i) { if (i % label_step == 0 || i == highlight) { // highlight? @@ -595,7 +714,7 @@ void GraphValueAxis::draw(RotatedDC& dc, int current, DrawLayer layer) const { dc.DrawLine(RealPoint(rect.left() - 2, y), RealPoint(rect.right(), y)); // draw label, aligned middle right if (! ((i < highlight && i + label_step > highlight) || - (i > highlight && i - label_step < highlight)) || current == -1) { + (i > highlight && i - label_step < highlight)) || highlight == -1) { // don't draw labels before/after current to make room String label; label << i; RealSize text_size = dc.GetTextExtent(label); @@ -670,7 +789,7 @@ void GraphControl::setLayout(GraphType type) { switch (type) { case GRAPH_TYPE_BAR: { intrusive_ptr combined(new GraphContainer()); - combined->add(new_intrusive1(0)); + combined->add(new_intrusive2(0, true)); combined->add(new_intrusive2(0, HORIZONTAL)); combined->add(new_intrusive1(0)); graph = new_intrusive5(combined, 23,8,7,20); @@ -682,9 +801,10 @@ void GraphControl::setLayout(GraphType type) { break; } case GRAPH_TYPE_STACK: { intrusive_ptr combined(new GraphContainer()); - combined->add(new_intrusive1(0)); + combined->add(new_intrusive2(0, false)); combined->add(new_intrusive2(0, HORIZONTAL)); combined->add(new_intrusive2(0,1)); + combined->add(new_intrusive3(1, ALIGN_TOP_RIGHT, true)); graph = new_intrusive5(combined, 23,8,7,20); break; } case GRAPH_TYPE_SCATTER: { @@ -694,6 +814,13 @@ void GraphControl::setLayout(GraphType type) { combined->add(new_intrusive2(0,1)); graph = new_intrusive5(combined, 80,8,7,20); break; + } case GRAPH_TYPE_SCATTER_PIE: { + intrusive_ptr combined(new GraphContainer()); + combined->add(new_intrusive4(0, HORIZONTAL, false, DRAW_LINES_MID)); + combined->add(new_intrusive4(1, VERTICAL, false, DRAW_LINES_MID)); + combined->add(new_intrusive3(0,1,2)); + graph = new_intrusive5(combined, 80,8,7,20); + break; } default: graph = GraphP(); } @@ -734,10 +861,46 @@ void GraphControl::onMouseDown(wxMouseEvent& ev) { if (!graph) return; wxSize cs = GetClientSize(); if (graph->findItem(RealPoint(ev.GetX(), ev.GetY()), RealRect(RealPoint(0,0),cs), current_item)) { - wxCommandEvent ev(EVENT_GRAPH_SELECT, GetId()); - ProcessEvent(ev); - Refresh(false); + onSelectionChange(); } + ev.Skip(); // focus +} +void GraphControl::onChar(wxKeyEvent& ev) { + if (!graph) return; + GraphDataP data = graph->getData(); + if (!data) return; + switch (ev.GetKeyCode()) { + case WXK_LEFT: + if (current_item.size() >= 1 && data->axes.size() >= 1 && current_item[0] != -1) { + current_item[0]--; + onSelectionChange(); + } + break; + case WXK_RIGHT: + if (current_item.size() >= 1 && data->axes.size() >= 1 && current_item[0] + 1 < (int)data->axes[0]->groups.size()) { + current_item[0]++; + onSelectionChange(); + } + break; + case WXK_UP: + if (current_item.size() >= 2 && data->axes.size() >= 2 && current_item[1] + 1 < (int)data->axes[1]->groups.size()) { + current_item[1]++; + onSelectionChange(); + } + break; + case WXK_DOWN: + if (current_item.size() >= 2 && data->axes.size() >= 2 && current_item[1] != -1) { + current_item[1]--; + onSelectionChange(); + } + break; + } +} + +void GraphControl::onSelectionChange() { + wxCommandEvent ev(EVENT_GRAPH_SELECT, GetId()); + ProcessEvent(ev); + Refresh(false); } bool GraphControl::hasSelection(size_t axis) const { @@ -755,4 +918,5 @@ BEGIN_EVENT_TABLE(GraphControl, wxControl) EVT_PAINT (GraphControl::onPaint) EVT_SIZE (GraphControl::onSize) EVT_LEFT_DOWN (GraphControl::onMouseDown) + EVT_CHAR (GraphControl::onChar) END_EVENT_TABLE () diff --git a/src/gui/control/graph.hpp b/src/gui/control/graph.hpp index 13488c2c..ceac2e28 100644 --- a/src/gui/control/graph.hpp +++ b/src/gui/control/graph.hpp @@ -100,6 +100,8 @@ class GraphData : public IntrusivePtrBase { /// Create a cross table for two axes void crossAxis(size_t axis1, size_t axis2, vector& out) const; + /// Create a cross table for three axes + void crossAxis(size_t axis1, size_t axis2, size_t axis3, vector& out) const; }; @@ -117,6 +119,8 @@ enum DrawLayer /** It is rendered into a sub-rectangle of the screen */ class Graph : public IntrusivePtrVirtualBase { public: + /// Determine the size of this graph viewer, return -1 if the viewer stretches + virtual RealSize determineSize(RotatedDC& dc) const { return RealSize(-1,-1); } /// Draw this graph, filling the internalRect() of the dc. virtual void draw(RotatedDC& dc, const vector& current, DrawLayer layer) const = 0; /// Find the item at the given position, the rectangle gives the screen size @@ -188,16 +192,41 @@ class ScatterGraph : public Graph2D { virtual void draw(RotatedDC& dc, const vector& current, DrawLayer layer) const; virtual bool findItem(const RealPoint& pos, const RealRect& rect, vector& out) const; virtual void setData(const GraphDataP& d); - private: + protected: UInt max_value; ///< highest value }; +/// A scatter plot with an extra dimension +class ScatterGraphPlus : public ScatterGraph { + public: + inline ScatterGraphPlus(size_t axis1, size_t axis2, size_t axis3) : ScatterGraph(axis1, axis2), axis3(axis3) {} + virtual void setData(const GraphDataP& d); + protected: + size_t axis3; + vector values3D; // axis1.size * axis2.size * axis3.size array + inline GraphAxis& axis3_data() const { return *data->axes.at(axis3); } +}; + +/// A scatter plot with a pie graph for the third dimension +class ScatterPieGraph : public ScatterGraphPlus { + public: + inline ScatterPieGraph(size_t axis1, size_t axis2, size_t axis3) : ScatterGraphPlus(axis1, axis2, axis3) {} + virtual void draw(RotatedDC& dc, const vector& current, DrawLayer layer) const; +}; + /// The legend, used for pie graphs class GraphLegend : public Graph1D { public: - inline GraphLegend(size_t axis) : Graph1D(axis) {} + inline GraphLegend(size_t axis, Alignment alignment, bool reverse = false) + : Graph1D(axis), alignment(alignment), reverse(reverse) + {} + virtual RealSize determineSize(RotatedDC& dc) const; virtual void draw(RotatedDC& dc, int current, DrawLayer layer) const; virtual int findItem(const RealPoint& pos, const RealRect& rect) const; + private: + mutable RealSize size, item_size; + Alignment alignment; + bool reverse; }; //class GraphTable { @@ -228,8 +257,10 @@ class GraphLabelAxis : public Graph1D { /// Draws an a vertical axis for counts class GraphValueAxis : public Graph1D { public: - inline GraphValueAxis(size_t axis) : Graph1D(axis) {} + inline GraphValueAxis(size_t axis, bool highlight_value) : Graph1D(axis), highlight_value(highlight_value) {} virtual void draw(RotatedDC& dc, int current, DrawLayer layer) const; + private: + bool highlight_value; }; /// A graph with margins @@ -296,6 +327,9 @@ class GraphControl : public wxControl { void onPaint(wxPaintEvent&); void onSize (wxSizeEvent&); void onMouseDown(wxMouseEvent& ev); + void onChar(wxKeyEvent& ev); + + void onSelectionChange(); }; // ----------------------------------------------------------------------------- : EOF diff --git a/src/util/real_point.hpp b/src/util/real_point.hpp index 0a410c04..4662821f 100644 --- a/src/util/real_point.hpp +++ b/src/util/real_point.hpp @@ -96,6 +96,21 @@ inline RealSize add_diagonal(const RealSize& a, const RealSize& b) { return RealSize(a.width + b.width, a.height + b.height); } +/// Piecewise minimum +inline RealSize piecewise_min(const RealSize& a, const RealSize& b) { + return RealSize( + a.width < b.width ? a.width : b.width, + a.height < b.height ? a.height : b.height + ); +} +/// Piecewise maximum +inline RealSize piecewise_max(const RealSize& a, const RealSize& b) { + return RealSize( + a.width < b.width ? b.width : a.width, + a.height < b.height ? b.height : a.height + ); +} + // ----------------------------------------------------------------------------- : Rectangle using doubles /// A rectangle (postion and size) using real (double) coordinats