Blake-Madden / Wisteria-Dataviz

Wisteria Dataviz Library
BSD 3-Clause "New" or "Revised" License
15 stars 3 forks source link

How do you draw the pie chart? #26

Closed asmwarrior closed 1 year ago

asmwarrior commented 1 year ago

Hi, I see this image:

https://github.com/Blake-Madden/Wisteria-Dataviz/blob/main/docs/doxygen/images/DonutChart.svg

I just want to now how do you draw the portion of a ring?

I have looked at the source code: Wisteria-Dataviz/piechart.cpp at 6670a45def9539e25f8be0e7c84763ffd202d2ce · Blake-Madden/Wisteria-Dataviz

I see it only have some GDI call like "dc.DrawEllipticArc".

I have a similar question in wx forum, see here: How to draw a portion of a ring?

So, do you use the pen which has the property set like: pen.SetCap(wxCAP_BUTT); or you use other method?

Thanks.

Blake-Madden commented 1 year ago

DrawEllipticArc draws the outer-arc line with the current pen (yes, I believe wxCAP_BUTT is recommended). It then fills from the outer ring to the pie center with the current brush. To draw the lines from the start and end points of the outer arc back to the center point, I have to use geometry::arc_vertex and manually draw those lines with the current pen.

Unfortunately, this will always give you a full pie slice (filled with the brush). To create the illusion of a ring, I draw another pie slice (with a smaller bounding box) on top of the larger slice. Making the inner slice use the same background color as the plot gives the illusion that the center of the pie chart is hollow.

I originally tried calling DrawEllipticArc for an outer ring, then again for an inner ring and then flood filling that area. (That would draw a true outer ring, like what I think you are looking for). Unfortunately, flood filling is not available on macOS, so I couldn't rely on that method.

asmwarrior commented 1 year ago

DrawEllipticArc draws the outer-arc line with the current pen (yes, I believe wxCAP_BUTT is recommended). It then fills from the outer ring to the pie center with the current brush. To draw the lines from the start and end points of the outer arc back to the center point, I have to use geometry::arc_vertex and manually draw those lines with the current pen.

Unfortunately, this will always give you a full pie slice (filled with the brush). To create the illusion of a ring, I draw another pie slice (with a smaller bounding box) on top of the larger slice. Making the inner slice use the same background color as the plot gives the illusion that the center of the pie chart is hollow.

Thanks for the explanation. I understand the method you use now.

I originally tried calling DrawEllipticArc for an outer ring, then again for an inner ring and then flood filling that area. (That would draw a true outer ring, like what I think you are looking for). Unfortunately, flood filling is not available on macOS, so I couldn't rely on that method.

In-fact, I haven't used wxDC::FloodFill before. From the document wxWidgets: wxDC Class Reference, it said:

The present implementation for non-Windows platforms may fail to find colour borders if the pixels do not match the colour exactly. However the function will still return true. This method shouldn't be used with wxPaintDC under non-Windows platforms as it uses GetPixel() internally and this may give wrong results, notably in wxGTK. If you need to flood fill wxPaintDC, create a temporary wxMemoryDC, flood fill it and then blit it to, or draw as a bitmap on, wxPaintDC. See the example of doing this in the drawing sample and wxBufferedPaintDC class.

So, it may not work under non-Windows OS.

In my post in wxWidgets forum, some one suggest I should use wxGraphicsPath way, and that maybe another alternative.

Blake-Madden commented 1 year ago

You can fill a wxGraphicsPath, although you have to specify the points in the pie slice polygon to do that. So, you won't be able to use DrawEllipticArc; you would need to provide a Bezier curve in your polygon instead. In other words, use geometry::arc_vertex to get the corners of a slice and use Bezier curves between those to create the wxGraphicsPath. Seems like a lot of work and not sure how precise the curve would look, but that could be a solution.

You could just draw with a really, really thick pen with DrawEllipticArc; you won't have an fill capabilities, but if you just want a solid-colored ring that could work too.

asmwarrior commented 1 year ago

