Files
Upsilon/apps/regression/graph_controller.cpp
Léa Saviot 53705fb333 [apps/graph_ctrlr] Reload range if no previous model is present
We memoize the checksum of the x first models, and we check that one of
these models is still present when the graph view appears. If so, we do
not reload the range, other wise we reload it.

Scenario:
f(t) = [t^2  t+1] in parametric
Display the graph
f(x) = 1 on ]-inf;0]
g(x) = 2 on [0;inf[
Display the graph -> the range did not change
2020-03-11 11:43:36 +01:00

423 lines
16 KiB
C++

#include "graph_controller.h"
#include "../shared/poincare_helpers.h"
#include "../shared/text_helpers.h"
#include "../apps_container.h"
#include <poincare/preferences.h>
#include <cmath>
using namespace Poincare;
using namespace Shared;
static inline float minFloat(float x, float y) { return x < y ? x : y; }
static inline float maxFloat(float x, float y) { return x > y ? x : y; }
static inline int maxInt(int x, int y) { return x > y ? x : y; }
namespace Regression {
GraphController::GraphController(Responder * parentResponder, InputEventHandlerDelegate * inputEventHandlerDelegate, ButtonRowController * header, Store * store, CurveViewCursor * cursor, uint32_t * modelVersion, uint32_t * previousModelsVersions, uint32_t * rangeVersion, int * selectedDotIndex, int * selectedSeriesIndex) :
InteractiveCurveViewController(parentResponder, inputEventHandlerDelegate, header, store, &m_view, cursor, modelVersion, previousModelsVersions, rangeVersion),
m_crossCursorView(),
m_roundCursorView(),
m_bannerView(this, inputEventHandlerDelegate, this),
m_view(store, m_cursor, &m_bannerView, &m_crossCursorView),
m_store(store),
m_initialisationParameterController(this, m_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);
}
ViewController * GraphController::initialisationParameterController() {
return &m_initialisationParameterController;
}
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 = maxInt(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) {
// 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(2)->setText(buffer);
// Set "r2=..."
numberOfChar = 0;
legend = " r2=";
double r2 = m_store->squaredCorrelationCoefficient(*m_selectedSeriesIndex);
numberOfChar += strlcpy(buffer, legend, bufferSize);
numberOfChar += PoincareHelpers::ConvertFloatToText<double>(r2, buffer + numberOfChar, bufferSize - numberOfChar, Preferences::LargeNumberOfSignificantDigits);
m_bannerView.subTextAtIndex(3)->setText(buffer);
// Clean the last subview
buffer[0] = 0;
m_bannerView.subTextAtIndex(4)->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, bool fast) {
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 {
x = m_cursor->x() + direction * m_store->xGridUnit()/k_numberOfCursorStepsInGradUnit;
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);
if (m_store->yAuto()) {
m_store->panToMakePointVisible(x, y, cursorTopMarginRatio(), k_cursorRightMarginRatio, cursorBottomMarginRatio(), k_cursorLeftMarginRatio);
}
*m_selectedDotIndex = m_store->numberOfPairsOfSeries(*m_selectedSeriesIndex);
}
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::modelVersion() {
return m_store->storeChecksum();
}
uint32_t GraphController::modelVersionAtIndex(size_t i) {
return m_store->seriesChecksumAtIndex(i);
}
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();
}
InteractiveCurveViewRangeDelegate::Range GraphController::computeYRange(InteractiveCurveViewRange * interactiveCurveViewRange) {
float minY = FLT_MAX;
float maxY = -FLT_MAX;
for (int series = 0; series < Store::k_numberOfSeries; series++) {
for (int k = 0; k < m_store->numberOfPairsOfSeries(series); k++) {
if (m_store->xMin() <= m_store->get(series, 0, k) && m_store->get(series, 0, k) <= m_store->xMax()) {
minY = minFloat(minY, m_store->get(series, 1, k));
maxY = maxFloat(maxY, m_store->get(series, 1, k));
}
}
}
InteractiveCurveViewRangeDelegate::Range range;
range.min = minY;
range.max = maxY;
return range;
}
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
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);
}
}