Skip to main content

Charts

The charting engine draws eight kinds of business chart: columns, bars, lines, areas and pies, with axes, gridlines, legends and data labels. You can reach it two ways, and both end in the same renderers:

  • Draw a chart onto a page. Build a PdfPinata.Charting.Chart, put it in a ChartFrame, and draw the frame with an XGraphics. You choose the rectangle. This needs the PdfPinata.Charting package.
  • Add a chart to a document. Call Section.AddChart on a PinataLayout document. The renderer places the chart in the flow, between paragraphs, and moves it to the next page if it does not fit. This needs PinataLayout.Rendering, which brings the charting package with it.

Charts draw text, so either route needs a font resolver. See Installation.

The eight chart types

ChartTypeWhat it draws
Column2DVertical bars, one group per category, the series side by side.
ColumnStacked2DVertical bars with the series stacked into one bar per category.
Bar2DThe same as Column2D, turned on its side.
BarStacked2DThe same as ColumnStacked2D, turned on its side.
LineA line per series, with optional markers at each point.
Area2DA filled area per series. A later series is drawn in front of an earlier one.
Pie2DOne series as slices of a circle.
PieExploded2DA pie with the slices pulled apart.

Both routes use the same names. The drawn route's types are in the PdfPinata.Charting namespace; the document route's are in PinataLayout.DocumentObjectModel.Shapes.Charts. The two namespaces have classes with the same names (Chart, Series, ChartType), so if you use both in one file, give one of them an alias. The demo uses using Charting = PdfPinata.Charting;.

Draw a chart on a page

A chart has no size of its own. A ChartFrame gives it one: set the frame's Location and Size, add the chart, and call DrawChart.

src/SampleApp/Demos/ChartsDemo.cs
// A chart carries no size of its own. ChartFrame is what gives it one: set the frame's
// Location and Size, add the chart, and DrawChart lays it out inside that rectangle.
// Draw - the other method - decorates the rectangle with a rounded border, a gradient and
// a drop shadow first, which page 3 shows.
void Place(XGraphics gfx, Charting.Chart chart, XRect rect, string label)
{
var frame = new Charting.ChartFrame
{
Location = new XPoint(rect.X, rect.Y),
Size = new XSize(rect.Width, rect.Height)
};
frame.Add(chart);
frame.DrawChart(gfx);

gfx.DrawString(label, caption, XBrushes.DimGray,
new XRect(rect.X, rect.Bottom + 2, rect.Width, 12), XStringFormats.TopCenter);
}

Build a chart from series

A chart holds two kinds of data:

  • An X series holds the category labels. chart.XValues.AddXSeries() makes one, and Add takes the labels.
  • A series holds the values for one thing being measured. chart.SeriesCollection.AddSeries() makes one, Add takes the numbers, and Name is what the legend shows.
src/SampleApp/Demos/ChartsDemo.cs
// Every chart on pages 1 and 2 is built the same way, so the shape of the API is visible
// once rather than four times: a chart of some type, an X series of labels, and a value
// series per region.
Charting.Chart Regional(Charting.ChartType type, params string[] series)
{
var chart = new Charting.Chart(type)
{
Font =
{
Name = "Liberation Sans",
Size = 7
}
};

var labels = chart.XValues.AddXSeries();
labels.Add(quarters);

foreach (var name in series)
{
var values = chart.SeriesCollection.AddSeries();
values.Name = name;
values.Add(name switch { "North" => north, "South" => south, _ => west });
}

chart.Legend.Docking = Charting.DockingType.Bottom;
chart.XAxis.MajorTickMark = Charting.TickMarkType.Outside;
chart.YAxis.MajorTickMark = Charting.TickMarkType.Outside;
chart.YAxis.HasMajorGridlines = true;
return chart;
}

chart.Font sets the family and size for every piece of text in the chart. chart.Legend.Docking puts the legend at the Top, Bottom, Left or Right of the chart.

Axes and scales

An axis you leave alone scales itself to the data. Set MinimumScale, MaximumScale and MajorTick to fix the scale. A fixed scale lets you compare two charts drawn from different figures, and it stops a chart from changing its scale each time the numbers change.

src/SampleApp/Demos/ChartsDemo.cs
var line = Regional(Charting.ChartType.Line, "North", "South", "West");
foreach (var index in new[] { 0, 1, 2 })
{
line.SeriesCollection[index].MarkerStyle = Charting.MarkerStyle.Circle;
line.SeriesCollection[index].MarkerSize = 4;
}

