/*
* Copyright (C) 2008 Emweb bvba, Kessel-Lo, Belgium.
*
* See the LICENSE file for terms of use.
*/
#include "ChartConfig.h"
#include "PanelList.h"
#include <iostream>
#include <boost/date_time/gregorian/greg_year.hpp>
#include <Wt/WAbstractItemModel>
#include <Wt/WApplication>
#include <Wt/WCheckBox>
#include <Wt/WComboBox>
#include <Wt/WDoubleValidator>
#include <Wt/WDate>
#include <Wt/WEnvironment>
#include <Wt/WIntValidator>
#include <Wt/WLineEdit>
#include <Wt/WLocale>
#include <Wt/WPanel>
#include <Wt/WPushButton>
#include <Wt/WStandardItemModel>
#include <Wt/WTable>
#include <Wt/WText>
#include <Wt/WPainterPath>
#include <Wt/Chart/WCartesianChart>
using namespace Wt;
using namespace Wt::Chart;
namespace {
void addHeader(WTable *t, const char *value) {
t->elementAt(0, t->columnCount())->addWidget(new WText(value));
}
void addEntry(WAbstractItemModel *model, const char *value) {
model->insertRows(model->rowCount(), 1);
model->setData(model->rowCount()-1, 0, boost::any(std::string(value)));
}
bool getDouble(WLineEdit *edit, double& value) {
try {
value = WLocale::currentLocale().toDouble(edit->text());
return true;
} catch (...) {
return false;
}
}
int seriesIndexOf(WCartesianChart* chart, int modelColumn) {
for (unsigned i = 0; i < chart->series().size(); ++i)
if (chart->series()[i].modelColumn() == modelColumn)
return i;
return -1;
}
}
ChartConfig::ChartConfig(WCartesianChart *chart, WContainerWidget *parent)
: WContainerWidget(parent),
chart_(chart),
fill_(MinimumValueFill)
{
chart_->setLegendStyle(chart_->legendFont(), WPen(black),
WBrush(WColor(0xFF, 0xFA, 0xE5)));
chart->initLayout();
PanelList *list = new PanelList(this);
WIntValidator *sizeValidator = new WIntValidator(200, 2000, this);
sizeValidator->setMandatory(true);
WDoubleValidator *anyNumberValidator = new WDoubleValidator(this);
anyNumberValidator->setMandatory(true);
WDoubleValidator *angleValidator = new WDoubleValidator(-90, 90, this);
angleValidator->setMandatory(true);
// ---- Chart properties ----
WStandardItemModel *orientation = new WStandardItemModel(0, 1, this);
addEntry(orientation, "Vertical");
addEntry(orientation, "Horizontal");
WStandardItemModel *legendLocation = new WStandardItemModel(0, 1, this);
addEntry(legendLocation, "Outside");
addEntry(legendLocation, "Inside");
WStandardItemModel *legendSide = new WStandardItemModel(0, 1, this);
addEntry(legendSide, "Top");
addEntry(legendSide, "Right");
addEntry(legendSide, "Bottom");
addEntry(legendSide, "Left");
WStandardItemModel *legendAlignment = new WStandardItemModel(0, 1, this);
addEntry(legendAlignment, "AlignLeft");
addEntry(legendAlignment, "AlignCenter");
addEntry(legendAlignment, "AlignRight");
addEntry(legendAlignment, "AlignTop");
addEntry(legendAlignment, "AlignMiddle");
addEntry(legendAlignment, "AlignBottom");
WTable *chartConfig = new WTable();
chartConfig->setMargin(WLength::Auto, Left | Right);
int row = 0;
chartConfig->elementAt(row, 0)->addWidget(new WText("Title:"));
titleEdit_ = new WLineEdit(chartConfig->elementAt(row, 1));
connectSignals(titleEdit_);
++row;
chartConfig->elementAt(row, 0)->addWidget(new WText("Width:"));
chartWidthEdit_ = new WLineEdit(chartConfig->elementAt(row, 1));
chartWidthEdit_
->setText(WLocale::currentLocale().toString(chart_->width().value()));
chartWidthEdit_->setValidator(sizeValidator);
chartWidthEdit_->setMaxLength(4);
connectSignals(chartWidthEdit_);
++row;
chartConfig->elementAt(row, 0)->addWidget(new WText("Height:"));
chartHeightEdit_ = new WLineEdit(chartConfig->elementAt(row, 1));
chartHeightEdit_
->setText(WLocale::currentLocale().toString(chart_->height().value()));
chartHeightEdit_->setValidator(sizeValidator);
chartHeightEdit_->setMaxLength(4);
connectSignals(chartHeightEdit_);
++row;
chartConfig->elementAt(row, 0)->addWidget(new WText("Orientation:"));
chartOrientationEdit_ = new WComboBox(chartConfig->elementAt(row, 1));
chartOrientationEdit_->setModel(orientation);
connectSignals(chartOrientationEdit_);
++row;
chartConfig->elementAt(row, 0)->addWidget(new WText("Legend location:"));
legendLocationEdit_ = new WComboBox(chartConfig->elementAt(row, 1));
legendLocationEdit_->setModel(legendLocation);
connectSignals(legendLocationEdit_);
++row;
chartConfig->elementAt(row, 0)->addWidget(new WText("Legend side:"));
legendSideEdit_ = new WComboBox(chartConfig->elementAt(row, 1));
legendSideEdit_->setModel(legendSide);
legendSideEdit_->setCurrentIndex(1);
connectSignals(legendSideEdit_);
++row;
chartConfig->elementAt(row, 0)->addWidget(new WText("Legend alignment:"));
legendAlignmentEdit_ = new WComboBox(chartConfig->elementAt(row, 1));
legendAlignmentEdit_->setModel(legendAlignment);
legendAlignmentEdit_->setCurrentIndex(4);
connectSignals(legendAlignmentEdit_);
++row;
for (int i = 0; i < chartConfig->rowCount(); ++i) {
chartConfig->elementAt(i, 0)->setStyleClass("tdhead");
chartConfig->elementAt(i, 1)->setStyleClass("tddata");
}
WPanel *p = list->addWidget("Chart properties", chartConfig);
p->setMargin(WLength::Auto, Left | Right);
p->resize(880, WLength::Auto);
p->setMargin(20, Top | Bottom);
if (chart_->isLegendEnabled())
chart_->setPlotAreaPadding(200, Right);
// ---- Series properties ----
WStandardItemModel *types = new WStandardItemModel(0, 1, this);
addEntry(types, "Points");
addEntry(types, "Line");
addEntry(types, "Curve");
addEntry(types, "Bar");
addEntry(types, "Line Area");
addEntry(types, "Curve Area");
addEntry(types, "Stacked Bar");
addEntry(types, "Stacked Line Area");
addEntry(types, "Stacked Curve Area");
WStandardItemModel *markers = new WStandardItemModel(0, 1, this);
addEntry(markers, "None");
addEntry(markers, "Square");
addEntry(markers, "Circle");
addEntry(markers, "Cross");
addEntry(markers, "X cross");
addEntry(markers, "Triangle");
addEntry(markers, "Diamond");
WStandardItemModel *axes = new WStandardItemModel(0, 1, this);
addEntry(axes, "1st Y axis");
addEntry(axes, "2nd Y axis");
WStandardItemModel *labels = new WStandardItemModel(0, 1, this);
addEntry(labels, "None");
addEntry(labels, "X");
addEntry(labels, "Y");
addEntry(labels, "X: Y");
WTable *seriesConfig = new WTable();
seriesConfig->setMargin(WLength::Auto, Left | Right);
::addHeader(seriesConfig, "Name");
::addHeader(seriesConfig, "Enabled");
::addHeader(seriesConfig, "Type");
::addHeader(seriesConfig, "Marker");
::addHeader(seriesConfig, "Y axis");
::addHeader(seriesConfig, "Legend");
::addHeader(seriesConfig, "Shadow");
::addHeader(seriesConfig, "Value labels");
seriesConfig->rowAt(0)->setStyleClass("trhead");
for (int j = 1; j < chart->model()->columnCount(); ++j) {
SeriesControl sc;
new WText(asString(chart->model()->headerData(j)),
seriesConfig->elementAt(j, 0));
sc.enabledEdit = new WCheckBox(seriesConfig->elementAt(j, 1));
connectSignals(sc.enabledEdit);
sc.typeEdit = new WComboBox(seriesConfig->elementAt(j, 2));
sc.typeEdit->setModel(types);
connectSignals(sc.typeEdit);
sc.markerEdit = new WComboBox(seriesConfig->elementAt(j, 3));
sc.markerEdit->setModel(markers);
connectSignals(sc.markerEdit);
sc.axisEdit = new WComboBox(seriesConfig->elementAt(j, 4));
sc.axisEdit->setModel(axes);
connectSignals(sc.axisEdit);
sc.legendEdit = new WCheckBox(seriesConfig->elementAt(j, 5));
connectSignals(sc.legendEdit);
sc.shadowEdit = new WCheckBox(seriesConfig->elementAt(j, 6));
connectSignals(sc.shadowEdit);
sc.labelsEdit = new WComboBox(seriesConfig->elementAt(j, 7));
sc.labelsEdit->setModel(labels);
connectSignals(sc.labelsEdit);
int si = seriesIndexOf(chart, j);
if (si != -1) {
sc.enabledEdit->setChecked();
const WDataSeries& s = chart_->series(j);
switch (s.type()) {
case PointSeries:
sc.typeEdit->setCurrentIndex(0); break;
case LineSeries:
sc.typeEdit->setCurrentIndex(s.fillRange() != NoFill ?
(s.isStacked() ? 7 : 4) : 1); break;
case CurveSeries:
sc.typeEdit->setCurrentIndex(s.fillRange() != NoFill ?
(s.isStacked() ? 8 : 5) : 2); break;
case BarSeries:
sc.typeEdit->setCurrentIndex(s.isStacked() ? 6 : 3);
}
sc.markerEdit->setCurrentIndex((int)s.marker());
sc.legendEdit->setChecked(s.isLegendEnabled());
sc.shadowEdit->setChecked(s.shadow() != WShadow());
}
seriesControls_.push_back(sc);
seriesConfig->rowAt(j)->setStyleClass("trdata");
}
p = list->addWidget("Series properties", seriesConfig);
p->expand();
p->setMargin(WLength::Auto, Left | Right);
p->resize(880, WLength::Auto);
p->setMargin(20, Top | Bottom);
// ---- Axis properties ----
WStandardItemModel *yScales = new WStandardItemModel(0, 1, this);
addEntry(yScales, "Linear scale");
addEntry(yScales, "Log scale");
WStandardItemModel *xScales = new WStandardItemModel(0, 1, this);
addEntry(xScales, "Categories");
addEntry(xScales, "Linear scale");
addEntry(xScales, "Log scale");
addEntry(xScales, "Date scale");
WTable *axisConfig = new WTable();
axisConfig->setMargin(WLength::Auto, Left | Right);
::addHeader(axisConfig, "Axis");
::addHeader(axisConfig, "Visible");
::addHeader(axisConfig, "Scale");
::addHeader(axisConfig, "Automatic");
::addHeader(axisConfig, "Minimum");
::addHeader(axisConfig, "Maximum");
::addHeader(axisConfig, "Gridlines");
::addHeader(axisConfig, "Label angle");
axisConfig->rowAt(0)->setStyleClass("trhead");
for (int i = 0; i < 3; ++i) {
const char *axisName[] = { "X axis", "1st Y axis", "2nd Y axis" };
int j = i + 1;
const WAxis& axis = chart_->axis(static_cast<Axis>(i));
AxisControl sc;
new WText(WString(axisName[i], UTF8), axisConfig->elementAt(j, 0));
sc.visibleEdit = new WCheckBox(axisConfig->elementAt(j, 1));
sc.visibleEdit->setChecked(axis.isVisible());
connectSignals(sc.visibleEdit);
sc.scaleEdit = new WComboBox(axisConfig->elementAt(j, 2));
if (axis.scale() == CategoryScale)
sc.scaleEdit->addItem("Category scale");
else {
if (axis.id() == XAxis) {
sc.scaleEdit->setModel(xScales);
sc.scaleEdit->setCurrentIndex(axis.scale());
} else {
sc.scaleEdit->setModel(yScales);
sc.scaleEdit->setCurrentIndex(axis.scale() - 1);
}
}
connectSignals(sc.scaleEdit);
bool autoValues = axis.autoLimits() == (MinimumValue | MaximumValue);
sc.minimumEdit = new WLineEdit(axisConfig->elementAt(j, 4));
sc.minimumEdit->setText(WLocale::currentLocale()
.toString(axis.minimum()));
sc.minimumEdit->setValidator(anyNumberValidator);
sc.minimumEdit->setEnabled(!autoValues);
connectSignals(sc.minimumEdit);
sc.maximumEdit = new WLineEdit(axisConfig->elementAt(j, 5));
sc.maximumEdit->setText(WLocale::currentLocale()
.toString(axis.maximum()));
sc.maximumEdit->setValidator(anyNumberValidator);
sc.maximumEdit->setEnabled(!autoValues);
connectSignals(sc.maximumEdit);
sc.autoEdit = new WCheckBox(axisConfig->elementAt(j, 3));
sc.autoEdit->setChecked(autoValues);
connectSignals(sc.autoEdit);
sc.autoEdit->checked().connect(sc.maximumEdit, &WLineEdit::disable);
sc.autoEdit->unChecked().connect(sc.maximumEdit, &WLineEdit::enable);
sc.autoEdit->checked().connect(sc.minimumEdit, &WLineEdit::disable);
sc.autoEdit->unChecked().connect(sc.minimumEdit, &WLineEdit::enable);
sc.gridLinesEdit = new WCheckBox(axisConfig->elementAt(j, 6));
connectSignals(sc.gridLinesEdit);
sc.labelAngleEdit = new WLineEdit(axisConfig->elementAt(j, 7));
sc.labelAngleEdit->setText("0");
sc.labelAngleEdit->setValidator(angleValidator);
connectSignals(sc.labelAngleEdit);
axisConfig->rowAt(j)->setStyleClass("trdata");
axisControls_.push_back(sc);
}
p = list->addWidget("Axis properties", axisConfig);
p->setMargin(WLength::Auto, Left | Right);
p->resize(880, WLength::Auto);
p->setMargin(20, Top | Bottom);
/*
* If we do not have JavaScript, then add a button to reflect changes to
* the chart.
*/
if (!WApplication::instance()->environment().javaScript()) {
WPushButton *b = new WPushButton(this);
b->setText("Update chart");
b->setInline(false); // so we can add margin to center horizontally
b->setMargin(WLength::Auto, Left | Right);
b->clicked().connect(this, &ChartConfig::update);
}
}
void ChartConfig::setValueFill(FillRangeType fill)
{
fill_ = fill;
}
void ChartConfig::update()
{
bool haveLegend = false;
std::vector<WDataSeries> series;
for (int i = 1; i < chart_->model()->columnCount(); ++i) {
SeriesControl& sc = seriesControls_[i-1];
if (sc.enabledEdit->isChecked()) {
WDataSeries s(i);
switch (sc.typeEdit->currentIndex()) {
case 0:
s.setType(PointSeries);
if (sc.markerEdit->currentIndex() == 0)
sc.markerEdit->setCurrentIndex(1);
break;
case 1:
s.setType(LineSeries);
sc.markerEdit->setCurrentIndex(0);
break;
case 2:
s.setType(CurveSeries);
sc.markerEdit->setCurrentIndex(0);
break;
case 3:
s.setType(BarSeries);
sc.markerEdit->setCurrentIndex(0);
break;
case 4:
s.setType(LineSeries);
s.setFillRange(fill_);
sc.markerEdit->setCurrentIndex(0);
break;
case 5:
s.setType(CurveSeries);
s.setFillRange(fill_);
sc.markerEdit->setCurrentIndex(0);
break;
case 6:
s.setType(BarSeries);
s.setStacked(true);
sc.markerEdit->setCurrentIndex(0);
break;
case 7:
s.setType(LineSeries);
s.setFillRange(fill_);
s.setStacked(true);
sc.markerEdit->setCurrentIndex(0);
break;
case 8:
s.setType(CurveSeries);
s.setFillRange(fill_);
s.setStacked(true);
sc.markerEdit->setCurrentIndex(0);
}
//set WPainterPath to draw a diamond
if(sc.markerEdit->currentIndex() == CustomMarker){
WPainterPath pp = WPainterPath();
pp.moveTo(-6, 0);
pp.lineTo(0, 6);
pp.lineTo(6, 0);
pp.lineTo(0, -6);
pp.lineTo(-6, 0);
s.setCustomMarker(pp);
}
s.setMarker(static_cast<MarkerType>(sc.markerEdit->currentIndex()));
if (sc.axisEdit->currentIndex() == 1) {
s.bindToAxis(Y2Axis);
}
if (sc.legendEdit->isChecked()) {
s.setLegendEnabled(true);
haveLegend = true;
} else
s.setLegendEnabled(false);
if (sc.shadowEdit->isChecked()) {
s.setShadow(WShadow(3, 3, WColor(0, 0, 0, 127), 3));
} else
s.setShadow(WShadow());
switch (sc.labelsEdit->currentIndex()) {
case 1:
s.setLabelsEnabled(XAxis);
break;
case 2:
s.setLabelsEnabled(YAxis);
break;
case 3:
s.setLabelsEnabled(XAxis);
s.setLabelsEnabled(YAxis);
break;
}
series.push_back(s);
}
}
chart_->setSeries(series);
for (int i = 0; i < 3; ++i) {
AxisControl& sc = axisControls_[i];
WAxis& axis = chart_->axis(static_cast<Axis>(i));
axis.setVisible(sc.visibleEdit->isChecked());
if (sc.scaleEdit->count() != 1) {
int k = sc.scaleEdit->currentIndex();
if (axis.id() != XAxis)
k += 1;
else {
if (k == 0)
chart_->setType(CategoryChart);
else
chart_->setType(ScatterPlot);
}
switch (k) {
case 1:
axis.setScale(LinearScale); break;
case 2:
axis.setScale(LogScale); break;
case 3:
axis.setScale(DateScale); break;
}
}
if (sc.autoEdit->isChecked())
axis.setAutoLimits(MinimumValue | MaximumValue);
else {
if (validate(sc.minimumEdit) && validate(sc.maximumEdit)) {
double min, max;
getDouble(sc.minimumEdit, min);
getDouble(sc.maximumEdit, max);
if (axis.scale() == LogScale)
if (min <= 0)
min = 0.0001;
if (axis.scale() == DateScale){
//the number of julian days until year 1986
WDate dMin = WDate(1900,1,1);
double gregDaysMin = (double)dMin.toJulianDay();
//the number of julian days until year 1988
WDate dMax = WDate(3000,1,1);
double gregDaysMax = (double)dMax.toJulianDay();
bool greg_year_validation =
(min > gregDaysMin &&
min < gregDaysMax &&
max > gregDaysMin &&
max < gregDaysMax);
if(!greg_year_validation){
min = gregDaysMin;
max = gregDaysMax;
}
}
axis.setRange(min, max);
}
}
if (validate(sc.labelAngleEdit)) {
double angle;
getDouble(sc.labelAngleEdit, angle);
axis.setLabelAngle(angle);
}
axis.setGridLinesEnabled(sc.gridLinesEdit->isChecked());
}
chart_->setTitle(titleEdit_->text());
if (validate(chartWidthEdit_) && validate(chartHeightEdit_)) {
double width, height;
getDouble(chartWidthEdit_, width);
getDouble(chartHeightEdit_, height);
chart_->resize(width, height);
}
switch (chartOrientationEdit_->currentIndex()) {
case 0:
chart_->setOrientation(Vertical); break;
case 1:
chart_->setOrientation(Horizontal); break;
}
chart_->setLegendEnabled(haveLegend);
if (haveLegend) {
LegendLocation location = LegendOutside;
Side side = Right;
AlignmentFlag alignment = AlignMiddle;
switch (legendLocationEdit_->currentIndex()) {
case 0: location = LegendOutside; break;
case 1: location = LegendInside; break;
}
switch (legendSideEdit_->currentIndex()) {
case 0: side = Top; break;
case 1: side = Right; break;
case 2: side = Bottom; break;
case 3: side = Left; break;
}
if (side == Left || side == Right) {
if (legendAlignmentEdit_->currentIndex() < 3)
legendAlignmentEdit_->setCurrentIndex(4);
} else {
if (legendAlignmentEdit_->currentIndex() >= 3)
legendAlignmentEdit_->setCurrentIndex(2);
}
switch (legendAlignmentEdit_->currentIndex()) {
case 0: alignment = AlignLeft; break;
case 1: alignment = AlignCenter; break;
case 2: alignment = AlignRight; break;
case 3: alignment = AlignTop; break;
case 4: alignment = AlignMiddle; break;
case 5: alignment = AlignBottom; break;
}
chart_->setLegendLocation(location, side, alignment);
chart_->setLegendColumns((side == Top || side == Bottom ) ? 2 : 1,
WLength(100));
}
for (unsigned i = 0; i < 4; ++i) {
Side sides[] = { Top, Right, Bottom, Left };
bool legendRoom =
haveLegend
&& chart_->legendLocation() == LegendOutside
&& chart_->legendSide() == sides[i];
int padding;
if (i % 2 == 0)
padding = legendRoom ? 80 : 40;
else
padding = legendRoom ? 200 : 80;
chart_->setPlotAreaPadding(padding, sides[i]);
}
}
bool ChartConfig::validate(WFormWidget *w)
{
bool valid = w->validate() == WValidator::Valid;
if (!WApplication::instance()->environment().javaScript()) {
w->setStyleClass(valid ? "" : "Wt-invalid");
w->setToolTip(valid ? "" : "Invalid value");
}
return valid;
}
void ChartConfig::connectSignals(WFormWidget *w)
{
w->changed().connect(this, &ChartConfig::update);
if (dynamic_cast<WLineEdit *>(w))
w->enterPressed().connect(this, &ChartConfig::update);
}