Spreadsheets have a lot of different features that make manipulating data very intuitive and easy. One of the main features that helps with visualization of that data is charts. Having the ability to give the power of data visualizations to users and customers can be an invaluable resource for developers. In this blog, I will show you how to create a simple spreadsheet with the ability to add different types of charts based on the data in the spreadsheet. In this project, there are only two forms: the MainForm and the GenerateChart form. The MainForm contains the Spread component and is the main point of action for the program. The GenerateChart form has logic for handling the interface between user input and chart creation. I started off by creating the MainForm and adding the Spread component to it: The main form. The “Create Chart” button is what opens up the GenerateChart form, using the following code:
private void fpSpread1_ButtonClicked(object sender, FarPoint.Win.Spread.EditorNotifyEventArgs e)
{
if (fpSpread1.ActiveSheet.ActiveCell.Column.Index == 0 && fpSpread1.ActiveSheet.ActiveCell.Row.Index == 11)
{
GenerateChart newChart = new GenerateChart(this);
newChart.ShowDialog();
}
else if (fpSpread1.ActiveSheet.ActiveCell.Column.Index == 0 && fpSpread1.ActiveSheet.ActiveCell.Row.Index == 19)
{
if (ChartModel != null)
updateChart();
}
}
The MainForm is passed to the GenerateChart form as the parent form. Whenever the user chooses to create a new chart, a form like the following opens up: The GenerateChart form. This form allows the user to choose a chart type from a set list using the drop down menu under “Chart Type”. The user can then add a name to the chart and select the series to use for the chart. Once the information is filled in and “Create Chart” is clicked, a chart with the specified properties is created within the MainForm: The MainForm with a chart loaded into it. In my implementation, I wanted to have the chart displayed neatly within the bounds of the rows and columns that comprised the sheet, so I wrote code to calculate the location and size of the chart plot area:
public void placeChart(string chartName, ChartModel model, CellRange[] cellRanges, int seriesType, int rowStart, int rowCount, int colStart, int colCount)
{
SeriesType = seriesType;
ChartModel = model;
ChartSizeX = 0;
ChartSizeY = 0;
RowHeight = 0;
if (fpSpread1.Sheets[0].Charts.Count != 0)
fpSpread1.Sheets[0].Charts.Clear();
for (int i = colStart; i < colStart + colCount; i++)
{
ChartSizeX += (int)fpSpread1.Sheets[0].Columns[i].Width;
}
for (int i = rowStart; i < rowStart + rowCount; i++)
{
ChartSizeY += (int)fpSpread1.Sheets[0].Rows[i].Height;
}
for (int i = 0; i < rowStart; i++)
{
RowHeight += (int)fpSpread1.Sheets[0].Rows[i].Height;
}
int size = 0;
for (int i = 0; i < cellRanges.Length; i++)
{
if (cellRanges[i] != null)
size++;
}
tempRange = new CellRange[size];
int tempIndex = 0;
for (int i = 0; i < cellRanges.Length; i++)
{
if (cellRanges[i] != null)
{
tempRange[tempIndex] = cellRanges[i];
tempIndex++;
}
}
if (SeriesType == ClusteredBar)
fpSpread1.Sheets[0].AddChart(tempRange, typeof(ClusteredBarSeries), ChartSizeX, ChartSizeY, (int)fpSpread1.Sheets[0].Columns[0].Width, RowHeight);
else if (SeriesType == StackedBar)
fpSpread1.Sheets[0].AddChart(tempRange, typeof(StackedBarSeries), ChartSizeX, ChartSizeY, (int)fpSpread1.Sheets[0].Columns[0].Width, RowHeight);
else if (SeriesType == Pie)
fpSpread1.Sheets[0].AddChart(tempRange, typeof(PieSeries), ChartSizeX, ChartSizeY, (int)fpSpread1.Sheets[0].Columns[0].Width, RowHeight);
else if (SeriesType == Line)
fpSpread1.Sheets[0].AddChart(tempRange, typeof(LineSeries), ChartSizeX, ChartSizeY, (int)fpSpread1.Sheets[0].Columns[0].Width, RowHeight);
else if (SeriesType == Radar)
fpSpread1.Sheets[0].AddChart(tempRange, typeof(RadarPointSeries), ChartSizeX, ChartSizeY, (int)fpSpread1.Sheets[0].Columns[0].Width, RowHeight);
updateChart();
}
In order to provide support for different types of charts, I wrote a function for each type of series. The series methods all pretty much have the same format as the one below:
public BarSeries createBarSeries(string seriesName, int col, int rowStart, int rowEnd)
{
BarSeries series = new BarSeries();
series.SeriesName = seriesName;
for (int i = rowStart; i <= rowEnd; i++)
{
series.Values.Add(Convert.ToDouble(fpSpread1.Sheets[0].Cells[i, col].Value));
series.CategoryNames.Add(fpSpread1.Sheets[0].Cells[i, 0].Text);
}
return series;
}
A series of a specific type is created out of the range provided by the parameters. The names are added in order to have labels in the charts. Once a series has been created, a chart can be made using this function:
public ChartModel createChart(string name)
{
LabelArea label = new LabelArea();
label.Text = name;
label.Location = new PointF(0.5f, 0.02f);
label.AlignmentX = 0.5f;
label.AlignmentY = 0.0f;
LegendArea legend = new LegendArea();
legend.Location = new PointF(0.98f, 0.5f);
legend.AlignmentX = 1.0f;
legend.AlignmentY = 0.5f;
ChartModel model = new ChartModel();
model.LabelAreas.Add(label);
model.LegendAreas.Add(legend);
return model;
}
This function is used to create an initial chart with properties that are changed once cell ranges are created. This can be seen with the button click function in the GenerateChart form:
private void button1_Click(object sender, EventArgs e)
{
MainForm mainForm = (MainForm)this.parentForm;
string chartType = this.comboBox1.Text;
string chartName = this.textBox1.Text;
CellRange[] cellRanges = new CellRange[4];
if (checkBox1.Checked)
cellRanges[0] = new CellRange(3, MainForm.Price, 8, 1);
if (checkBox2.Checked)
cellRanges[1] = new CellRange(3, MainForm.Sold, 8, 1);
if (checkBox3.Checked)
cellRanges[2] = new CellRange(3, MainForm.Produced, 8, 1);
if (checkBox4.Checked)
cellRanges[3] = new CellRange(3, MainForm.Profit, 8, 1);
if (!this.comboBox1.Text.Equals(null))
{
ChartModel chart = mainForm.createChart(chartName);
if (chartType.Equals("Clustered Bar"))
{
mainForm.placeChart(chartName, chart, cellRanges, MainForm.ClusteredBar, MainForm.ChartAreaRowStart, MainForm.ChartAreaRowCount, MainForm.ChartAreaColStart, MainForm.ChartAreaColCount);
this.Close();
}
else if (chartType.Equals("Stacked Bar"))
{
mainForm.placeChart(chartName, chart, cellRanges, MainForm.StackedBar, MainForm.ChartAreaRowStart, MainForm.ChartAreaRowCount, MainForm.ChartAreaColStart, MainForm.ChartAreaColCount);
this.Close();
}
else if (chartType.Equals("Pie"))
{
mainForm.placeChart(chartName, chart, cellRanges, MainForm.Pie, MainForm.ChartAreaRowStart, MainForm.ChartAreaRowCount, MainForm.ChartAreaColStart, MainForm.ChartAreaColCount);
this.Close();
}
else if (chartType.Equals("Line"))
{
mainForm.placeChart(chartName, chart, cellRanges, MainForm.Line, MainForm.ChartAreaRowStart, MainForm.ChartAreaRowCount, MainForm.ChartAreaColStart, MainForm.ChartAreaColCount);
this.Close();
}
else if (chartType.Equals("Radar Point"))
{
mainForm.placeChart(chartName, chart, cellRanges, MainForm.Radar, MainForm.ChartAreaRowStart, MainForm.ChartAreaRowCount, MainForm.ChartAreaColStart, MainForm.ChartAreaColCount);
this.Close();
}
}
}
These particular charts can handle at most 4 series (the 4 categories in the spreadsheet). Every chart created can have less than 4 series, and the other series could be passed in as null values. Both creating a chart and updating a chart utilized the updateChart function, which gives the chart all of its labels and legend and ensure that the data is displayed correctly:
public void updateChart()
{
if (SeriesType == ClusteredBar)
{
ClusteredBarSeries bars = new ClusteredBarSeries();
for (int i = 0; i < tempRange.Length; i++)
bars.Series.Add(createBarSeries(SeriesNames[tempRange[i].Column - 1], tempRange[i].Column, 3, 11));
fpSpread1.Sheets[0].Charts[0].Model.PlotAreas[0].Series[0] = bars;
YPlotArea barChart = (YPlotArea)fpSpread1.Sheets[0].Charts[0].Model.PlotAreas[0];
barChart.Vertical = false;
}
else if (SeriesType == StackedBar)
{
StackedBarSeries bars = new StackedBarSeries();
for (int i = 0; i < tempRange.Length; i++)
bars.Series.Add(createBarSeries(SeriesNames[tempRange[i].Column - 1], tempRange[i].Column, 3, 11));
fpSpread1.Sheets[0].Charts[0].Model.PlotAreas[0].Series[0] = bars;
YPlotArea barChart = (YPlotArea)fpSpread1.Sheets[0].Charts[0].Model.PlotAreas[0];
barChart.Vertical = false;
}
else if (SeriesType == Pie)
{
for (int i = 0; i < tempRange.Length; i++)
fpSpread1.Sheets[0].Charts[0].Model.PlotAreas[0].Series[i] = createPieSeries(SeriesNames[tempRange[i].Column - 1], tempRange[i].Column, 3, 11);
}
else if (SeriesType == Line)
{
for (int i = 0; i < tempRange.Length; i++)
fpSpread1.Sheets[0].Charts[0].Model.PlotAreas[0].Series[i] = createLineSeries(SeriesNames[tempRange[i].Column - 1], tempRange[i].Column, 3, 11);
}
else if (SeriesType == Radar)
{
for (int i = 0; i < tempRange.Length; i++)
fpSpread1.Sheets[0].Charts[0].Model.PlotAreas[0].Series[i] = createRadarPointSeries(SeriesNames[tempRange[i].Column - 1], tempRange[i].Column, 3, 11);
}
fpSpread1.Sheets[0].Charts[0].Model.LabelAreas.Add(ChartModel.LabelAreas[0]);
fpSpread1.Sheets[0].Charts[0].CanMove = FarPoint.Win.Spread.DrawingSpace.Moving.None;
fpSpread1.Sheets[0].Charts[0].CanSize = FarPoint.Win.Spread.DrawingSpace.Sizing.None;
}
Now whenever a chart is created, a user can edit the values that the chart is based on and click the “Update Chart” button to have the changes reflected in the chart. In this blog, I have shown how to create a Windows Form application that utilizes Spread’s charting capabilities. The creation of charts is a simple and intuitive process that makes it easy for developers to add charts to their spreadsheet applications. Different kinds of charts can be created with very simple code that is generalized across most of the chart types. With this functionality, Spread can be a powerful tool for data visualization. The download link below contains the project files for this sample. WinForms Charting