mirror of
https://github.com/UpsilonNumworks/Upsilon.git
synced 2026-01-18 16:27:34 +01:00
408 lines
16 KiB
C++
408 lines
16 KiB
C++
#include "graph_controller.h"
|
|
#include "../shared/poincare_helpers.h"
|
|
#include "../shared/text_helpers.h"
|
|
#include "../apps_container.h"
|
|
#include "../shared/poincare_helpers.h"
|
|
#include <poincare/preferences.h>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
|
|
using namespace Poincare;
|
|
using namespace Shared;
|
|
|
|
namespace Regression {
|
|
|
|
GraphController::GraphController(Responder * parentResponder, InputEventHandlerDelegate * inputEventHandlerDelegate, ButtonRowController * header, Store * store, CurveViewCursor * cursor, uint32_t * rangeVersion, int * selectedDotIndex, int * selectedSeriesIndex) :
|
|
InteractiveCurveViewController(parentResponder, inputEventHandlerDelegate, header, store, &m_view, cursor, rangeVersion),
|
|
m_crossCursorView(),
|
|
m_roundCursorView(),
|
|
m_bannerView(this, inputEventHandlerDelegate, this),
|
|
m_view(store, m_cursor, &m_bannerView, &m_crossCursorView),
|
|
m_store(store),
|
|
m_graphOptionsController(this, inputEventHandlerDelegate, m_store, m_cursor, this),
|
|
m_selectedDotIndex(selectedDotIndex),
|
|
m_selectedSeriesIndex(selectedSeriesIndex)
|
|
{
|
|
for (int i = 0; i < Store::k_numberOfSeries; i++) {
|
|
m_modelType[i] = (Model::Type) -1;
|
|
}
|
|
m_store->setDelegate(this);
|
|
}
|
|
|
|
bool GraphController::isEmpty() const {
|
|
return m_store->isEmpty();
|
|
}
|
|
|
|
I18n::Message GraphController::emptyMessage() {
|
|
return I18n::Message::NoDataToPlot;
|
|
}
|
|
|
|
void GraphController::viewWillAppear() {
|
|
#ifdef GRAPH_CURSOR_SPEEDUP
|
|
m_roundCursorView.resetMemoization();
|
|
#endif
|
|
/* At this point, some series might have been removed from the model. We need
|
|
* to reinitialize the selected series index if the current selection is
|
|
* either null (right after construction) or refering a removed series. */
|
|
if (*m_selectedSeriesIndex < 0 || m_store->seriesIsEmpty(*m_selectedSeriesIndex)) {
|
|
*m_selectedSeriesIndex = m_store->indexOfKthNonEmptySeries(0);
|
|
}
|
|
|
|
/* Both the GraphController and the Store hold the Model::Type of each
|
|
* series. The values differ in two cases:
|
|
* 1) the very first time the graph view appears
|
|
* 2) when the user selects another Model::Type for a series.
|
|
* where we decide to place the cursor at a default position. */
|
|
if (m_modelType[*m_selectedSeriesIndex] != m_store->seriesRegressionType(*m_selectedSeriesIndex)) {
|
|
initCursorParameters();
|
|
}
|
|
|
|
/* Equalize the Model::Type of each series between the GraphController and
|
|
* the Store.
|
|
* TODO In passing, one may probably avoid keeping the Model::Type of each
|
|
* series in two places:
|
|
* 1) call initCursorParameters elsewhere the very first time the graph view
|
|
* appears,
|
|
* 2) take into account the Model::Type in the computation of the
|
|
* storeChecksum in order to detect any change in the series and in
|
|
* their model types. */
|
|
for (int i = 0; i < Store::k_numberOfSeries; i++) {
|
|
m_modelType[i] = m_store->seriesRegressionType(*m_selectedSeriesIndex);
|
|
}
|
|
|
|
/* The following
|
|
* - calls initCursorParameters() if necessary,
|
|
* - reloads the bannerView and the curveView. */
|
|
InteractiveCurveViewController::viewWillAppear();
|
|
|
|
/* Since *m_selectedDotIndex is altered by initCursorParameters(),
|
|
* the following must absolutely come at the end. */
|
|
setRoundCrossCursorView();
|
|
}
|
|
|
|
// Private
|
|
|
|
Poincare::Context * GraphController::globalContext() {
|
|
return AppsContainer::sharedAppsContainer()->globalContext();
|
|
}
|
|
|
|
// SimpleInteractiveCurveViewController
|
|
|
|
void GraphController::reloadBannerView() {
|
|
// Set point equals: "P(...) ="
|
|
constexpr size_t bufferSize = k_maxNumberOfCharacters + PrintFloat::charSizeForFloatsWithPrecision(Preferences::LargeNumberOfSignificantDigits);
|
|
char buffer[bufferSize];
|
|
int numberOfChar = 0;
|
|
const char * legend = " P(";
|
|
numberOfChar += strlcpy(buffer, legend, bufferSize);
|
|
if (*m_selectedDotIndex == m_store->numberOfPairsOfSeries(*m_selectedSeriesIndex)) {
|
|
legend = I18n::translate(I18n::Message::MeanDot);
|
|
assert(numberOfChar <= bufferSize);
|
|
numberOfChar += strlcpy(buffer + numberOfChar, legend, bufferSize - numberOfChar);
|
|
} else if (*m_selectedDotIndex < 0) {
|
|
legend = I18n::translate(I18n::Message::Reg);
|
|
assert(numberOfChar <= bufferSize);
|
|
numberOfChar += strlcpy(buffer + numberOfChar, legend, bufferSize - numberOfChar);
|
|
} else {
|
|
numberOfChar += PoincareHelpers::ConvertFloatToTextWithDisplayMode<float>(std::round((float)*m_selectedDotIndex+1.0f), buffer + numberOfChar, bufferSize - numberOfChar, Preferences::ShortNumberOfSignificantDigits, Preferences::PrintFloatMode::Decimal);
|
|
}
|
|
legend = ") ";
|
|
assert(numberOfChar <= bufferSize);
|
|
strlcpy(buffer + numberOfChar, legend, bufferSize - numberOfChar);
|
|
m_bannerView.dotNameView()->setText(buffer);
|
|
|
|
// Set "x=..." or "xmean=..."
|
|
legend = "x=";
|
|
double x = m_cursor->x();
|
|
// Display a specific legend if the mean dot is selected
|
|
if (*m_selectedDotIndex == m_store->numberOfPairsOfSeries(*m_selectedSeriesIndex)) {
|
|
// \xCC\x85 represents the combining overline ' ̅'
|
|
legend = "x\xCC\x85=";
|
|
x = m_store->meanOfColumn(*m_selectedSeriesIndex, 0);
|
|
}
|
|
m_bannerView.abscissaSymbol()->setText(legend);
|
|
|
|
numberOfChar = PoincareHelpers::ConvertFloatToText<double>(x, buffer, bufferSize, Preferences::MediumNumberOfSignificantDigits);
|
|
// Padding
|
|
Shared::TextHelpers::PadWithSpaces(buffer, bufferSize, &numberOfChar, k_maxLegendLength);
|
|
m_bannerView.abscissaValue()->setText(buffer);
|
|
|
|
// Set "y=..." or "ymean=..."
|
|
numberOfChar = 0;
|
|
legend = "y=";
|
|
double y = m_cursor->y();
|
|
if (*m_selectedDotIndex == m_store->numberOfPairsOfSeries(*m_selectedSeriesIndex)) {
|
|
// \xCC\x85 represents the combining overline ' ̅'
|
|
legend = "y\xCC\x85=";
|
|
y = m_store->meanOfColumn(*m_selectedSeriesIndex, 1);
|
|
}
|
|
numberOfChar += strlcpy(buffer, legend, bufferSize);
|
|
numberOfChar += PoincareHelpers::ConvertFloatToText<double>(y, buffer + numberOfChar, bufferSize - numberOfChar, Preferences::MediumNumberOfSignificantDigits);
|
|
// Padding
|
|
Shared::TextHelpers::PadWithSpaces(buffer, bufferSize, &numberOfChar, k_maxLegendLength);
|
|
m_bannerView.ordinateView()->setText(buffer);
|
|
|
|
// Set formula
|
|
Model * model = m_store->modelForSeries(*m_selectedSeriesIndex);
|
|
I18n::Message formula = model->formulaMessage();
|
|
m_bannerView.regressionTypeView()->setMessage(formula);
|
|
|
|
// Get the coefficients
|
|
double * coefficients = m_store->coefficientsForSeries(*m_selectedSeriesIndex, globalContext());
|
|
bool coefficientsAreDefined = true;
|
|
for (int i = 0; i < model->numberOfCoefficients(); i++) {
|
|
if (std::isnan(coefficients[i])) {
|
|
coefficientsAreDefined = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!coefficientsAreDefined) {
|
|
// Force the "Data not suitable" message to be on the next line
|
|
int numberOfCharToCompleteLine = std::max<int>(Ion::Display::Width / BannerView::Font()->glyphSize().width() - strlen(I18n::translate(formula)), 0);
|
|
numberOfChar = 0;
|
|
// Padding
|
|
Shared::TextHelpers::PadWithSpaces(buffer, bufferSize, &numberOfChar, numberOfCharToCompleteLine - 1);
|
|
m_bannerView.subTextAtIndex(0)->setText(buffer);
|
|
|
|
const char * dataNotSuitableMessage = I18n::translate(I18n::Message::DataNotSuitableForRegression);
|
|
m_bannerView.subTextAtIndex(1)->setText(const_cast<char *>(dataNotSuitableMessage));
|
|
for (int i = 2; i < m_bannerView.numberOfsubTexts(); i++) {
|
|
m_bannerView.subTextAtIndex(i)->setText("");
|
|
}
|
|
m_bannerView.reload();
|
|
return;
|
|
}
|
|
char coefficientName = 'a';
|
|
for (int i = 0; i < model->numberOfCoefficients(); i++) {
|
|
numberOfChar = 0;
|
|
char leg[] = {' ', coefficientName, '=', 0};
|
|
legend = leg;
|
|
numberOfChar += strlcpy(buffer, legend, bufferSize);
|
|
numberOfChar += PoincareHelpers::ConvertFloatToText<double>(coefficients[i], buffer + numberOfChar, bufferSize - numberOfChar, Preferences::LargeNumberOfSignificantDigits);
|
|
m_bannerView.subTextAtIndex(i)->setText(buffer);
|
|
coefficientName++;
|
|
}
|
|
|
|
if (m_store->seriesRegressionType(*m_selectedSeriesIndex) == Model::Type::Linear) {
|
|
int index = model->numberOfCoefficients();
|
|
// Set "r=..."
|
|
numberOfChar = 0;
|
|
legend = " r=";
|
|
double r = m_store->correlationCoefficient(*m_selectedSeriesIndex);
|
|
numberOfChar += strlcpy(buffer, legend, bufferSize);
|
|
numberOfChar += PoincareHelpers::ConvertFloatToText<double>(r, buffer + numberOfChar, bufferSize - numberOfChar, Preferences::LargeNumberOfSignificantDigits);
|
|
m_bannerView.subTextAtIndex(0+index)->setText(buffer);
|
|
|
|
// Set "r2=..."
|
|
numberOfChar = 0;
|
|
legend = " r2=";
|
|
double r2 = m_store->determinationCoefficientForSeries(*m_selectedSeriesIndex, globalContext());
|
|
numberOfChar += strlcpy(buffer, legend, bufferSize);
|
|
numberOfChar += PoincareHelpers::ConvertFloatToText<double>(r2, buffer + numberOfChar, bufferSize - numberOfChar, Preferences::LargeNumberOfSignificantDigits);
|
|
m_bannerView.subTextAtIndex(1+index)->setText(buffer);
|
|
|
|
// Clean the last subview
|
|
buffer[0] = 0;
|
|
m_bannerView.subTextAtIndex(2+index)->setText(buffer);
|
|
|
|
} else {
|
|
// Empty all non used subviews
|
|
for (int i = model->numberOfCoefficients(); i < m_bannerView.numberOfsubTexts(); i++) {
|
|
buffer[0] = 0;
|
|
m_bannerView.subTextAtIndex(i)->setText(buffer);
|
|
}
|
|
}
|
|
m_bannerView.reload();
|
|
}
|
|
|
|
bool GraphController::moveCursorHorizontally(int direction, int scrollSpeed) {
|
|
double x;
|
|
double y;
|
|
if (*m_selectedDotIndex >= 0) {
|
|
int dotSelected = m_store->nextDot(*m_selectedSeriesIndex, direction, *m_selectedDotIndex);
|
|
if (dotSelected >= 0 && dotSelected < m_store->numberOfPairsOfSeries(*m_selectedSeriesIndex)) {
|
|
x = m_store->get(*m_selectedSeriesIndex, 0, dotSelected);
|
|
y = m_store->get(*m_selectedSeriesIndex, 1, dotSelected);
|
|
} else if (dotSelected == m_store->numberOfPairsOfSeries(*m_selectedSeriesIndex)) {
|
|
x = m_store->meanOfColumn(*m_selectedSeriesIndex, 0);
|
|
y = m_store->meanOfColumn(*m_selectedSeriesIndex, 1);
|
|
} else {
|
|
return false;
|
|
}
|
|
*m_selectedDotIndex = dotSelected;
|
|
} else {
|
|
double step = direction * scrollSpeed * m_store->xGridUnit()/k_numberOfCursorStepsInGradUnit;
|
|
x = m_cursor->x() + step;
|
|
y = yValue(*m_selectedSeriesIndex, x, globalContext());
|
|
}
|
|
m_cursor->moveTo(x, x, y);
|
|
return true;
|
|
}
|
|
|
|
InteractiveCurveViewRange * GraphController::interactiveCurveViewRange() {
|
|
return m_store;
|
|
}
|
|
|
|
CurveView * GraphController::curveView() {
|
|
return &m_view;
|
|
}
|
|
|
|
bool GraphController::handleEnter() {
|
|
stackController()->push(&m_graphOptionsController);
|
|
return true;
|
|
}
|
|
|
|
// InteractiveCurveViewController
|
|
void GraphController::initCursorParameters() {
|
|
double x = m_store->meanOfColumn(*m_selectedSeriesIndex, 0);
|
|
double y = m_store->meanOfColumn(*m_selectedSeriesIndex, 1);
|
|
m_cursor->moveTo(x, x, y);
|
|
*m_selectedDotIndex = m_store->numberOfPairsOfSeries(*m_selectedSeriesIndex);
|
|
}
|
|
|
|
bool GraphController::cursorMatchesModel() {
|
|
if (m_store->seriesIsEmpty(*m_selectedSeriesIndex)) {
|
|
return false;
|
|
}
|
|
Coordinate2D<double> xy;
|
|
if (*m_selectedDotIndex == -1) {
|
|
xy = xyValues(*m_selectedSeriesIndex, m_cursor->t(), globalContext());
|
|
} else if (*m_selectedDotIndex == m_store->numberOfPairsOfSeries(*m_selectedSeriesIndex)) {
|
|
xy = Coordinate2D<double>(m_store->meanOfColumn(*m_selectedSeriesIndex, 0), m_store->meanOfColumn(*m_selectedSeriesIndex, 1));
|
|
} else if (*m_selectedDotIndex > m_store->numberOfPairsOfSeries(*m_selectedSeriesIndex)) {
|
|
return false;
|
|
} else {
|
|
xy = Coordinate2D<double>(m_store->get(*m_selectedSeriesIndex, 0, *m_selectedDotIndex), m_store->get(*m_selectedSeriesIndex, 1, *m_selectedDotIndex));
|
|
}
|
|
return PoincareHelpers::equalOrBothNan(xy.x1(), m_cursor->x()) && PoincareHelpers::equalOrBothNan(xy.x2(), m_cursor->y());
|
|
}
|
|
|
|
bool GraphController::moveCursorVertically(int direction) {
|
|
Poincare::Context * context = globalContext();
|
|
double x = m_cursor->x();
|
|
double y = m_cursor->y();
|
|
|
|
// Find the closest regression
|
|
int selectedRegressionIndex = *m_selectedDotIndex == -1 ? *m_selectedSeriesIndex : -1;
|
|
int closestRegressionSeries = closestCurveIndexVertically(direction > 0, selectedRegressionIndex, context);
|
|
|
|
// Find the closest dot
|
|
int closestDotSeries = -1;
|
|
int dotSelected = m_store->closestVerticalDot(direction, x, y, *m_selectedSeriesIndex, *m_selectedDotIndex, &closestDotSeries, context);
|
|
|
|
// Choose between selecting the regression or the dot
|
|
bool validRegression = closestRegressionSeries > -1;
|
|
bool validDot = dotSelected >= 0 && dotSelected <= m_store->numberOfPairsOfSeries(closestDotSeries);
|
|
if (validRegression && validDot) {
|
|
/* Compare the abscissa distances to select either the dot or the
|
|
* regression. If they are equal, compare the ordinate distances. */
|
|
double dotDistanceX = -1;
|
|
if (dotSelected == m_store->numberOfPairsOfSeries(closestDotSeries)) {
|
|
dotDistanceX = std::fabs(m_store->meanOfColumn(closestDotSeries, 0) - x);
|
|
} else {
|
|
dotDistanceX = std::fabs(m_store->get(closestDotSeries, 0, dotSelected) - x);
|
|
}
|
|
if (dotDistanceX != 0) {
|
|
/* The regression X distance to the point is 0, so it is closer than the
|
|
* dot. */
|
|
validDot = false;
|
|
} else {
|
|
// Compare the y distances
|
|
double regressionDistanceY = std::fabs(yValue(closestRegressionSeries, x, context) - y);
|
|
double dotDistanceY = (dotSelected == m_store->numberOfPairsOfSeries(closestDotSeries)) ?
|
|
std::fabs(m_store->meanOfColumn(closestDotSeries, 1) - y) :
|
|
std::fabs(m_store->get(closestDotSeries, 1, dotSelected) - y);
|
|
if (regressionDistanceY <= dotDistanceY) {
|
|
validDot = false;
|
|
} else {
|
|
validRegression = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
assert(!validDot || !validRegression);
|
|
|
|
/* The model should be up to date before setting the cursor view. */
|
|
|
|
if (validRegression) {
|
|
// Select the regression
|
|
*m_selectedSeriesIndex = closestRegressionSeries;
|
|
*m_selectedDotIndex = -1;
|
|
setRoundCrossCursorView();
|
|
m_cursor->moveTo(x, x, yValue(*m_selectedSeriesIndex, x, context));
|
|
return true;
|
|
}
|
|
|
|
if (validDot) {
|
|
// Select the dot
|
|
*m_selectedSeriesIndex = closestDotSeries;
|
|
*m_selectedDotIndex = dotSelected;
|
|
setRoundCrossCursorView();
|
|
if (dotSelected == m_store->numberOfPairsOfSeries(*m_selectedSeriesIndex)) {
|
|
// Select the mean dot
|
|
double x = m_store->meanOfColumn(*m_selectedSeriesIndex, 0);
|
|
double y = m_store->meanOfColumn(*m_selectedSeriesIndex, 1);
|
|
m_cursor->moveTo(x, x, y);
|
|
} else {
|
|
// Select a data point dot
|
|
double x = m_store->get(*m_selectedSeriesIndex, 0, *m_selectedDotIndex);
|
|
double y = m_store->get(*m_selectedSeriesIndex, 1, *m_selectedDotIndex);
|
|
m_cursor->moveTo(x, x, y);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// There was no suitable selection
|
|
return false;
|
|
}
|
|
|
|
uint32_t GraphController::rangeVersion() {
|
|
return m_store->rangeChecksum();
|
|
}
|
|
|
|
bool GraphController::closestCurveIndexIsSuitable(int newIndex, int currentIndex) const {
|
|
return newIndex != currentIndex && !m_store->seriesIsEmpty(newIndex);
|
|
}
|
|
|
|
Coordinate2D<double> GraphController::xyValues(int curveIndex, double x, Poincare::Context * context) const {
|
|
return Coordinate2D<double>(x, yValue(curveIndex, x, context));
|
|
}
|
|
|
|
double GraphController::yValue(int curveIndex, double x, Poincare::Context * context) const {
|
|
return m_store->yValueForXValue(curveIndex, x, context);
|
|
}
|
|
|
|
bool GraphController::suitableYValue(double y) const {
|
|
return m_store->yMin() <= y && y <= m_store->yMax();
|
|
}
|
|
|
|
int GraphController::numberOfCurves() const {
|
|
return Store::k_numberOfSeries;
|
|
}
|
|
|
|
int GraphController::estimatedBannerNumberOfLines() const {
|
|
return (selectedSeriesIndex() < 0) ? 3 : m_store->modelForSeries(selectedSeriesIndex())->bannerLinesCount();
|
|
}
|
|
|
|
void GraphController::setRoundCrossCursorView() {
|
|
/* At this point, the model (selected series and dot indices) should be up
|
|
* to date. */
|
|
bool round = *m_selectedDotIndex < 0;
|
|
if (round) {
|
|
// Set the color although the cursor view stays round
|
|
assert(*m_selectedSeriesIndex < Palette::numberOfDataColors());
|
|
m_roundCursorView.setColor(Palette::DataColor[*m_selectedSeriesIndex]);
|
|
}
|
|
CursorView * nextCursorView = round ? static_cast<Shared::CursorView *>(&m_roundCursorView) : static_cast<Shared::CursorView *>(&m_crossCursorView);
|
|
// Escape if the cursor view stays the same
|
|
if (m_view.cursorView() == nextCursorView) {
|
|
return;
|
|
}
|
|
#ifdef GRAPH_CURSOR_SPEEDUP
|
|
m_roundCursorView.resetMemoization();
|
|
#endif
|
|
m_view.setCursorView(nextCursorView);
|
|
}
|
|
|
|
}
|