// An axis left alone scales itself to the data. Fixing the scale is how two charts drawn
// from different figures become comparable - and how a chart stops rescaling itself every
// time the numbers move.
line.YAxis.MinimumScale = 0;
line.YAxis.MaximumScale = 80;
line.YAxis.MajorTick = 20;
Place(gfx2, line, new XRect(50, 90, 235, 210), "Line - markers and a fixed scale");

Each axis also has HasMajorGridlines and HasMinorGridlines, MajorTickMark and MinorTickMark, TickLabels.Format for the format of its numbers, and Title for a caption beside it (chart.YAxis.Title.Caption). For a line chart, MarkerStyle and MarkerSize on a series put a marker at each point.

Pies and data labels

A pie shows one series. The X series labels the slices instead of an axis.

src/SampleApp/Demos/ChartsDemo.cs
// A pie shows one series, so the X series labels the slices rather than an axis.
Charting.Chart Pie(Charting.ChartType type, Charting.DockingType docking)
{
var chart = new Charting.Chart(type)
{
Font =
{
Name = "Liberation Sans",
Size = 7
}
};
chart.XValues.AddXSeries().Add(quarters);
chart.SeriesCollection.AddSeries().Add(north);
chart.Legend.Docking = docking;

// A pie's natural label is the share rather than the number, which is what Percent
// means here; Value would print 42, 58, 51, 73 again.
chart.HasDataLabel = true;
chart.DataLabel.Type = Charting.DataLabelType.Percent;
chart.DataLabel.Position = Charting.DataLabelPosition.InsideEnd;
chart.DataLabel.Format = "0%";
return chart;
}

Set HasDataLabel to true to write a label on each point. DataLabel.Type chooses what the label says: Value for the number, Percent for its share of the total, or None. DataLabel.Position takes Center, InsideBase, InsideEnd or OutsideEnd, and DataLabel.Format is a .NET number format. You can set the same properties on one series instead of on the whole chart.

Combination charts

Each series has its own ChartType. When a series has a different type from its chart, the chart is drawn as a combination. That is the whole API: set ChartType on the series that must be different.

src/SampleApp/Demos/ChartsDemo.cs
// A series carries its own ChartType, and a chart whose series disagree with it is drawn by
// CombinationChartRenderer instead. That is the whole of the combination API: set the
// property on the series that should be different.
var combination = Regional(Charting.ChartType.Column2D, "North", "South", "West");
combination.SeriesCollection[2].ChartType = Charting.ChartType.Line;
combination.SeriesCollection[2].MarkerStyle = Charting.MarkerStyle.Diamond;
combination.SeriesCollection[2].MarkerSize = 5;
Place(gfx2, combination, new XRect(50, 350, 495, 210),
"Column2D with one series set to Line - a combination chart");

Leave a gap in a series

Series.AddBlank() adds a point with no value. Use it when a figure is missing, rather than inventing a zero. XSeries.AddBlank() does the same for a category label.

Charting.Series sales = chart.SeriesCollection.AddSeries();
sales.Add(42);
sales.AddBlank(); // no figure for Q2
sales.Add(51);

A column, bar or pie chart draws nothing for a blank point, and the blank does not count towards the axis scale. A line or area chart draws a blank point as zero.

Draw a chart with a frame

ChartFrame has a second method, Draw. It paints a rounded border, a vertical gradient and a drop shadow, then lays the chart out inside them. It also draws every chart the frame holds, where DrawChart draws only the first.

src/SampleApp/Demos/ChartsDemo.cs
// The other draw method. Draw() paints a rounded border, a vertical gradient and a drop
// shadow of its own before laying the chart out inside them, and it draws every chart the
// frame holds rather than only the first. Worth knowing which one you called: a chart that
// arrives with a border nobody asked for arrived through here.
var framed = new Charting.ChartFrame
{
Location = new XPoint(50, 350),
Size = new XSize(495, 230)
};
framed.Add(Regional(Charting.ChartType.Column2D, "North", "South"));
framed.Draw(gfx3);

If a chart arrives with a border you did not ask for, check which of the two methods you called.

Charts in a PinataLayout document

Section.AddChart adds a chart to the document like a paragraph or a table. Give it a Width and Height; the renderer decides where it goes.

