|
//
|
|
// Created by mark on 5/30/25.
|
|
//
|
|
|
|
#include <any>
|
|
#include <string_view>
|
|
#include <utility>
|
|
#include <boost/proto/transform/env.hpp>
|
|
#include <ReportsAndCharts//WChartJSLibrary.h>
|
|
#include <Wt/EscapeOStream.h>
|
|
#include <Wt/WAbstractItemModel.h>
|
|
#include <Wt/WApplication.h>
|
|
#include <Wt/WFlags.h>
|
|
#include <Wt/WPainter.h>
|
|
#include <Wt/WWidget.h>
|
|
#include <Wt/Chart/WAbstractChartModel.h>
|
|
#include <Wt/Chart/WStandardChartProxyModel.h>
|
|
#include <Wt/Json/Serializer.h>
|
|
|
|
constexpr std::string_view WChartJSLibrary_js = R"wtjs(
|
|
window.WChartJSLibrary = window.WChartJSLibrary || function(APP, el, chart_config_str) {
|
|
const self = this;
|
|
el.chartJSObj = this;
|
|
this.chart = null;
|
|
this.widget = el;
|
|
|
|
this.canvas = document.createElement('canvas');
|
|
this.canvas.style.position = 'absolute';
|
|
this.canvas.style.top = '0';
|
|
this.canvas.style.left = '0';
|
|
this.canvas.style.width = '100%';
|
|
this.canvas.style.height = '100%';
|
|
el.appendChild(this.canvas);
|
|
|
|
this.handleResize = function() {
|
|
if (self.chart && self.canvas && self.canvas.parentNode) {
|
|
self.chart.resize();
|
|
}
|
|
};
|
|
|
|
this.addNonModelData = function(label_str, newData_str) {
|
|
const label = JSON.parse(label_str);
|
|
const newData = JSON.parse(newData_str);
|
|
self.chart.data.labels.push(label);
|
|
self.chart.data.datasets.forEach((dataset) => {
|
|
dataset.data.push(newData);
|
|
});
|
|
self.chart.update();
|
|
}
|
|
|
|
this.resetSpecificDataSeries = function(seriesNumber) {
|
|
var index = parseInt(seriesNumber, 10);
|
|
if (self.chart && self.chart.data && self.chart.data.datasets && self.chart.data.datasets[index]) {
|
|
self.chart.data.datasets[index].data = [];
|
|
self.chart.update();
|
|
} else {
|
|
console.warn("ChartJS tried to reset dataset at index " + index + ", but it is undefined.");
|
|
}
|
|
}
|
|
|
|
this.removeSpecificDataSeries = function(seriesNumber) {
|
|
self.chart.data.datasets.splice(seriesNumber, 1);
|
|
self.chart.update();
|
|
}
|
|
|
|
this.replaceSpecificDataSeries = function(seriesNumber, data_str) {
|
|
const newData = JSON.parse(data_str);
|
|
self.chart.data.datasets.splice(seriesNumber, 1);
|
|
self.chart.data.datasets.push(newData);
|
|
self.chart.update();
|
|
}
|
|
|
|
this.removeAllData = function() {
|
|
self.chart.data.labels.pop();
|
|
self.chart.data.datasets.forEach((dataset) => {
|
|
dataset.data.pop();
|
|
});
|
|
self.chart.update();
|
|
}
|
|
|
|
this.addModelData = function(data_str) {
|
|
const newData = JSON.parse(data_str);
|
|
self.chart.data.datasets.push(newData);
|
|
self.chart.update();
|
|
}
|
|
|
|
this.appendExistingDataSeries = function(seriesNumber, data_str) {
|
|
self.chart.options.animations.x = false;
|
|
const newData = JSON.parse(data_str);
|
|
const data = newData.data;
|
|
const targetDataset = self.chart.data.datasets[seriesNumber];
|
|
targetDataset.data = targetDataset.data.concat(data);
|
|
self.chart.data.labels = targetDataset.data.map((_, index) => index + 1);
|
|
self.chart.update();
|
|
}
|
|
|
|
this.updateChartTitle = function(title_str) {
|
|
self.chart.options.plugins.title.text = title_str;
|
|
self.chart.update();
|
|
}
|
|
|
|
this.updateConfigAsNewObject = function(config_str) {
|
|
const options = JSON.parse(config_str);
|
|
self.chart.options = options;
|
|
self.chart.update();
|
|
}
|
|
|
|
this.destroy = function() {
|
|
if (self.chart) self.chart.destroy();
|
|
}
|
|
|
|
this.init = function(chart_config_str) {
|
|
const chart_config = JSON.parse(chart_config_str);
|
|
const type = chart_config.type;
|
|
const data = chart_config.data || {};
|
|
|
|
if (data.labels === "datacount" && data.datasets && data.datasets.length > 0) {
|
|
data.labels = data.datasets[0].data.map((_, index) => index + 1);
|
|
}
|
|
|
|
const plugins = chart_config.plugins || {};
|
|
const options = chart_config.options || {};
|
|
|
|
options.onClick = function(event, activeElements, chart) {
|
|
if (activeElements && activeElements.length > 0) {
|
|
const firstPoint = activeElements[0];
|
|
Wt.emit(self.widget, 'chartClicked', firstPoint.datasetIndex, firstPoint.index);
|
|
}
|
|
};
|
|
|
|
if (options && options.scales && options.scales.x && options.scales.x.ticks && options.scales.x.ticks.callback === "toFixed") {
|
|
options.scales.x.ticks.callback = function(value, index, values) { return value.toFixed(0); };
|
|
}
|
|
|
|
if (options && options.plugins && options.plugins.tooltip && options.plugins.tooltip.callback === "percentage") {
|
|
options.plugins.tooltip.callbacks = {
|
|
label: function(context) {
|
|
let label = context.dataset.label || '';
|
|
if (label) {
|
|
label += ': ';
|
|
}
|
|
if (context.parsed.y !== null) {
|
|
label += (context.parsed.y * 100).toFixed(1) + '%';
|
|
}
|
|
return label;
|
|
}
|
|
};
|
|
}
|
|
|
|
self.chart = new Chart(self.canvas, {
|
|
type,
|
|
data,
|
|
plugins,
|
|
options
|
|
});
|
|
};
|
|
|
|
if (chart_config_str) this.init(chart_config_str);
|
|
};
|
|
)wtjs";
|
|
|
|
WChartJSLibrary::WChartJSLibrary(const ChartJSType chartType) : chartType_(chartType),
|
|
resized_(false),
|
|
jsDefined_(false),
|
|
chartClicked_(this, "chartClicked")
|
|
{
|
|
wApp->enableUpdates(true);
|
|
WWebWidget::setPositionScheme(Wt::PositionScheme::Relative);
|
|
|
|
setWidth(Wt::WLength(100, Wt::LengthUnit::Percentage));
|
|
// Must hard code the height or ChartJS resizing function will go into an infinite
|
|
// resizing loop with CSS Flexbox
|
|
setHeight(Wt::WLength(300, Wt::LengthUnit::Pixel));
|
|
chartTypeName_ = getChartJSType(chartType);
|
|
}
|
|
|
|
std::string WChartJSLibrary::getChartJSType(ChartJSType type) {
|
|
switch (chartType_) {
|
|
case ChartJSType::BAR: return "bar";
|
|
case ChartJSType::BUBBLE: return "bubble";
|
|
case ChartJSType::DOUGHNUT: return "doughnut";
|
|
case ChartJSType::PIE: return "pie";
|
|
case ChartJSType::LINE: return "line";
|
|
case ChartJSType::POLAR: return "polarArea";
|
|
case ChartJSType::RADAR: return "radar";
|
|
case ChartJSType::SCATTER: return "scatter";
|
|
}
|
|
|
|
return "line";
|
|
}
|
|
|
|
void WChartJSLibrary::updateChartTitleOnly(const Wt::WString& title) {
|
|
set(titleText_, title);
|
|
if (fullyRendered_) {
|
|
Wt::WStringStream jsStream; // Local stream
|
|
{ // Scope block for EscapeOStream
|
|
Wt::EscapeOStream es(jsStream);
|
|
es << "var o=" << jsRef() << ";if(o && o.chartJSObj){o.chartJSObj.updateChartTitle('";
|
|
es.pushEscape(Wt::EscapeOStream::JsStringLiteralSQuote);
|
|
es << titleText_.toUTF8();
|
|
es.popEscape();
|
|
es << "');};";
|
|
} // Forces flush
|
|
|
|
doJavaScript(jsStream.str());
|
|
}
|
|
}
|
|
|
|
void WChartJSLibrary::setModel(const std::shared_ptr<Wt::Chart::WAbstractChartModel>& model) {
|
|
if (model_ != nullptr) {
|
|
/* disconnect slots from previous model */
|
|
for (auto& modelConnection : modelConnections_)
|
|
modelConnection.disconnect();
|
|
modelConnections_.clear();
|
|
}
|
|
model_ = model;
|
|
modelConnections_.push_back(model_->changed().connect
|
|
(this, &WChartJSLibrary::modelChanged));
|
|
|
|
modelChanged();
|
|
}
|
|
|
|
void WChartJSLibrary::setModel(const std::shared_ptr<Wt::WAbstractItemModel>& model) {
|
|
standardModel_ = model;
|
|
setModel(std::shared_ptr<Wt::Chart::WAbstractChartModel>(
|
|
std::make_shared<Wt::Chart::WStandardChartProxyModel>(model)));
|
|
}
|
|
|
|
std::shared_ptr<Wt::WAbstractItemModel> WChartJSLibrary::itemModel() const {
|
|
const auto* proxy
|
|
= dynamic_cast<Wt::Chart::WStandardChartProxyModel*>(model_.get());
|
|
if (proxy)
|
|
return proxy->sourceModel();
|
|
else
|
|
return nullptr;
|
|
}
|
|
|
|
void WChartJSLibrary::modelChanged() {
|
|
// not sure what should be here. Will figure it out later.
|
|
// modelColumnsLoaded_.clear();
|
|
}
|
|
|
|
void WChartJSLibrary::updateModel() {
|
|
if (!modelColumnsLoaded_.empty()) {
|
|
// add test for multiple series. Right now, assume only one
|
|
Wt::Json::Object dataSeries;
|
|
Wt::Json::Array series;
|
|
int modelCount = static_cast<int>(modelColumnsLoaded_.size());
|
|
for (unsigned i = 0; i < modelCount; ++i) {
|
|
for (int j = 0; j < model_->rowCount(); ++j) {
|
|
series.push_back(model_->data(j, modelColumnsLoaded_[i].modelColumnNumber));
|
|
}
|
|
|
|
if (modelColumnsLoaded_[i].countForLabels) {
|
|
countForLabels_ = true;
|
|
dataSeries["labels"] = Wt::Json::Value("datacount");
|
|
} else {
|
|
countForLabels_ = false;
|
|
dataLabels_.clear();
|
|
int numRows = standardModel_->rowCount();
|
|
for (int j = 0; j < numRows; ++j) {
|
|
auto headerData = std::any_cast<std::string>(
|
|
standardModel_->headerData(j, Wt::Orientation::Vertical, Wt::ItemDataRole::Display));
|
|
dataLabels_.push_back(Wt::Json::Value(headerData));
|
|
}
|
|
dataSeries["labels"] = Wt::Json::Value(dataLabels_);
|
|
}
|
|
|
|
dataSeries["data"] = series;
|
|
std::string output = Wt::Json::serialize(dataSeries);
|
|
|
|
Wt::WStringStream jsStream; // Local stream
|
|
{ // Scope block for EscapeOStream
|
|
Wt::EscapeOStream es(jsStream);
|
|
es << "var o=" << jsRef() << ";if(o && o.chartJSObj){o.chartJSObj.replaceSpecificDataSeries('" << i << "','";
|
|
es.pushEscape(Wt::EscapeOStream::JsStringLiteralSQuote);
|
|
es << output;
|
|
es.popEscape();
|
|
es << "');};";
|
|
} // Forces flush
|
|
|
|
doJavaScript(jsStream.str());
|
|
|
|
// if there are multiple series, this javascript function will delete this particular series
|
|
// and replace it with the newly created series by pushing it onto the end of datasets. To
|
|
// remain in sync, the same needs to be done with the modelColumnsLoaded_.
|
|
ModelColumnInfo modelColumnInfo = modelColumnsLoaded_[i];
|
|
modelColumnsLoaded_.erase(modelColumnsLoaded_.begin() + i);
|
|
modelColumnsLoaded_.push_back(modelColumnInfo);
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
std::pair<int, int> WChartJSLibrary::getModelCoordinates(int datasetIndex, int dataIndex) const {
|
|
// 1. Bounds checking for safety against rogue JS events
|
|
if (datasetIndex < 0 || datasetIndex >= static_cast<int>(modelColumnsLoaded_.size())) {
|
|
return {-1, -1};
|
|
}
|
|
|
|
// 2. The dataIndex from Chart.js maps 1:1 to the rows you iterated over in addSeries
|
|
int modelRow = dataIndex;
|
|
|
|
// 3. Look up the exact C++ model column from your tracking struct
|
|
int modelColumn = modelColumnsLoaded_[datasetIndex].modelColumnNumber;
|
|
|
|
return {modelRow, modelColumn};
|
|
}
|
|
|
|
// addSeries needs a modelColumn and a fully populated Wt::Json::Object called ChartJSDataSeries
|
|
void WChartJSLibrary::addSeries(int modelColumn, bool countForLabels, Wt::Json::Object dataSeriesOptions,
|
|
bool dependantIndependantSeries) {
|
|
// if series_[modelColumn] exists, throw an error otherwise, set value.
|
|
int labelCol = dependantIndependantSeries ? 1 : modelColumn;
|
|
//concatenate the data series title and the actual data and add it to the series_ std::map
|
|
Wt::Json::Object dataSeries = std::move(dataSeriesOptions);
|
|
Wt::Json::Array series;
|
|
// If the Chart type is bubble, the data should be constructed as [{x: 0, y: 0, r: 0}]
|
|
if ((chartType_ != ChartJSType::BUBBLE) && !dependantIndependantSeries) {
|
|
for (int i = 0; i < model_->rowCount(); ++i) {
|
|
series.push_back(model_->data(i, modelColumn));
|
|
}
|
|
}
|
|
|
|
if (dependantIndependantSeries) {
|
|
for (int i = 0; i < model_->rowCount(); ++i) {
|
|
series.push_back(model_->data(i, 1));
|
|
}
|
|
}
|
|
|
|
// set the labels for contiguous data points when there are no row headers set
|
|
countForLabels_ = countForLabels;
|
|
// and the data is numeric
|
|
dataSeries["data"] = series;
|
|
|
|
auto headerData = Wt::asString(standardModel_->headerData(labelCol, Wt::Orientation::Horizontal, Wt::ItemDataRole::Display)).toUTF8();
|
|
dataSeries["label"] = Wt::Json::Value(headerData);
|
|
|
|
if (countForLabels_) {
|
|
dataSeries["label"] = model_->headerData(modelColumn);
|
|
} else {
|
|
dataLabels_.clear();
|
|
int numRows = standardModel_->rowCount();
|
|
for (int i = 0; i < numRows; ++i) {
|
|
// Extract the raw, unformatted data from the Edit role
|
|
auto editData = standardModel_->data(i, 0, Wt::ItemDataRole::Edit);
|
|
|
|
// Safely unpack the std::any container based on its true C++ type
|
|
if (editData.type() == typeid(double)) {
|
|
dataLabels_.push_back(Wt::Json::Value(std::any_cast<double>(editData)));
|
|
} else if (editData.type() == typeid(int)) {
|
|
dataLabels_.push_back(Wt::Json::Value(std::any_cast<int>(editData)));
|
|
} else if (editData.type() == typeid(long long)) {
|
|
dataLabels_.push_back(Wt::Json::Value(static_cast<double>(std::any_cast<long long>(editData))));
|
|
} else {
|
|
// Fallback for categorical strings and dates
|
|
auto labelStr = Wt::asString(editData).toUTF8();
|
|
dataLabels_.push_back(Wt::Json::Value(labelStr));
|
|
}
|
|
}
|
|
}
|
|
|
|
Wt::Json::Object additionalSeries;
|
|
if (fullyRendered_) additionalSeries = Wt::Json::Value(dataSeries);
|
|
ModelColumnInfo colInfo;
|
|
colInfo.countForLabels = countForLabels_;
|
|
colInfo.dependantIndependantSeries = dependantIndependantSeries;
|
|
colInfo.modelColumnNumber = modelColumn;
|
|
colInfo.lastDataCount = model_->rowCount();
|
|
modelColumnsLoaded_.push_back(colInfo);
|
|
series_.push_back(dataSeries);
|
|
assert(modelColumnsLoaded_.size() == series_.size());
|
|
// If the chart is already rendered, we need to add the net change to the
|
|
// chart. Otherwise, the prior data will get picked up on the full render.
|
|
|
|
if (fullyRendered_) {
|
|
std::string output = Wt::Json::serialize(additionalSeries);
|
|
Wt::WStringStream jsStream; // Local stream
|
|
|
|
{ // Scope block for EscapeOStream
|
|
Wt::EscapeOStream es(jsStream);
|
|
es << "var o=" << jsRef() << ";if(o && o.chartJSObj){o.chartJSObj.addModelData('";
|
|
es.pushEscape(Wt::EscapeOStream::JsStringLiteralSQuote);
|
|
es << output;
|
|
es.popEscape();
|
|
es << "');};";
|
|
} // Forces flush
|
|
|
|
doJavaScript(jsStream.str()); // Execute directly instead of relying on render()
|
|
}
|
|
scheduleRender();
|
|
}
|
|
|
|
void WChartJSLibrary::addRawSeries(const Wt::Json::Object& rawSeries) {
|
|
series_.push_back(rawSeries);
|
|
if (fullyRendered_) {
|
|
std::string output = Wt::Json::serialize(rawSeries);
|
|
Wt::WStringStream jsStream;
|
|
{
|
|
Wt::EscapeOStream es(jsStream);
|
|
es << "var o=" << jsRef() << ";if(o && o.chartJSObj){o.chartJSObj.addModelData('";
|
|
es.pushEscape(Wt::EscapeOStream::JsStringLiteralSQuote);
|
|
es << output;
|
|
es.popEscape();
|
|
es << "');};";
|
|
}
|
|
doJavaScript(jsStream.str());
|
|
}
|
|
scheduleRender();
|
|
}
|
|
|
|
void WChartJSLibrary::updateExistingSeries() {
|
|
if (!modelColumnsLoaded_.empty()) {
|
|
// add test for multiple series. Right now, assume only one
|
|
Wt::Json::Object dataSeries;
|
|
Wt::Json::Array series;
|
|
int modelCount = static_cast<int>(modelColumnsLoaded_.size());
|
|
for (int i = 0; i < modelCount; ++i) {
|
|
int netChangeRows = (int)model_->rowCount() - modelColumnsLoaded_[i].lastDataCount;
|
|
if (netChangeRows < 0) {
|
|
modelColumnsLoaded_[i].lastDataCount = 0;
|
|
resetSeries(modelColumnsLoaded_[i].modelColumnNumber);
|
|
netChangeRows = (int)model_->rowCount();
|
|
}
|
|
if (netChangeRows == 0) continue;
|
|
int recordsAdded = 0;
|
|
for (int j = modelColumnsLoaded_[i].lastDataCount; j < model_->rowCount(); j++) {
|
|
series.push_back(model_->data(j, modelColumnsLoaded_[i].modelColumnNumber));
|
|
recordsAdded++;
|
|
}
|
|
modelColumnsLoaded_[i].lastDataCount += recordsAdded;
|
|
|
|
if (modelColumnsLoaded_[i].countForLabels) {
|
|
countForLabels_ = true;
|
|
dataSeries["labels"] = Wt::Json::Value("datacount");
|
|
} else {
|
|
countForLabels_ = false;
|
|
dataLabels_.clear();
|
|
int numRows = standardModel_->rowCount();
|
|
for (int i = 0; i < numRows; ++i) {
|
|
auto labelStr = Wt::asString(standardModel_->data(i, 0, Wt::ItemDataRole::Display)).toUTF8();
|
|
dataLabels_.push_back(Wt::Json::Value(labelStr));
|
|
}
|
|
dataSeries["labels"] = Wt::Json::Value(dataLabels_);
|
|
}
|
|
|
|
dataSeries["data"] = series;
|
|
std::string output = Wt::Json::serialize(dataSeries);
|
|
|
|
Wt::WStringStream jsStream; // Local stream
|
|
{ // Scope block for EscapeOStream
|
|
Wt::EscapeOStream es(jsStream);
|
|
es << "var o=" << jsRef() << ";if(o && o.chartJSObj){o.chartJSObj.appendExistingDataSeries(" << i << ",'";
|
|
es.pushEscape(Wt::EscapeOStream::JsStringLiteralSQuote);
|
|
es << output;
|
|
es.popEscape();
|
|
es << "');};";
|
|
} // Forces flush
|
|
|
|
doJavaScript(jsStream.str());
|
|
}
|
|
}
|
|
}
|
|
|
|
void WChartJSLibrary::removeSeries(int modelColumn) {
|
|
Wt::WStringStream jsStream; // Local stream
|
|
{
|
|
Wt::EscapeOStream es(jsStream);
|
|
if (!modelColumnsLoaded_.empty()) {
|
|
|
|
// CRITICAL: Iterate backwards so erasing items don't corrupt the loop index!
|
|
for (int j = modelColumnsLoaded_.size() - 1; j >= 0; --j) {
|
|
if (modelColumnsLoaded_[j].modelColumnNumber == modelColumn) {
|
|
|
|
// 1. Tell JS to completely destroy the dataset
|
|
es << "var o=" << jsRef() << ";if(o && o.chartJSObj){"
|
|
"if(o.chartJSObj.chart.data.datasets[" << j << "]) {"
|
|
" o.chartJSObj.chart.data.datasets.splice(" << j << ", 1);"
|
|
" o.chartJSObj.chart.update();"
|
|
"}"
|
|
"};";
|
|
|
|
// 2. Remove it from the C++ tracker so it stops stacking up infinitely
|
|
series_.erase(series_.begin() + j);
|
|
modelColumnsLoaded_.erase(modelColumnsLoaded_.begin() + j);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
doJavaScript(jsStream.str());
|
|
}
|
|
|
|
void WChartJSLibrary::removeLastRawSeries() {
|
|
if (series_.empty()) return;
|
|
|
|
// Grab the index of the last dataset (which will be our residuals)
|
|
int lastIndex = series_.size() - 1;
|
|
|
|
// Remove from the C++ tracker so it doesn't infinitely stack on redraws
|
|
series_.erase(series_.begin() + lastIndex);
|
|
|
|
// Call your existing JavaScript function to splice it out of ChartJS
|
|
Wt::WStringStream jsStream;
|
|
{
|
|
Wt::EscapeOStream es(jsStream);
|
|
es << "var o=" << jsRef() << ";if(o && o.chartJSObj){"
|
|
<< "o.chartJSObj.removeSpecificDataSeries(" << lastIndex << ");"
|
|
<< "};";
|
|
}
|
|
doJavaScript(jsStream.str());
|
|
}
|
|
|
|
|
|
void WChartJSLibrary::resetSeries(int modelColumn) {
|
|
Wt::WStringStream jsStream;
|
|
{
|
|
Wt::EscapeOStream es(jsStream);
|
|
if (!modelColumnsLoaded_.empty()) {
|
|
for (int j = 0; j < modelColumnsLoaded_.size(); ++j) {
|
|
if (modelColumnsLoaded_[j].modelColumnNumber == modelColumn) {
|
|
// FIX: Removed the single quotes around the j variable
|
|
es << "var o=" << jsRef() << ";if(o && o.chartJSObj){"
|
|
"o.chartJSObj.resetSpecificDataSeries(" << j << ");"
|
|
"};";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
doJavaScript(jsStream.str());
|
|
}
|
|
|
|
void WChartJSLibrary::setXaxisLabels(const std::vector<std::string>& labels) {
|
|
dataLabels_.clear();
|
|
for (const auto& label : labels) {
|
|
dataLabels_.push_back(Wt::Json::Value(label));
|
|
}
|
|
countForLabels_ = false;
|
|
}
|
|
|
|
void WChartJSLibrary::setOptions(const Wt::Json::Object& chartOptions) {
|
|
chartOptions_ = chartOptions;
|
|
}
|
|
|
|
void WChartJSLibrary::defineJavaScript() {
|
|
Wt::WApplication* app = Wt::WApplication::instance();
|
|
|
|
// 1. Ensure external libraries load
|
|
app->require("https://cdn.jsdelivr.net/npm/chart.js");
|
|
app->require("https://cdn.jsdelivr.net/npm/perfect-scrollbar@1.5.6/dist/perfect-scrollbar.min.js");
|
|
app->require("https://cdn.jsdelivr.net/npm/smooth-scrollbar@8.8.4/dist/smooth-scrollbar.min.js");
|
|
app->require("https://cdn.jsdelivr.net/npm/hammerjs@2.0.8");
|
|
app->require("https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.2.0/dist/chartjs-plugin-zoom.min.js");
|
|
|
|
std::string output = Wt::Json::serialize(chartjsConfigObj);
|
|
Wt::WStringStream jsStream;
|
|
|
|
// 2. Output the class definition securely into the stream
|
|
jsStream << std::string(WChartJSLibrary_js) << "\n";
|
|
|
|
// 3. Instantiate the chart using the newly defined class
|
|
{
|
|
Wt::EscapeOStream es(jsStream);
|
|
es << "var el = " << jsRef() << ";"
|
|
<< "if (el) {"
|
|
<< " el.chartJSObj = new window.WChartJSLibrary(Wt, el, '";
|
|
es.pushEscape(Wt::EscapeOStream::JsStringLiteralSQuote);
|
|
es << output;
|
|
es.popEscape();
|
|
es << "');"
|
|
<< "}";
|
|
}
|
|
|
|
// 4. Force Wt to evaluate everything synchronously in the widget queue
|
|
doJavaScript(jsStream.str());
|
|
|
|
jsDefined_ = true;
|
|
scheduleRender();
|
|
}
|
|
|
|
void WChartJSLibrary::render(Wt::WFlags<Wt::RenderFlag> flags) {
|
|
if (flags.test(Wt::RenderFlag::Full) || !jsDefined_) {
|
|
defineJavaScript();
|
|
fullyRendered_ = true;
|
|
}
|
|
|
|
Wt::WContainerWidget::render(flags);
|
|
}
|
|
|
|
void WChartJSLibrary::layoutSizeChanged(int width, int height) {
|
|
Wt::WContainerWidget::layoutSizeChanged(width, height);
|
|
if (jsDefined_ && fullyRendered_) {
|
|
// C++20 formatting for string concatenation
|
|
doJavaScript(std::format("var o={}; if(o && o.chartJSObj) o.chartJSObj.handleResize();", jsRef()));
|
|
}
|
|
}
|
|
|
|
WChartJSLibrary::~WChartJSLibrary() {
|
|
if (jsDefined_) {
|
|
// Clean up Chart.js to prevent memory leaks in the browser
|
|
Wt::WWebWidget::doJavaScript(std::format("var o={}; if(o && o.chartJSObj) o.chartJSObj.destroy();", jsRef()));
|
|
}
|
|
}
|
|
|
|
void WChartJSLibrary::buildAndShowChart() {
|
|
chartjsConfigObj["type"] = Wt::Json::Value(chartTypeName_);
|
|
|
|
if (countForLabels_) {
|
|
chartData_["labels"] = Wt::Json::Value("datacount");
|
|
countForLabels_ = false;
|
|
} else {
|
|
chartData_["labels"] = Wt::Json::Value(dataLabels_);
|
|
}
|
|
|
|
chartData_["datasets"] = Wt::Json::Value(series_);
|
|
chartjsConfigObj["data"] = Wt::Json::Value(chartData_);
|
|
|
|
if (!chartOptions_.empty()) {
|
|
chartjsConfigObj["options"] = Wt::Json::Value(chartOptions_);
|
|
} else {
|
|
// FIX: Use an empty Json::Object instead of the string "{},"
|
|
// which breaks JSON parsing in JS
|
|
chartjsConfigObj["options"] = Wt::Json::Object{};
|
|
}
|
|
|
|
if (!plugins_.empty()) {
|
|
chartjsConfigObj["plugins"] = Wt::Json::Value(plugins_);
|
|
}
|
|
|
|
// NEW: If the chart is already on screen, forcefully sync the whole thing
|
|
// to pick up any new Date boundaries from the Redraw button.
|
|
if (fullyRendered_) {
|
|
std::string output = Wt::Json::serialize(chartjsConfigObj);
|
|
Wt::WStringStream jsStream;
|
|
{
|
|
Wt::EscapeOStream es(jsStream);
|
|
es << "var o=" << jsRef() << ";if(o && o.chartJSObj){"
|
|
<< "o.chartJSObj.destroy();"
|
|
<< "o.chartJSObj.init('";
|
|
es.pushEscape(Wt::EscapeOStream::JsStringLiteralSQuote);
|
|
es << output;
|
|
es.popEscape();
|
|
es << "');};";
|
|
}
|
|
doJavaScript(jsStream.str());
|
|
}
|
|
|
|
scheduleRender();
|
|
}
|