Note: This post makes use of the XYZExtension, which at the time the post was written was not available on the public version of Chart FX for WPF. If you are interested in using the extension before its release with the next Service Pack you may download the hotfix at http://support.softwarefx.com/cfxwpf/update/hotfix/hotfix.htm.
Chart FX for WPF is about to introduce a brand new feature: the XYZExtension. This extension will allow us to add and control a third axis (Z axis) on a 3D chart. For the first time it will be possible to plot X, Y and Z values to a Chart FX chart. Right out of the gate there are two different galleries that can use this extension: SurfaceXYZ and ScatterXYZ. Multiple series of each or even combinations are also possible.
One of the cool things I was able to accomplish with the new SurfaceXYZ chart was create geographically accurate surface with data from different cities. I can see multiple uses for a chart like this, from temperature plotting, revenue per county or even sales per store in one state.
Suppose for the sake of this example that we need to plot the customer satisfaction rating on a little more than 800 stores across Florida. Plotting it on a bar chart will create more than 800 bars. And even if we order the cities alphabetically, while locating the score of a specific store should be easy, it is not so simple to understand which areas of the state are doing better than the others. Plotting it on a surface, on the other hand, will group all the stores by proximity allowing us to locate the best and worse regions. Particularly if we can do so over a map of the state. Here is what our final surface chart should look like.
Looking at that map it is very easy to spot that the cities stores around Jacksonville and the west coast are doing better than the ones on the east coast of Florida. Let’s look at what was necessary to obtain that great chart, shall we?
First thing we need is a map. I have selected a Mercator projection of the state, with no county lines. If you have not noticed yet, we are laying the map on top of our surface. That is done because the triangulation used for our surface will interpolate all the data passed to our chart, creating a false impression that data was collected from stores located in the Gulf of Mexico. To take care of that, we lay a map over the surface where the state itself is transparent. That way the non-transparent ocean will cover the interpolated data that does not make sense. Here is what our map looks like without our surface.
Now that we have selected a map, let’s look at a slice of our data.
<Store CITY="MIAMI" LATITUDE="25.64" LONGITUDE="-80.32" SCORE="91" />
<Store CITY="NORTH MIAMI BEAC" LATITUDE="25.94" LONGITUDE="-80.14" SCORE="89" />
<Store CITY="NORTH MIAMI" LATITUDE="25.89" LONGITUDE="-80.18" SCORE="91" />
<Store CITY="NORTH MIAMI BEAC" LATITUDE="25.93" LONGITUDE="-80.18" SCORE="91" />
<Store CITY="OLYMPIA HEIGHTS" LATITUDE="25.74" LONGITUDE="-80.36" SCORE="92" />
<Store CITY="MIAMI SPRINGS" LATITUDE="25.82" LONGITUDE="-80.30" SCORE="92" />
We have the latitude and longitude data for the store. While that makes it perfect to locate it via GPS, it does not make for a surface friendly data. So we are going to have to convert it to pixel values on our selected map. Since this is a Mercator projection, we use the formulas bellow to convert the data (where φ is the latitude and λ is the longitude).
To keep this a short post (or try to) I won’t post the full code for the conversion. But I will make it available as a sample on the next Service Pack (or you can ask support for it, at support [at] softwarefx [dot] com).
Once we have our data formatted in a way we can use, let’s pass it to the chart and see what we get.
SurfaceXYZ surfaceXYZ = new SurfaceXYZ();
surfaceXYZ.ShowPointsGridlines = false;
surfaceXYZ.ShowSeriesGridlines = false;
surfaceXYZ.ShowContourLines = true;
chart1.ItemsSource = chartData;
SeriesAttributes series0 = new SeriesAttributes();
SeriesAttributes series1 = new SeriesAttributes();
series0.GalleryAttributes = surfaceXYZ;
series1.GalleryAttributes = surfaceXYZ;
series0.BindingPath = "Score";
series0.BindingPathX = "X";
series1.BindingPath = "Y";
Note how we need two series for an XYZ chart. The second series is bound to the Z data, but since we are looking to our surface from above in a two dimensional way, we will call it “Y” (or latitude, on a map). The Y axis represents the value we are plotting, the Score. If this was not a 2D representation of the chart (or contour) it would represent the depth or height. This is what we end up with.
I am sparing you from the cosmetic code, that can also be available if requested. If we make a few changes to our chart to make it a contour, this is what we will have.
ChartFX.WPF.View3D view3D = chart1.View3D;
view3D.IsEnabled = true;
view3D.AngleX = -90;
view3D.AngleY = 0;
view3D.Projection = Projection.Orthographic;
view3D.BackWallVisibility = Visibility.Collapsed;
chart1.AxisX.Line.Visibility = Visibility.Hidden;
chart1.AxisX.Grids.Major.Visibility = Visibility.Hidden;
System.Windows.Media.Media3D.AmbientLight ambLight =
new System.Windows.Media.Media3D.AmbientLight(Color.FromRgb(0xD0, 0xD0, 0xD0));
If you have very sharp sight you will notice something. The “Florida shape” looks a little distorted. That happens because our contour was placed on a square, not keeping the ratio set by our chosen map. Also, the Max value of both the X and Z axis are not the values we used in our map. All that will distort our surface, making an unrealistic representation of our data. To fix that, here is the code necessary.
chart1.AxisX.Max = 820; //Height of the bitmap
chart1.AxisX.Min = 0;
XYZExtension xyzExtension = new XYZExtension();
xyzExtension.AxisZ.Max = 772; //Width of the bitmap
xyzExtension.AxisZ.Min = 0;
chart1.View3D.Depth = 94.1463; //Width divided by Height
Note that we use the XYZExtension to control the Max and Min of our Z axis. We then set both X and Z axis’ Max and Mix to the size of the map. We also changed the Depth property to the ratio of our map (almost a square, but not really). Changing the Depth property will control the size of the Z axis.
All we need now is to put the map on top! And here is the trick. We will create an ImageBrush out of the map, use it to paint the bottom of the chart (the X Axis) and relocate the bottom of the chart to the top.
chart1.AxisX.Position = AxisPosition.Far;
ImageBrush mapBrush = new ImageBrush();
mapBrush.Stretch = Stretch.Fill;
new BitmapImage(new Uri("pack://siteoforigin:,,,/Resources/FloridaMap.png"));
chart1.AxisX.Background = mapBrush;
chart1.View3D.Wall.Thickness = 1;
Voilà! Our chart is ready. As mentioned before (more than a couple times, i guess), the necessary API to create a chart like this will be available on our next Service Pack, but if you can’t wait to play with it, download the hotfix at http://support.softwarefx.com/cfxwpf/update/hotfix/hotfix.htm.