src/SampleApp/Demos/ChartsDemo.cs
var flowed = section.AddChart(ChartType.Column2D);
flowed.Width = Unit.FromPoint(440);
flowed.Height = Unit.FromPoint(220);
// The DOM's chart takes its type from a paragraph format rather than from a Font of its
// own, because everything in a PinataLayout document is formatted the same way.
flowed.Format.Font.Name = "Liberation Sans";
flowed.Format.Font.Size = 8;

flowed.XValues.AddXSeries().Add(quarters);
var domNorth = flowed.SeriesCollection.AddSeries();
domNorth.Name = "North";
domNorth.Add(north);
var domSouth = flowed.SeriesCollection.AddSeries();
domSouth.Name = "South";
domSouth.Add(south);

// The DOM's chart has six text areas around the plot - header, footer, left, right, top,
// bottom - and the legend is added to one of them rather than docked to a side.
flowed.HeaderArea.AddParagraph("Sales by region").Format.Font.Bold = true;
flowed.BottomArea.AddLegend();
flowed.YAxis.HasMajorGridlines = true;
flowed.XAxis.MajorTickMark = TickMarkType.Outside;

The document chart differs from the drawn one in two ways:

  • Text is formatted through Format, like everything else in a PinataLayout document, not through a Font of its own.
  • The legend goes in a text area. The chart has six areas around the plot: HeaderArea, FooterArea, TopArea, BottomArea, LeftArea and RightArea. You add paragraphs to them for titles and notes, and AddLegend() puts the legend in one.

A document chart is a shape, so it can also stand beside the text, with the text running down one side of it. See Columns, drop caps and wrapping.

Choose the document route when the chart belongs to a report and must move with the text around it. Choose the drawn route when you are placing everything on the page yourself.

Things to know

  • An unnamed font is Arial. A drawn chart whose Font.Name you do not set asks the font resolver for Arial. If your resolver cannot supply it, set chart.Font.Name to a family it can.
  • A frame too small for its axes draws no plot. When the axes and labels take up all the room in the frame, the plot area is left empty. Nothing is thrown. Make the frame bigger or the font smaller.
  • An empty chart draws its axes. A chart with no series, or with series that hold no points, draws its axes and nothing inside them. A line series with only one point draws no line.
  • Chart.DisplayBlanksAs has no effect. You can set it, but no renderer reads it. Blank points behave as described above whatever it says.
  • An axis title cannot move across its axis. AxisTitle.Alignment and VerticalAlignment move a title along its axis. The strip kept for the title is only as big as the title, so it has nowhere to move across it.
  • Only the document route breaks across pages. A drawn chart goes exactly where you put it, whether or not there is room.

See it in action

The Charts demo draws all eight types from one set of figures, a combination chart, two pies with percentage labels and a framed chart. Its last page draws the same figures again through PinataLayout.

The full Charts demo
src/SampleApp/Demos/ChartsDemo.cs
var document = new PdfDocument();
document.Info.Title = "Charts";

// One set of figures for the whole demo, so that what changes from chart to chart is the
// chart type rather than the data.
string[] quarters = { "Q1", "Q2", "Q3", "Q4" };
double[] north = { 42, 58, 51, 73 };
double[] south = { 31, 29, 44, 38 };
double[] west = { 18, 26, 33, 47 };

var heading = new XFont("Liberation Sans", 16, XFontStyle.Bold);
var caption = new XFont("Liberation Sans", 8);

// A chart carries no size of its own. ChartFrame is what gives it one: set the frame's
// Location and Size, add the chart, and DrawChart lays it out inside that rectangle.
// Draw - the other method - decorates the rectangle with a rounded border, a gradient and
// a drop shadow first, which page 3 shows.
void Place(XGraphics gfx, Charting.Chart chart, XRect rect, string label)
{
var frame = new Charting.ChartFrame
{
Location = new XPoint(rect.X, rect.Y),
Size = new XSize(rect.Width, rect.Height)
};
frame.Add(chart);
frame.DrawChart(gfx);

gfx.DrawString(label, caption, XBrushes.DimGray,
new XRect(rect.X, rect.Bottom + 2, rect.Width, 12), XStringFormats.TopCenter);
}

