2016-09-24
At this point, you have a basic chart up and running because
you know how to configure Web.Config to support the Chart
object, and because you have configured a basic column chart
object.
I'd like to continue by showing you a more advanced
configuration, and show you the code for a column chart
with a few moving parts.
In this case, I'm using a column chart that allows
different data to be "overlayed" atop the chart using a
filter control.
CODE FORWARD
I like to keep the code forward as plain as possible,
preferring to handle the heavier configuration in the
compiled codebehind.
The basic chart object has two elements with the control
tag -- series and chart area. Each of these elements
contains a web control (asp:Series and asp:ChartArea).
This chart also adds asp:Title controls.
I'm also including an asp:Panel control, which contains
a dropdownlist control for filtration, and an asp:Label
control or two.
Finally, a label at bottom to communicate status back
to the user in case something goes south (you'll see
the code for this in Catch blocks).
Notice that the Series web control points to the ID of the
ChartArea control.
DATA SOURCE
My datasource for this is an XML doc, just like was used in
the basic column chart.
10/20/2015
273.5
10/20/2015 - 273.5lbs
Doctor's office
. . .
The two main columns we'll use are the date and weight
columns. The location, scale and medication columns contain
data we'll present based on a filter control outside of the
chart. We'll show the comment column as a tooltip at each
data point.
One of the ways the data is bound to the control actually
requires a group by column. I couldn't find any way
around this (the method wouldn't acccept null or empty
string as a value) for this binding method, so I figured the
next best thing I could do here was to simply add an empty
column to my data. I'm sure there are better workarounds out
there; I guess I'm lucky that I have the freedom to use the
option I did -- not everyone will.
We'll talk about this more when we get to data binding.
CODE BEHIND
(1) Load the Dataset
I didn't make any subatantial changes to how the dataset is
loaded compared with the basic chart:
Private Function LoadDataSet() As DataSet
Dim ds As New DataSet
Dim nt As New NameTable()
Dim objWeight As Object = nt.Add("weight")
Dim objDate As Object = nt.Add("date")
Dim objComment As Object = nt.Add("comment")
Dim objLocation As Object = nt.Add("location")
Dim objScale As Object = nt.Add("scale")
Dim objMedication As Object = nt.Add("medication")
Dim objGroupBy As Object = nt.Add("groupby") 'the databindcrosstable method requires a group by column
Dim settings As New XmlReaderSettings()
settings.NameTable = nt
'create a reader object and point it to the xml doc
Dim rdr As XmlReader = XmlReader.Create(Server.MapPath("~/xml/fitness_graph.xml"), settings)
'disable parsing DTD's -- reduce attack surface vs. XML bombing
'Call ProhibitXmlDtd(rdr)
Try
'import the reader's data into the dataset
ds.ReadXml(rdr, XmlReadMode.InferSchema)
Catch ex As Exception
'build the status message, hide the chart and panel.
lblStatus.Text = ex.Message
lblStatus.ForeColor = Color.Red
lblStatus.Visible = True
Chart1.Visible = False
pnlLegend.Visible = False
Finally
'close the reader
If Not rdr.ReadState = ReadState.Closed Then
rdr.Close()
End If
End Try
Return ds
End Function
In my Catch block, I capture the message and display it in a
Label control called lblStatus, and hide the chart control and
an additional Panel control, in which I'm manufacturing a
legend for the chart. More on this in a bit.
(2) Yes, I'm Still Harping about XML Bombing
An XML bomb is an XML document crafted in such a way that it
could overload the parser on the server by causing it to
create an "exception"-ally large object, generally through
recursion (see what I did there?). This amounts to a denial-
of-service attack on your Web server.
in the settings object, be sure you set that DtdProcessing
property to ignore.
(3) Configure the Chart
Access the chart object by its ID in the codebehind and
set these values:
'create a dataview from the dataset
Dim dvDataView As New DataView(DsDataSet.Tables(0))
'create a sort order
Const psoSortOrder As PointSortOrder = PointSortOrder.Ascending
'set the minimum Y value where it crosses the X axis
Const dblYAxisMinValue As Double = 230
'set the maximum Y value at the top of the chart.
Const dblYAxisMaxValue As Double = 280
With Chart1
'basic dimensions
.Height = 350
.Width = 600
'configure the chart axis so Y-axis starts at 220 lbs
With .ChartAreas("ChartArea1")
.AxisY.Crossing = dblYAxisMinValue
.AxisY.Minimum = dblYAxisMinValue
.AxisY.Maximum = dblYAxisMaxValue
End With
'borders
.BorderlineDashStyle = ChartDashStyle.Solid
.BorderlineColor = Color.DarkGray
.BorderlineWidth = 2
'color
.Palette = ChartColorPalette.EarthTones
.ForeColor = Color.DarkGray
With .Series.Item("Series1")
'values
.XValueType = ChartValueType.Date
.YValueType = ChartValueType.Int32
.YValueMembers = "weight"
.XValueMember = "date"
.XAxisType = AxisType.Primary
'chart type
.ChartType = SeriesChartType.Column
'sort
.Sort(psoSortOrder)
End With
'image goop. The handler is in Web.Config.
.ImageStorageMode = ImageStorageMode.UseHttpHandler
.ImageType = ChartImageType.Png
'databinding
.Series("Series1").Points.DataBind(dvDataView, "date", "weight", "Date=date,Weight=weight,Comment=comment, Location=location,Scale=scale,Medication=medication")
'.DataBindCrossTable(dvDataView, "groupby", "date", "weight", "Tooltip=comment", psoSortOrder)
.Visible = True
End With
Yes, you CAN nest With...End statements. Who knew?
I took care of a couple of housekeeping items before pressing
forward with configuring the chart. First, I created a sort
order object. This gets consumed later when setting the sort
for the series, and in the DataBindCrossTable method of data
binding. Next, I set a pair of constants for the Y-values
both at the X-axis (the minimum value) and the top of the chart
(the maximum value). This value range is auto-set by default,
and the minimum value is set to 0. These constants will be
used to set a more readable range.
Now on to the chart properties. The first two properties I
assign are basic image size properties. Followed by decorations
(color, border and so on.) Next, I handle all of the series
configurations -- setting the data types and data members for
the axes. Next come a couple of image storage properties --
one directing use of the handler in our Web.Config, and the
next specifying what type of image to generate.
Once all of these other settings are made, I specify the data
source and bind the chart object to it. I'll talk a little
about those data binding methods in just a moment. Lastly, I
make sure the completed chart is visible. We wouldn't want all
this hard work to go to waste. :-)
(4) Data Binding
I have two methods included in the example above, and wanted to
explain a bit about each.
The biggest "pro" I have for using DataBindCrossTable() method
is that it gives you an easy way to assign data columns to
series properties. Consider this statement:
DataBindCrossTable(dvDataView, "groupby", "date", "weight", "Tooltip=comment", psoSortOrder)
That fourth argument in the method is assigning the row data
in the comment column as the value for tooltip messages in
the data points.
The biggest "con" I have for using DataBindCrossTable() is
its requirement for a group by column. That is, the method wants
to know which column is the one your data is grouped by. It is
the second argument in the example call above. And there was no
way to get around it -- including an empty string or a pair of
quotes or "Null" would only create an unhandled exception. So
my workaround was to include a blank column in my data (node,
really) called "groupby", and specify it as my group by column.
In my opinion, the better method to use is to bind the data
down at the data points level within the series. The big "pro"
here is the ability to create properties on the data points.
In the example which follows, I use the fourth argument in the
method to create a series of properties named for my data columns:
.Series("Series1").Points.DataBind(dvDataView, "date", "weight", "Date=date,Weight=weight,Comment=comment, Location=location,Scale=scale,Medication=medication")
Later I can refer to these properties by name in an event handler,
like this:
. . .
For Each point In Chart1.Series("Series1").Points
Dim strComment As String = point("Comment")
Dim strMedication As String = point("Medication")
Dim strLocation As String = point("Location")
Dim strScale As String = point("Scale")
. . .
This greatly surpasses the flexibility offered by the
DataBindCrossTable() method in my humble opinion. Plus, there's
no requirement for a group-by column. These reasons made this
method of data binding my choice for this project.
(5) The Filter Control
I mentioned at the top that this chart "allows different data to
be 'overlayed' atop the chart using a filter control.
In the broad strokes, the filter control is a dropdown control that
allows the user to select any or none of the available options for
additional data to be drawn on the chart. The control is populated
with static values and the change in index triggers an event. The
code forward is pretty basic:
Here's how the control is populated in the code behind:
If Not Page.IsPostBack Then
With ddlFilter.Items
.Add(New ListItem("None", ""))
.Add(New ListItem("Location", "Location"))
.Add(New ListItem("Medication", "Medication"))
.Add(New ListItem("Scale", "Scale"))
End With
'set default selected option
ViewState("filter") = "None"
End If 'If Not Page.IsPostBack
The filter's SelectedIndexChanged event handler simply records the
new selected item:
Sub ddlFilter_SelectedIndexChanged(ByVal sender As Object, ByVal e As EventArgs) Handles ddlFilter.SelectedIndexChanged
Dim strFilter As String = ddlFilter.SelectedItem.Text
'copy the new filter value into ViewState
ViewState("filter") = strFilter
End Sub
(6) The Chart_DataBound Event Handler
The real power behind the advanced chart is showcased in this
event handler. This code, based on the selected value in the
filter control:
-- sets the values for the subtitle control ("Title2") and the
legend label control
-- iterates through the different points in the series, copies
all of the properties into string variables, and then sets values
in the various properties (tooltip, label, color).
The full content of the event handler is posted below. It's heavily
commented to help explain what's happening where.
Note that the code in the Catch block mirrors that of the
LoadDataSet() function at top. In the event an exception is
caught, its message is displayed in red text and the chart
is hidden.
Sub Chart1_DataBound(ByVal sender As Object, ByVal e As EventArgs)
Handles Chart1.DataBound
Dim strFilter As String = CType(ViewState("filter"), String).Trim()
Dim point As DataPoint
pnlLegend.Visible = False
Try
'chart
With Chart1
'add custom title and legend text based on the filter value.
'the legend is not the chart legend -- it's a legend I made in a panel on the side.
'the chart legend for bar charts can't be manipulated -- only shows the series.
If strFilter <> String.Empty Then
Select Case strFilter
Case "Medication"
.Titles("Title2").Text = "With Prescription Weight Loss Medications"
lblLegend.Text = "
- Qsymia
- Phentermine
- No Medication
View of progress made with the aid of prescription weight loss medications."
Case "Location"
.Titles("Title2").Text = "With Measurement Location"
lblLegend.Text = "Doctor's office measurements include some added weight for clothing."
Case "Scale"
.Titles("Title2").Text = "With Scale Descriptions"
lblLegend.Text = "- Old scale
- New scale
- Doctor's Office
The old scale was less accurate than the new when compared to the doctor's office (look at the differences in bar height).
Also, the doctor's office measurements include some added weight for clothing."
End Select
End If 'strFilter <> String.Empty
End With
'points
'set the toolip, label and color information
For Each point In Chart1.Series("Series1").Points
Dim strComment As String = point("Comment")
Dim strMedication As String = point("Medication")
Dim strLocation As String = point("Location")
Dim strScale As String = point("Scale")
'set default values
point.ToolTip = strComment 'the tooltip is set to the Comment property content
point.Color = Color.LightGray 'the bars are colored light gray by default
'set values based on filter value
If strFilter <> String.Empty Then
Select Case strFilter
Case "Medication"
point.ToolTip += " - " & strMedication
If Left(LCase(strMedication), 4) = "qsym" Then
point.Color = Color.Purple
End If
If Left(LCase(strMedication), 4) = "phen" Then
point.Color = Color.DarkOrange
End If
If strMedication Is Nothing Then
point.LegendText = "No Medication"
End If
Case "Location"
'differentiate between measurements taken at the doctor's office
'and at home.
If strLocation <> "home" Then
point.Color = Color.DarkOrange
point.ToolTip += " - " & strLocation
End If
Case "Scale"
'Differentiate among the old scale, the new scale, and the
'doctor's office scale.
If strScale = "old" Then
point.Color = Color.Navy
point.ToolTip += " - " & strScale
End If
If strScale = "new" Then
point.Color = Color.DarkOrange
point.ToolTip += " - " & strScale
End If
If strScale Is Nothing Then
point.Color = Color.DarkGray
End If
Case "None"
point.Color = Color.DarkOrange
End Select
End If 'strFilter <> string.Empty
Next
Catch ex As Exception
'build the status message, hide the chart and panel.
lblStatus.Text = ex.Message
lblStatus.ForeColor = Color.Red
lblStatus.Visible = True
Chart1.Visible = False
pnlLegend.Visible = False
End Try
pnlLegend.Visible = True
End Sub
(7) Bonus: Save
Despite the configuration, I wanted to know if it was
possible to save a copy of the chart into an image file.
I ran this on my local machine, but haven't enabled/
attempted it in my production environment. I suspect it
wouldn't work because the trust is locked down so hard.
Anyway, here's a bonus subroutine for you.
Sub SaveChart()
'2016-10-01
'called from Chart1_DataBound() event handler
'wrapped in a Boolean variable
Try
Chart1.SaveImage(Server.MapPath("~/fitness/fitnessGraph.png"), ChartImageFormat.Png)
Catch ex As Exception
'build the status message, hide the chart and panel.
lblStatus.Text = ex.Message
lblStatus.ForeColor = Color.Red
lblStatus.Visible = True
Chart1.Visible = False
pnlLegend.Visible = False
End Try
End Sub
Note that the code in the Catch block mirrors that of the
LoadDataSet() function and the Chart1_DataBound() event handler
at top. In the event an exception is caught, its message is
displayed in red text and the chart is hidden.
That should be it for now! Contact me using the site's contact form if
you have questions. Feel free to use the code in your projects. A shout
out in your project would be thoughtful. Also, drop me a line and let me
know how you might have tweaked things to better suit your needs.
Finally, I wouldn't profess to be THE expert on matters represented in
my code -- so drop me a line if you have constructive suggestions, too.
I'd like to hear from you!
Best,
halfgk
Copyright 2016 halfgk.com