3D Numerical Chart

The chart

The 3D charting API is built on top of Wt's 3D painting API. A chart is represented by a WCartesian3DChart, which derives from WGLWidget.

Based on the sort of data that will be shown, a chart is one of two types (the same as for 2D charts):

  • a CategoryChart for categorized data
  • a ScatterPlot for numerical data

Types of data

All data that can be shown in a 3D chart, has to derive from WAbstractDataSeries3D. Instances of this class include a model containing the actual data and all logic to draw themselves on the chart. This means all graphical features of the data (color, point size ...) are also part of this class.

Wt provides three built-in classes representing data:

  • WGridData: data lying on a grid specified by an x- and y-axis
  • WEquidistantGridData: data lying on a grid with an equidistant x- and y-axis
  • WScatterData: data containing a collection of unconstrained points

Each instance of these data series has a WAbstractItemModel backing them. The model for all these data series has to be provided as a table.
In WGridData, the table must contain one column representing the x-axis values and one row representing the y-axis values. All other values then specify the z-axis value belonging to the corresponding (x, y) pair.
The backing model for WEquidistantGridData is similar, with the difference that no x- and y-axis values are required. These can be specified directly to the class instance using setXAbscis(double XMinimum, double deltaX) and setYAbscis(double YMinimum, double deltaY).
The model for WScatterData must have three columns containing (x, y, z) values for every point.

All grid-based data has the additional feature that the visualization of the data is determined by its Series3DType, which can be either of three values:

  • PointSeries3D
  • SurfaceSeries3D
  • BarSeries3D
Note that PointSeries3D and SurfaceSeries3D only apply to a chart of type ScatterPlot and BarSeries3D only applies to a CategoryChart.

source
#include <Wt/WApplication.h>
#include <Wt/WCssDecorationStyle.h>
#include <Wt/WBorder.h>
#include <Wt/WContainerWidget.h>
#include <Wt/WImage.h>
#include <Wt/Chart/WCartesian3DChart.h>
#include <Wt/Chart/WGridData.h>
#include <Wt/Chart/WEquidistantGridData.h>
#include <Wt/Chart/WScatterData.h>
#include <Wt/Chart/WStandardColorMap.h>

#include "DataModels.h"


auto container = std::make_unique<WContainerWidget>();

// create the chart and add a border to the widget
Chart::WCartesian3DChart *chart = container->addNew<Chart::WCartesian3DChart>();
chart->setType(Chart::ChartType::Scatter);

// disable server-side rendering fallback; our VPSes don't have that
chart->setRenderOptions(GLRenderOption::ClientSide | GLRenderOption::AntiAliasing);

WCssDecorationStyle style;
style.setBorder(WBorder(BorderStyle::Solid, BorderWidth::Medium,
                            WColor(StandardColor::Black)));
chart->setDecorationStyle(style);

chart->resize(900, 700);
chart->setGridEnabled(Chart::Plane::XY, Chart::Axis::X3D, true);
chart->setGridEnabled(Chart::Plane::XY, Chart::Axis::Y3D, true);
chart->setGridEnabled(Chart::Plane::XZ, Chart::Axis::X3D, true);
chart->setGridEnabled(Chart::Plane::XZ, Chart::Axis::Z3D, true);
chart->setGridEnabled(Chart::Plane::YZ, Chart::Axis::Y3D, true);
chart->setGridEnabled(Chart::Plane::YZ, Chart::Axis::Z3D, true);

chart->axis(Chart::Axis::X3D).setTitle("X");
chart->axis(Chart::Axis::Y3D).setTitle("Y");
chart->axis(Chart::Axis::Z3D).setTitle("Z");

chart->setIntersectionLinesEnabled(true);
chart->setIntersectionLinesColor(WColor(0, 255, 255));

// make first dataset (WGridData)
auto model1 = std::make_shared<SombreroData>(40, 40);
auto dataset1 = std::make_unique<Chart::WGridData>(model1);
dataset1->setType(Chart::Series3DType::Surface);
dataset1->setSurfaceMeshEnabled(true);
auto colormap =
    std::make_shared<Chart::WStandardColorMap>(dataset1->minimum(Chart::Axis::Z3D),
                                     dataset1->maximum(Chart::Axis::Z3D),
                                     true);
dataset1->setColorMap(colormap);

// make second dataset (WEquidistantGridData)
auto model2 = std::make_shared<PlaneData>(40, 40);
for (int i=0; i < model2->rowCount(); i++) { // set a few size-roles
    model2->setData(i, 0, 5, ItemDataRole::MarkerScaleFactor);
    model2->setData(i, model2->columnCount()-1, 5, ItemDataRole::MarkerScaleFactor);
}

for (int i=0; i < model2->columnCount(); i++) {
    model2->setData(0, i, 5, ItemDataRole::MarkerScaleFactor);
    model2->setData(model2->rowCount()-1, i, 5, ItemDataRole::MarkerScaleFactor);
}

for (int i=0; i < model2->rowCount(); i++) { // set a few color-roles
    model2->setData(i, 5, WColor(0,255,0), ItemDataRole::MarkerBrushColor);
    model2->setData(i, 6, WColor(0,0,255), ItemDataRole::MarkerBrushColor);
    model2->setData(i, 7, WColor(0,255,0), ItemDataRole::MarkerBrushColor);
    model2->setData(i, 8, WColor(0,0,255), ItemDataRole::MarkerBrushColor);
}

auto dataset2 =
    std::make_unique<Chart::WEquidistantGridData>(model2, -10, 0.5f, -10, 0.5f);

// make third dataset (WScatterData)
auto model3 = std::make_shared<SpiralData>(100);
auto dataset3 = std::make_unique<Chart::WScatterData>(model3);
dataset3->setPointSize(5);

// make fourth dataset (WEquidistantGridData, intersecting with dataset1)
auto model4 = std::make_shared<HorizontalPlaneData>(20, 20);
auto dataset4 =
  std::make_unique<Chart::WEquidistantGridData>(model4, -10, 1.0f, -10, 1.0f);
dataset4->setType(Chart::Series3DType::Surface);
dataset4->setSurfaceMeshEnabled(true);

// add the data to the chart
chart->addDataSeries(std::move(dataset1));
chart->addDataSeries(std::move(dataset2));
chart->addDataSeries(std::move(dataset3));
chart->addDataSeries(std::move(dataset4));

chart->setAlternativeContent
    (std::make_unique<WImage>(WLink("pics/numericalChartScreenshot.png")));