// Every chart on pages 1 and 2 is built the same way, so the shape of the API is visible
// once rather than four times: a chart of some type, an X series of labels, and a value
// series per region.
Charting.Chart Regional(Charting.ChartType type, params string[] series)
{
var chart = new Charting.Chart(type)
{
Font =
{
Name = "Liberation Sans",
Size = 7
}
};

var labels = chart.XValues.AddXSeries();
labels.Add(quarters);

foreach (var name in series)
{
var values = chart.SeriesCollection.AddSeries();
values.Name = name;
values.Add(name switch { "North" => north, "South" => south, _ => west });
}

chart.Legend.Docking = Charting.DockingType.Bottom;
chart.XAxis.MajorTickMark = Charting.TickMarkType.Outside;
chart.YAxis.MajorTickMark = Charting.TickMarkType.Outside;
chart.YAxis.HasMajorGridlines = true;
return chart;
}

// ----- page 1: the column and bar family -----

var page1 = document.AddPage();
var gfx1 = XGraphics.FromPdfPage(page1);
gfx1.DrawString("Columns and bars", heading, XBrushes.Black, new XPoint(50, 60));

// Clustered puts the regions side by side and compares them; stacked puts them on top of
// one another and compares the totals. Same numbers, different question.
Place(gfx1, Regional(Charting.ChartType.Column2D, "North", "South", "West"),
new XRect(50, 90, 235, 210), "Column2D - clustered");
Place(gfx1, Regional(Charting.ChartType.ColumnStacked2D, "North", "South", "West"),
new XRect(310, 90, 235, 210), "ColumnStacked2D - one bar per quarter");
Place(gfx1, Regional(Charting.ChartType.Bar2D, "North", "South", "West"),
new XRect(50, 350, 235, 210), "Bar2D - the same chart on its side");
Place(gfx1, Regional(Charting.ChartType.BarStacked2D, "North", "South", "West"),
new XRect(310, 350, 235, 210), "BarStacked2D");

// ----- page 2: lines, areas, and one chart of two kinds -----

var page2 = document.AddPage();
var gfx2 = XGraphics.FromPdfPage(page2);
gfx2.DrawString("Lines, areas and combinations", heading, XBrushes.Black, new XPoint(50, 60));

var line = Regional(Charting.ChartType.Line, "North", "South", "West");
foreach (var index in new[] { 0, 1, 2 })
{
line.SeriesCollection[index].MarkerStyle = Charting.MarkerStyle.Circle;
line.SeriesCollection[index].MarkerSize = 4;
}

// An axis left alone scales itself to the data. Fixing the scale is how two charts drawn
// from different figures become comparable - and how a chart stops rescaling itself every
// time the numbers move.
line.YAxis.MinimumScale = 0;
line.YAxis.MaximumScale = 80;
line.YAxis.MajorTick = 20;
Place(gfx2, line, new XRect(50, 90, 235, 210), "Line - markers and a fixed scale");

Place(gfx2, Regional(Charting.ChartType.Area2D, "North", "South"),
new XRect(310, 90, 235, 210), "Area2D - two series, the later in front");

// A series carries its own ChartType, and a chart whose series disagree with it is drawn by
// CombinationChartRenderer instead. That is the whole of the combination API: set the
// property on the series that should be different.
var combination = Regional(Charting.ChartType.Column2D, "North", "South", "West");
combination.SeriesCollection[2].ChartType = Charting.ChartType.Line;
combination.SeriesCollection[2].MarkerStyle = Charting.MarkerStyle.Diamond;
combination.SeriesCollection[2].MarkerSize = 5;
Place(gfx2, combination, new XRect(50, 350, 495, 210),
"Column2D with one series set to Line - a combination chart");

// ----- page 3: pies, labels and the framed drawing -----

var page3 = document.AddPage();
var gfx3 = XGraphics.FromPdfPage(page3);
gfx3.DrawString("Pies, labels and frames", heading, XBrushes.Black, new XPoint(50, 60));

// A pie shows one series, so the X series labels the slices rather than an axis.
Charting.Chart Pie(Charting.ChartType type, Charting.DockingType docking)
{
var chart = new Charting.Chart(type)
{
Font =
{
Name = "Liberation Sans",
Size = 7
}
};
chart.XValues.AddXSeries().Add(quarters);
chart.SeriesCollection.AddSeries().Add(north);
chart.Legend.Docking = docking;

// A pie's natural label is the share rather than the number, which is what Percent
// means here; Value would print 42, 58, 51, 73 again.
chart.HasDataLabel = true;
chart.DataLabel.Type = Charting.DataLabelType.Percent;
chart.DataLabel.Position = Charting.DataLabelPosition.InsideEnd;
chart.DataLabel.Format = "0%";
return chart;
}