You can fill a wxGraphicsPath, although you have to specify the points in the pie slice polygon to do that. So, you won't be able to use DrawEllipticArc; you would need to provide a Bezier curve in your polygon instead. In other words, use geometry::arc_vertex to get the corners of a slice and use Bezier curves between those to create the wxGraphicsPath. Seems like a lot of work and not sure how precise the curve would look, but that could be a solution.

I haven't used Bezier curve before, it looks like a complex task. But the below code works, it use some function calls like "path.AddArc" and "path.AddLineToPoint".

#include <wx/wx.h>
#include <wx/graphics.h>
#include <math.h>

class MyFrame : public wxFrame
{
public:
    MyFrame()
        : wxFrame(NULL, wxID_ANY, wxT("Annular Sector"), wxDefaultPosition, wxSize(640, 480))
    {
        Connect(wxEVT_PAINT, wxPaintEventHandler(MyFrame::OnPaint));
    }

private:
    void OnPaint(wxPaintEvent& event)
    {
        wxPaintDC pdc(this);
        wxGraphicsContext* gc = wxGraphicsContext::Create(pdc);

        if (gc)
        {
            gc->SetPen(wxPen(wxColor(255, 0, 0), 2));       // set the pen color and width
            gc->SetBrush(wxBrush(wxColor(255, 255, 0)));   // set the brush color

            // define the outer and inner radii of the annular sector
            const double r1 = 100.0;
            const double r2 = 60.0;

            // define the start and end angles of the annular sector, in radians
            const double startAngle = M_PI / 3.0;
            const double endAngle = 2.0 * M_PI / 3.0;

            wxPoint center;
            center.x = GetClientSize().GetWidth() / 2;
            center.y = GetClientSize().GetHeight() / 2;

            // calculate the coordinate of each endpoint of the annular sector
            wxPoint pt1, pt2, pt3, pt4;
            pt1.x = static_cast<int>(center.x + r1 * cos(startAngle));
            pt1.y = static_cast<int>(center.y - r1 * sin(startAngle));
            pt2.x = static_cast<int>(center.x + r1 * cos(endAngle));
            pt2.y = static_cast<int>(center.y - r1 * sin(endAngle));
            pt3.x = static_cast<int>(center.x + r2 * cos(endAngle));
            pt3.y = static_cast<int>(center.y - r2 * sin(endAngle));
            pt4.x = static_cast<int>(center.x + r2 * cos(startAngle));
            pt4.y = static_cast<int>(center.y - r2 * sin(startAngle));

            // create a graphics path object from the coordinate of each endpoint and draw it
            wxGraphicsPath path = gc->CreatePath();
            path.MoveToPoint(pt1.x, pt1.y);

            //path.AddLineToPoint(pt2.x, pt2.y);
            //path.AddLineToPoint(pt3.x, pt3.y);
            //path.AddLineToPoint(pt4.x, pt4.y);
            //path.AddLineToPoint(pt1.x, pt1.y);

            path.AddArc(center.x, center.y, static_cast<float>(r1), 2*M_PI - static_cast<float>(startAngle), 2*M_PI - static_cast<float>(endAngle), false);
            path.AddLineToPoint(pt3.x, pt3.y);
            path.AddArc(center.x, center.y, static_cast<float>(r2), 2*M_PI - static_cast<float>(endAngle), 2*M_PI - static_cast<float>(startAngle), true);
            path.AddLineToPoint(pt1.x, pt1.y);

            path.CloseSubpath();
            gc->FillPath(path);

            delete gc;
        }
    }
};

class MyApp : public wxApp
{
public:
    virtual bool OnInit()
    {
        MyFrame* frame = new MyFrame();
        frame->Show();
        return true;
    }
};

IMPLEMENT_APP(MyApp);

I have post the above code and screen shot in the forum: How to draw a portion of a ring? - wxWidgets Discussion Forum. I'm not sure such code works under macOS.

Blake-Madden commented 1 year ago

Nice. It should be fine on all platforms. As far as I know, floodfill is the only drawing feature not available on macOS.

P.S. Bezier curves requite a lot of...trial and error, shall we say. If you're curious, I use them in the "shapes.cpp" file, where I draw male and female outlines.