Title: Make a sunburst chart in C#, Part 3
The post Make a sunburst chart in C#, Part 1 shows how to build a basic sunburst chart. The post Make a sunburst chart in C#, Part 2 enhances that example by drawing each node's text at an angle so it fits within its wedge better.
This example draws node text along an arc so it fits its wedge better still.
The key is the following DrawTextOnArc method.
// Draw text along this arc.
private void DrawTextOnArc(Graphics gr, Brush brush,
Font font, string text, float cx, float cy,
double min_theta, double max_theta, float radius)
{
// Use at most 32 characters.
if (text.Length > 32) text = text.Substring(0, 32);
// Make a CharacterRange for the string's characters.
List<CharacterRange> range_list = new List<CharacterRange>();
for (int i = 0; i < text.Length; i++)
{
range_list.Add(new CharacterRange(i, 1));
}
using (StringFormat sf = new StringFormat())
{
sf.SetMeasurableCharacterRanges(range_list.ToArray());
// Measure the string's character ranges.
RectangleF rect = new RectangleF(0, 0, 1000, 1000);
Region[] regions =
gr.MeasureCharacterRanges(text, font, rect, sf);
// Get the total width.
float total_width = regions.Sum(
region => region.GetBounds(gr).Width);
// Get the angle subtended by the text.
double total_theta = total_width / radius;
// Find the starting angle.
double start_theta = (min_theta + max_theta - total_theta) / 2;
// Find the angle per unit of character width.
double theta_per_width = total_theta / total_width;
// Draw the characters.
for (int i = 0; i < regions.Length; i++)
{
float x = (float)(cx + radius * Math.Cos(start_theta));
float y = (float)(cy + radius * Math.Sin(start_theta));
float angle = (float)(start_theta / Math.PI * 180 + 90);
PointF point = new PointF(x, y);
DrawCenteredText(gr, font, brush,
text[i].ToString(), point, angle);
start_theta +=
theta_per_width * regions[i].GetBounds(gr).Width;
}
}
}
This method uses the Graphics class's MeasureCharacterRanges method to find the exact dimensions of each of the characters in the text that it will draw. The MeasureCharacterRanges method can only work for up to 32 characters, so the method starts by truncating the text to 32 characters if necessary. (If the text exceeds that length, the MeasureCharacterRanges method throws a non-descriptive "Overflow error" exception.)
To use MeasureCharacterRanges, the code makes a list of CharacterRange and adds CharacterRange structures to it. Each CharacterRange gives the starting position and length of a piece of the text that should be measured. That lets you measure pieces of the text that have different sizes. In this example, we want to measure the characters individually.
Next the code creates a StringFormat and calls its SetMeasurableCharacterRanges method to set the StringFormat object's character ranges.
The code then creates a rectangle to hold the drawn text. If the rectangle were smaller, the StringFormat would use it to wrap the text. In this example we just want the sizes of the characters so the rectangle is large enough to prevent wrapping.
After all this set up, the code finally calls the Graphics object's MeasureCharacterRanges method to measure the characters.
The code now uses a LINQ statement to add up the widths of the characters' regions. It then uses the radius where it should draw the text to calculate the angle subtended by the text. It uses that angle together with the minimum and maximum angles of the text's wedge to calculate where the first character should be drawn.
The code also calculates the number of radians per unit of character width so it can easily update the angle where the next character should be drawn after it draws each character.
Finally the code draws the characters. For each of the character regions, the code calculates the point where the text should be drawn and the angle it should have. It then uses the DrawCenteredText method described in the preceding post to draw the character at the desired location with the necessary rotation. The code then updates the value start_theta so it can draw the next character in the correct position.
There are plenty of enhancements you could make to the basic program. For example, I tried experimenting with colors that blend between parent and child nodes. It looked okay if the colors were related (blending between dark and light green), but looked terrible with unrelated colors (blending between red and blue).
It might also be nice to automatically size the font. Rotated text looks fuzzy or pixelated when the font is complicated and the characters are small. This example uses 10-point Arial, which is about the smallest font that looks nice. You could make the program adjust the font size to fit the available wedges.
Download the example to experiment with it and to see additional details.
|