Place(gfx3, Pie(Charting.ChartType.Pie2D, Charting.DockingType.Right),
new XRect(50, 90, 235, 200), "Pie2D - legend docked Right");
Place(gfx3, Pie(Charting.ChartType.PieExploded2D, Charting.DockingType.Left),
new XRect(310, 90, 235, 200), "PieExploded2D - legend docked Left");

// The other draw method. Draw() paints a rounded border, a vertical gradient and a drop
// shadow of its own before laying the chart out inside them, and it draws every chart the
// frame holds rather than only the first. Worth knowing which one you called: a chart that
// arrives with a border nobody asked for arrived through here.
var framed = new Charting.ChartFrame
{
Location = new XPoint(50, 350),
Size = new XSize(495, 230)
};
framed.Add(Regional(Charting.ChartType.Column2D, "North", "South"));
framed.Draw(gfx3);
gfx3.DrawString("ChartFrame.Draw - the frame is the frame's, not the chart's",
caption, XBrushes.DimGray, new XRect(50, 584, 495, 12), XStringFormats.TopCenter);

// ----- page 4: the same engine, reached through PinataLayout -----

// PinataLayout holds a chart of its own in the document object model and maps it onto the
// charting engine above at render time - PinataLayout.Rendering.ChartMapper does the copying.
// The difference is not the picture, it is who decides where the chart goes: here the
// renderer places it in the flow, where above the caller passed a rectangle.
var report = new Document();
report.Styles[StyleNames.Normal].Font.Name = "Liberation Sans";
var section = report.AddSection();
section.PageSetup.LeftMargin = Unit.FromPoint(50);
section.PageSetup.RightMargin = Unit.FromPoint(50);
section.PageSetup.TopMargin = Unit.FromPoint(50);

var title = section.AddParagraph("The same figures through PinataLayout");
title.Format.Font.Size = 16;
title.Format.Font.Bold = true;
title.Format.SpaceAfter = Unit.FromPoint(12);

section.AddParagraph(
"A chart added to a section is an element of the document like a paragraph or a table. "
+ "It is laid out in the flow, it moves when the text above it moves, and it breaks to "
+ "the next page if it does not fit - none of which the drawn route does for you.")
.Format.SpaceAfter = Unit.FromPoint(10);

var flowed = section.AddChart(ChartType.Column2D);
flowed.Width = Unit.FromPoint(440);
flowed.Height = Unit.FromPoint(220);
// The DOM's chart takes its type from a paragraph format rather than from a Font of its
// own, because everything in a PinataLayout document is formatted the same way.
flowed.Format.Font.Name = "Liberation Sans";
flowed.Format.Font.Size = 8;

flowed.XValues.AddXSeries().Add(quarters);
var domNorth = flowed.SeriesCollection.AddSeries();
domNorth.Name = "North";
domNorth.Add(north);
var domSouth = flowed.SeriesCollection.AddSeries();
domSouth.Name = "South";
domSouth.Add(south);

// The DOM's chart has six text areas around the plot - header, footer, left, right, top,
// bottom - and the legend is added to one of them rather than docked to a side.
flowed.HeaderArea.AddParagraph("Sales by region").Format.Font.Bold = true;
flowed.BottomArea.AddLegend();
flowed.YAxis.HasMajorGridlines = true;
flowed.XAxis.MajorTickMark = TickMarkType.Outside;

section.AddParagraph(
"The two routes reach the same renderers. Reach for this one when the chart belongs to "
+ "a document, and for the drawn one when it belongs to a page you are laying out "
+ "yourself.").Format.SpaceBefore = Unit.FromPoint(10);

var renderer = new PdfDocumentRenderer(unicode: true) { Document = report };
renderer.RenderDocument();

// Saved and reopened rather than imported from the live document: a document being written
// and a document being read are different things to PdfPinata, and Import is the mode that
// permits taking pages out of one.
using (var buffer = new MemoryStream())
{
renderer.PdfDocument.Save(buffer, false);
buffer.Position = 0;

using var laidOut = PdfReader.Open(buffer, PdfDocumentOpenMode.Import);
foreach (var rendered in laidOut.Pages)
_ = document.AddPage(rendered);
}