mirror of
https://gitlab.com/pholy/OSCAR-code.git
synced 2025-04-04 02:00:43 +00:00
1301 lines
38 KiB
C++
1301 lines
38 KiB
C++
/* gOverviewGraph Implementation
|
|
*
|
|
* Copyright (c) 2019-2024 The OSCAR Team
|
|
* Copyright (c) 2011-2018 Mark Watkins
|
|
*
|
|
* This file is subject to the terms and conditions of the GNU General Public
|
|
* License. See the file COPYING in the main directory of the source code
|
|
* for more details. */
|
|
|
|
#define TEST_MACROS_ENABLEDoff
|
|
#include "test_macros.h"
|
|
|
|
#include <math.h>
|
|
#include <QLabel>
|
|
#include <QDateTime>
|
|
#include "gYAxis.h"
|
|
#include "gOverviewGraph.h"
|
|
|
|
#ifndef REMOVE_FITNESS
|
|
/* To enable this module change the REMOTE_FITNESS define in appsettings.h
|
|
*/
|
|
|
|
gOverviewGraph::gOverviewGraph(QString label, GraphType type)
|
|
: Layer(NoChannel), m_label(label), m_graphtype(type)
|
|
{
|
|
m_empty = true;
|
|
hl_day = -1;
|
|
m_machinetype = MT_CPAP;
|
|
|
|
QDateTime d1 = QDateTime::currentDateTime();
|
|
QDateTime d2 = d1;
|
|
d1.setTimeSpec(Qt::UTC);
|
|
tz_offset = d2.secsTo(d1);
|
|
tz_hours = tz_offset / 3600.0;
|
|
m_layertype = LT_SummaryChart;
|
|
}
|
|
gOverviewGraph::~gOverviewGraph()
|
|
{
|
|
}
|
|
void gOverviewGraph::SetDay(Day * nullday)
|
|
{
|
|
Q_UNUSED(nullday)
|
|
Day *day = nullptr;
|
|
Layer::SetDay(day);
|
|
|
|
m_values.clear();
|
|
m_times.clear();
|
|
m_days.clear();
|
|
m_hours.clear();
|
|
m_goodcodes.clear();
|
|
m_miny = 999999999.0F;
|
|
m_maxy = -999999999.0F;
|
|
m_physmaxy = 0;
|
|
m_physminy = 0;
|
|
m_minx = 0;
|
|
m_maxx = 0;
|
|
|
|
|
|
int dn;
|
|
EventDataType tmp, total;
|
|
ChannelID code;
|
|
CPAPMode cpapmode = (CPAPMode)(int)p_profile->calcSettingsMax(CPAP_Mode, MT_CPAP,
|
|
p_profile->FirstDay(MT_CPAP), p_profile->LastDay(MT_CPAP));
|
|
|
|
|
|
//////////////////////////////////////////////////////////
|
|
// Setup for dealing with different CPAP Pressure types
|
|
//////////////////////////////////////////////////////////
|
|
if (m_label == STR_TR_Pressure) {
|
|
m_codes.clear();
|
|
m_colors.clear();
|
|
m_type.clear();
|
|
m_typeval.clear();
|
|
|
|
float perc = p_profile->general->prefCalcPercentile() / 100.0;
|
|
int mididx = p_profile->general->prefCalcMiddle();
|
|
SummaryType mid;
|
|
|
|
if (mididx == 0) { mid = ST_PERC; }
|
|
else if (mididx == 1) { mid = ST_WAVG; }
|
|
else mid = ST_AVG;
|
|
|
|
|
|
if (cpapmode >= MODE_ASV) {
|
|
addSlice(CPAP_EPAP, QColor("green"), ST_SETMIN);
|
|
addSlice(CPAP_IPAPLo, QColor("light blue"), ST_SETMIN);
|
|
addSlice(CPAP_IPAP, QColor("cyan"), mid, 0.5);
|
|
addSlice(CPAP_IPAP, QColor("dark cyan"), ST_PERC, perc);
|
|
//addSlice(CPAP_IPAP,QColor("light blue"),ST_PERC,0.95);
|
|
addSlice(CPAP_IPAPHi, QColor("blue"), ST_SETMAX);
|
|
} else if (cpapmode >= MODE_BILEVEL_AUTO_FIXED_PS) {
|
|
addSlice(CPAP_EPAP, QColor("green"), ST_SETMIN);
|
|
addSlice(CPAP_IPAP, QColor("light cyan"), mid, 0.5);
|
|
addSlice(CPAP_IPAP, QColor("light blue"), ST_PERC, perc);
|
|
addSlice(CPAP_PSMin, QColor("blue"), ST_SETMIN, perc);
|
|
addSlice(CPAP_PSMax, QColor("red"), ST_SETMAX, perc);
|
|
|
|
} else if (cpapmode >= MODE_BILEVEL_FIXED) {
|
|
addSlice(CPAP_EPAP, QColor("green"), ST_SETMIN);
|
|
addSlice(CPAP_EPAP, QColor("light green"), ST_PERC, perc);
|
|
addSlice(CPAP_IPAP, QColor("light cyan"), mid, 0.5);
|
|
addSlice(CPAP_IPAP, QColor("light blue"), ST_PERC, perc);
|
|
addSlice(CPAP_IPAPHi, QColor("blue"), ST_SETMAX);
|
|
} else if (cpapmode >= MODE_APAP) {
|
|
addSlice(CPAP_PressureMin, QColor("orange"), ST_SETMIN);
|
|
addSlice(CPAP_Pressure, QColor("dark green"), mid, 0.5f);
|
|
addSlice(CPAP_Pressure, QColor("grey"), ST_PERC, perc);
|
|
addSlice(CPAP_PressureMax, QColor("red"), ST_SETMAX);
|
|
} else {
|
|
addSlice(CPAP_Pressure, QColor("dark green"), ST_SETWAVG);
|
|
}
|
|
}
|
|
|
|
// Initialize goodcodes (which identified which legends are drawn) to all off
|
|
m_goodcodes.resize(m_codes.size());
|
|
for (int i = 0; i < m_codes.size(); i++) {
|
|
m_goodcodes[i] = false;
|
|
}
|
|
|
|
m_fday = 0;
|
|
qint64 tt;
|
|
m_empty = true;
|
|
|
|
if (m_graphtype == GT_SESSIONS) {
|
|
// No point drawing anything if no real data on record
|
|
if (p_profile->countDays(MT_CPAP, p_profile->FirstDay(MT_CPAP), p_profile->LastDay(MT_CPAP)) == 0) {
|
|
return;
|
|
}
|
|
}
|
|
bool first = true;
|
|
|
|
int suboffset;
|
|
SummaryType type;
|
|
|
|
// For each day in the main profile daylist
|
|
|
|
for (auto d=p_profile->daylist.begin(), dend=p_profile->daylist.end(); d!=dend; ++d) {
|
|
Day * day = d.value();
|
|
|
|
// get the timestamp of this day.
|
|
tt = QDateTime(d.key(), QTime(0, 0, 0), Qt::UTC).toTime_t();
|
|
|
|
// calculate day number
|
|
dn = tt / 86400;
|
|
|
|
// to ms since epoch.
|
|
tt *= 1000L;
|
|
|
|
// update min and max for this timestamp
|
|
if (!m_minx || tt < m_minx) { m_minx = tt; }
|
|
if (!m_maxx || tt > m_maxx) { m_maxx = tt; }
|
|
|
|
total = 0;
|
|
bool fnd = false;
|
|
|
|
//////////////////////////////////////////////////////////
|
|
// Setup for Sessions Time display chart
|
|
//////////////////////////////////////////////////////////
|
|
if (m_graphtype == GT_SESSIONS) {
|
|
qint64 zt;
|
|
EventDataType tmp2;
|
|
// Turn all legends on
|
|
for (int i = 0; i < m_codes.size(); i++) {
|
|
m_goodcodes[i] = true;
|
|
}
|
|
|
|
// for each day object on record for this date
|
|
|
|
// skip any empty or irrelevant day records
|
|
if (!day || (day->machine(m_machinetype) == nullptr)) { continue; }
|
|
|
|
//int ft = qint64(day->first()) / 1000L;
|
|
//ft += tz_offset; // convert to local time
|
|
|
|
//int dz2 = ft / 86400;
|
|
//dz2 *= 86400;
|
|
// ft = first sessions time, rounded back to midnight..
|
|
|
|
// For each session in this day record
|
|
for (int s=0, size=day->size(); s < size; s++) {
|
|
Session *sess = (*day)[s];
|
|
|
|
if (!sess->enabled()) { continue; }
|
|
|
|
// Get session duration
|
|
tmp = sess->hours();
|
|
m_values[dn][s] = tmp;
|
|
|
|
total += tmp;
|
|
|
|
// Get session start timestamp
|
|
zt = qint64(sess->first()) / 1000L;
|
|
zt += tz_offset;
|
|
|
|
// Calculate the starting hour
|
|
tmp2 = zt - dn * 86400;
|
|
tmp2 /= 3600.0;
|
|
|
|
m_times[dn][s] = tmp2;
|
|
|
|
// Update min & max Y values
|
|
if (first) {
|
|
m_miny = tmp2;
|
|
m_maxy = tmp2 + tmp;
|
|
first = false;
|
|
} else {
|
|
if (tmp2 < m_miny) {
|
|
m_miny = tmp2;
|
|
}
|
|
|
|
if (tmp2 + tmp > m_maxy) {
|
|
m_maxy = tmp2 + tmp;
|
|
}
|
|
}
|
|
} // for each session
|
|
|
|
// if total hours for all sessions more than 0, register the day as valid
|
|
if (total > 0) {
|
|
m_days[dn] = day;
|
|
m_hours[dn] = total;
|
|
m_empty = false;
|
|
}
|
|
} else
|
|
{
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
// Data Channel summary charts
|
|
//////////////////////////////////////////////////////////////////////////////
|
|
|
|
// For each Channel
|
|
for (int j = 0; j < m_codes.size(); j++) {
|
|
code = m_codes[j];
|
|
suboffset = 0;
|
|
type = m_type[j];
|
|
EventDataType typeval = m_typeval[j];
|
|
|
|
day = d.value();
|
|
|
|
CPAPMode mode = (CPAPMode)(int)day->settings_max(CPAP_Mode);
|
|
|
|
// ignore irrelevent day objects
|
|
if (day->machine(m_machinetype) == nullptr) { continue; }
|
|
|
|
bool hascode = //day->channelHasData(code) ||
|
|
(type == ST_HOURS) ||
|
|
(type == ST_SESSIONS) ||
|
|
day->settingExists(code) ||
|
|
day->hasData(code, type);
|
|
|
|
|
|
if (code == CPAP_Pressure) {
|
|
if ((cpapmode > MODE_CPAP) && (mode == MODE_CPAP)) {
|
|
hascode = false;
|
|
|
|
if ((type == ST_WAVG) || (type == ST_AVG) || ((type == ST_PERC) && (typeval == 0.5))) {
|
|
type = ST_SETWAVG;
|
|
hascode = true;
|
|
}
|
|
} else {
|
|
type = m_type[j];
|
|
}
|
|
}
|
|
|
|
//if (code==CPAP_Hypopnea) { // Make sure at least one of the CPAP data gets through with 0
|
|
// hascode=true;
|
|
//}
|
|
if (hascode) {
|
|
m_days[dn] = day;
|
|
|
|
switch (type) {
|
|
case ST_AVG:
|
|
tmp = day->avg(code);
|
|
break;
|
|
|
|
case ST_SUM:
|
|
tmp = day->sum(code);
|
|
break;
|
|
|
|
case ST_WAVG:
|
|
tmp = day->wavg(code);
|
|
break;
|
|
|
|
case ST_90P:
|
|
tmp = day->p90(code);
|
|
break;
|
|
|
|
case ST_PERC:
|
|
tmp = day->percentile(code, typeval);
|
|
break;
|
|
|
|
case ST_MIN:
|
|
tmp = day->Min(code);
|
|
break;
|
|
|
|
case ST_MAX:
|
|
tmp = day->Max(code);
|
|
break;
|
|
|
|
case ST_CNT:
|
|
tmp = day->count(code);
|
|
break;
|
|
|
|
case ST_CPH:
|
|
tmp = day->count(code) / day->hours(m_machinetype);
|
|
break;
|
|
|
|
case ST_SPH:
|
|
tmp = day->sph(code);
|
|
break;
|
|
|
|
case ST_HOURS:
|
|
tmp = day->hours(m_machinetype);
|
|
break;
|
|
|
|
case ST_SESSIONS:
|
|
tmp = day->size();
|
|
break;
|
|
|
|
case ST_SETMIN:
|
|
tmp = day->settings_min(code);
|
|
break;
|
|
|
|
case ST_SETMAX:
|
|
tmp = day->settings_max(code);
|
|
break;
|
|
|
|
case ST_SETAVG:
|
|
tmp = day->settings_avg(code);
|
|
break;
|
|
|
|
case ST_SETWAVG:
|
|
tmp = day->settings_wavg(code);
|
|
break;
|
|
|
|
case ST_SETSUM:
|
|
tmp = day->settings_sum(code);
|
|
break;
|
|
|
|
default:
|
|
tmp = 0;
|
|
break;
|
|
}
|
|
|
|
if (suboffset > 0) {
|
|
tmp -= suboffset;
|
|
|
|
if (tmp < 0) { tmp = 0; }
|
|
}
|
|
|
|
total += tmp;
|
|
m_values[dn][j + 1] = tmp;
|
|
|
|
if (tmp < m_miny) { m_miny = tmp; }
|
|
|
|
if (tmp > m_maxy) { m_maxy = tmp; }
|
|
|
|
m_goodcodes[j] = true;
|
|
fnd = true;
|
|
}
|
|
|
|
}
|
|
|
|
if (fnd) {
|
|
if (!m_fday) { m_fday = dn; }
|
|
|
|
m_values[dn][0] = total;
|
|
m_hours[dn] = day->hours(m_machinetype);
|
|
|
|
if (m_graphtype == GT_BAR) {
|
|
if (total < m_miny) { m_miny = total; }
|
|
|
|
if (total > m_maxy) { m_maxy = total; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
m_empty = true;
|
|
|
|
for (const auto & goodcode : m_goodcodes) {
|
|
if (goodcode) {
|
|
m_empty = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (m_graphtype == GT_BAR) {
|
|
m_miny = 0;
|
|
}
|
|
|
|
// m_minx=qint64(QDateTime(p_profile->FirstDay(),QTime(0,0,0),Qt::UTC).toTime_t())*1000L;
|
|
m_maxx = qint64(QDateTime(p_profile->LastDay(), QTime(23, 59, 0), Qt::UTC).toTime_t()) * 1000L;
|
|
m_physmaxy = m_maxy;
|
|
m_physminy = m_miny;
|
|
}
|
|
|
|
void gOverviewGraph::paint(QPainter &painter, gGraph &w, const QRegion ®ion)
|
|
{
|
|
int left = region.boundingRect().left();
|
|
int top = region.boundingRect().top();
|
|
int width = region.boundingRect().width();
|
|
int height = region.boundingRect().height();
|
|
|
|
if (!m_visible) { return; }
|
|
|
|
GraphType graphtype = m_graphtype;
|
|
|
|
if (graphtype == GT_LINE || graphtype == GT_POINTS) {
|
|
bool pts = AppSetting->overviewLinechartMode() == OLC_Lines;
|
|
graphtype = pts ? GT_POINTS : GT_LINE;
|
|
}
|
|
|
|
rtop = top;
|
|
|
|
painter.setPen(QColor(Qt::black));
|
|
painter.drawLine(left, top, left, top+height);
|
|
painter.drawLine(left, top+height, left+width, top+height);
|
|
painter.drawLine(left+width, top+height, left+width, top);
|
|
painter.drawLine( left+width, top, left, top);
|
|
|
|
qint64 minx = w.min_x, maxx = w.max_x;
|
|
|
|
int days = ceil(double(maxx-minx) / 86400000.0);
|
|
|
|
bool buttuglydaysteps = false ; //!p_profile->appearance->animations();
|
|
|
|
double lcursor = w.graphView()->currentTime();
|
|
if (days >= 1) {
|
|
|
|
double b = w.max_x - w.min_x;
|
|
double a = lcursor - w.min_x;
|
|
double c = a / b;
|
|
|
|
if (buttuglydaysteps) {
|
|
// this kills the beautiful smooth scrolling and makes days stop on day boundaries :(
|
|
minx = floor(double(minx)/86400000.0);
|
|
minx *= 86400000L;
|
|
|
|
maxx = minx + 86400000L * qint64(days)-1;
|
|
}
|
|
|
|
b = maxx - minx;
|
|
double d = c * b;
|
|
lcursor = d + minx;
|
|
|
|
|
|
}
|
|
|
|
|
|
qint64 xx = maxx - minx;
|
|
|
|
|
|
|
|
EventDataType miny = m_physminy;
|
|
EventDataType maxy = m_physmaxy;
|
|
|
|
w.roundY(miny, maxy);
|
|
|
|
EventDataType yy = maxy - miny;
|
|
EventDataType ymult = float(height - 2) / yy;
|
|
|
|
barw = (float(width) / float(days));
|
|
|
|
// graph = &w;
|
|
float px;// = left;
|
|
l_left = w.marginLeft() + gYAxis::Margin;
|
|
l_top = w.marginTop();
|
|
l_width = width;
|
|
l_height = height;
|
|
float py;
|
|
EventDataType total;
|
|
|
|
int daynum = 0;
|
|
EventDataType h, tmp;
|
|
|
|
|
|
l_offset = (minx) % 86400000L;
|
|
offset = float(l_offset) / 86400000.0;
|
|
|
|
offset *= barw;
|
|
px = left - offset;
|
|
l_minx = minx;
|
|
l_maxx = maxx + 86400000L;
|
|
|
|
int total_days = 0;
|
|
double total_val = 0;
|
|
double total_hours = 0;
|
|
bool lastdaygood = false;
|
|
QVector<double> totalcounts;
|
|
QVector<double> totalvalues;
|
|
QVector<float> lastX;
|
|
QVector<short> lastY;
|
|
int numcodes = m_codes.size();
|
|
totalcounts.resize(numcodes);
|
|
totalvalues.resize(numcodes);
|
|
lastX.resize(numcodes);
|
|
lastY.resize(numcodes);
|
|
int zd = minx / 86400000L;
|
|
zd--;
|
|
auto d = m_values.find(zd);
|
|
|
|
QVector<bool> goodcodes;
|
|
goodcodes.resize(m_goodcodes.size());
|
|
|
|
lastdaygood = true;
|
|
|
|
// Display Line Cursor
|
|
if (AppSetting->lineCursorMode()) {
|
|
qint64 time = lcursor;
|
|
double xmult = double(width) / xx;
|
|
|
|
if ((time > minx) && (time < maxx)) {
|
|
double xpos = (time - minx) * xmult;
|
|
painter.setPen(QPen(QBrush(QColor(0,255,0,255)),1));
|
|
painter.drawLine(left+xpos, top-w.marginTop()-3, left+xpos, top+height+w.bottom-1);
|
|
}
|
|
|
|
|
|
|
|
// QDateTime dt=QDateTime::fromMSecsSinceEpoch(time,Qt::UTC);
|
|
|
|
// QString text = dt.date().toString(Qt::SystemLocaleLongDate);
|
|
|
|
// int wid, h;
|
|
// GetTextExtent(text, wid, h);
|
|
// w.renderText(text, left + width/2 - wid/2, top-h+5);
|
|
|
|
}
|
|
|
|
for (int i = 0; i < numcodes; i++) {
|
|
totalcounts[i] = 0;
|
|
|
|
// Set min and max to the opposite largest starting value
|
|
if ((m_type[i] == ST_MIN) || (m_type[i] == ST_SETMIN)) {
|
|
totalvalues[i] = maxy;
|
|
} else if ((m_type[i] == ST_MAX) || (m_type[i] == ST_SETMAX)) {
|
|
totalvalues[i] = miny;
|
|
} else {
|
|
totalvalues[i] = 0;
|
|
}
|
|
|
|
// Turn off legend display.. It will only display if it's turned back on during draw.
|
|
goodcodes[i] = false;
|
|
|
|
if (!m_goodcodes[i]) { continue; }
|
|
|
|
lastX[i] = px;
|
|
|
|
if (d != m_values.end() && d.value().contains(i + 1)) {
|
|
|
|
tmp = d.value()[i + 1];
|
|
h = tmp * ymult;
|
|
} else {
|
|
lastdaygood = false;
|
|
h = 0;
|
|
}
|
|
|
|
lastY[i] = top + height - 1 - h;
|
|
}
|
|
|
|
float compliance_hours = 0;
|
|
|
|
int incompliant = 0;
|
|
Day *day;
|
|
EventDataType hours;
|
|
|
|
//quint32 * tptr;
|
|
//EventStoreType * dptr;
|
|
|
|
short px2, py2;
|
|
const qint64 ms_per_day = 86400000L;
|
|
|
|
|
|
painter.setClipRect(left, top, width, height);
|
|
painter.setClipping(true);
|
|
|
|
QColor summaryColor = QColor("dark gray");
|
|
|
|
float lineThickness = AppSetting->lineThickness();
|
|
|
|
|
|
for (qint64 Q = minx; Q <= maxx + ms_per_day; Q += ms_per_day) {
|
|
zd = Q / ms_per_day;
|
|
d = m_values.find(zd);
|
|
|
|
if (Q < minx) {
|
|
goto jumpnext;
|
|
}
|
|
|
|
if (d != m_values.end()) {
|
|
day = m_days[zd];
|
|
bool summary_only = day && day->summaryOnly();
|
|
|
|
|
|
if (!m_hours.contains(zd)) {
|
|
goto jumpnext;
|
|
}
|
|
|
|
hours = m_hours[zd];
|
|
|
|
int x1 = px;
|
|
//x1-=(barw/2.0);
|
|
int x2 = px + barw;
|
|
|
|
//if (x1 < left) { x1 = left; }
|
|
|
|
if (x2 > left + width) { x2 = left + width; }
|
|
|
|
// if (x2<x1)
|
|
// goto jumpnext;
|
|
|
|
|
|
if (zd == hl_day) {
|
|
QColor col = QColor("red");
|
|
col.setAlpha(64);
|
|
|
|
if (graphtype != GT_POINTS) {
|
|
painter.fillRect(x1-1, top, barw, height, QBrush(col));
|
|
// quads->add(x1 - 1, top, x1 - 1, top + height, x2, top + height, x2, top, col.rgba());
|
|
} else {
|
|
painter.fillRect((x1+barw/2)-5, top, barw, height, QBrush(col));
|
|
// quads->add((x1 + barw / 2) - 5, top, (x1 + barw / 2) - 5, top + height, (x2 - barw / 2) + 5,
|
|
// top + height, (x2 - barw / 2) + 5, top, col.rgba());
|
|
}
|
|
}
|
|
|
|
if (graphtype == GT_SESSIONS) {
|
|
int j;
|
|
auto times = m_times.find(zd);
|
|
QColor col = m_colors[0];
|
|
//if (hours<compliance_hours) col=QColor("#f03030");
|
|
|
|
if (summary_only) {
|
|
col = summaryColor;
|
|
}
|
|
if (zd == hl_day) {
|
|
col = COLOR_Gold;
|
|
}
|
|
|
|
QColor col1 = col;
|
|
QColor col2 = brighten(col,2.37f);
|
|
//outlines->setColor(Qt::black);
|
|
|
|
int np = d.value().size();
|
|
|
|
if (np > 0) {
|
|
for (auto & goodcode : goodcodes) {
|
|
goodcode = true;
|
|
}
|
|
}
|
|
|
|
for (j = 0; j < np; j++) {
|
|
EventDataType tmp2 = times.value()[j] - miny;
|
|
py = top + height - (tmp2 * ymult);
|
|
|
|
tmp = d.value()[j]; // length
|
|
|
|
//tmp-=miny;
|
|
h = tmp * ymult;
|
|
|
|
QLinearGradient gradient(x1, py-h, x1+barw, py-h);
|
|
gradient.setColorAt(0,col1);
|
|
gradient.setColorAt(1,col2);
|
|
painter.fillRect(x1, py-h, barw, h, QBrush(gradient));
|
|
// quads->add(x1, py, x1, py - h, x2, py - h, x2, py, col1, col2);
|
|
|
|
if ((h > 0) && (barw > 2)) {
|
|
painter.setPen(QColor(Qt::black));
|
|
painter.drawLine(x1, py, x1, py - h);
|
|
painter.drawLine(x1, py - h, x2, py - h);
|
|
painter.drawLine(x1, py, x2, py);
|
|
painter.drawLine(x2, py, x2, py - h);
|
|
}
|
|
|
|
totalvalues[0] += hours * tmp;
|
|
}
|
|
|
|
totalcounts[0] += hours;
|
|
totalvalues[1] += j;
|
|
totalcounts[1]++;
|
|
total_val += hours;
|
|
total_hours += hours;
|
|
total_days++;
|
|
} else
|
|
{
|
|
if (!d.value().contains(0)) {
|
|
goto jumpnext;
|
|
}
|
|
|
|
total = d.value()[0];
|
|
|
|
//if (total>0) {
|
|
if (day) {
|
|
EventDataType hours = m_hours[zd];
|
|
total_val += total * hours;
|
|
total_hours += hours;
|
|
total_days++;
|
|
}
|
|
|
|
py = top + height;
|
|
|
|
//}
|
|
bool good;
|
|
SummaryType type;
|
|
|
|
for (auto g=d.value().begin(), dend=d.value().end(); g != dend; g++) {
|
|
short j = g.key();
|
|
|
|
if (!j) { continue; }
|
|
|
|
j--;
|
|
good = m_goodcodes[j];
|
|
|
|
if (!good) {
|
|
continue;
|
|
}
|
|
|
|
type = m_type[j];
|
|
// code was actually used (to signal the display of the legend summary)
|
|
goodcodes[j] = good;
|
|
|
|
tmp = g.value();
|
|
|
|
QColor col = m_colors[j];
|
|
|
|
if (type == ST_HOURS) {
|
|
if (tmp < compliance_hours) {
|
|
col = QColor("#f04040");
|
|
incompliant++;
|
|
} else if (summary_only) {
|
|
col = summaryColor;
|
|
}
|
|
}
|
|
|
|
if (zd == hl_day) {
|
|
col = COLOR_Gold;
|
|
}
|
|
|
|
//if (!tmp) continue;
|
|
if ((type == ST_MAX) || (type == ST_SETMAX)) {
|
|
if (totalvalues[j] < tmp) {
|
|
totalvalues[j] = tmp;
|
|
}
|
|
} else if ((type == ST_MIN) || (type == ST_SETMIN)) {
|
|
if (totalvalues[j] > tmp) {
|
|
totalvalues[j] = tmp;
|
|
}
|
|
} else {
|
|
totalvalues[j] += tmp * hours;
|
|
}
|
|
|
|
//if (tmp) {
|
|
totalcounts[j] += hours;
|
|
//}
|
|
tmp -= miny;
|
|
h = tmp * ymult; // height in pixels
|
|
|
|
if (graphtype == GT_BAR) {
|
|
QColor col1 = col;
|
|
QColor col2 = brighten(col,2.5);
|
|
|
|
QLinearGradient gradient(x1, py-h, x1+barw, py-h);
|
|
gradient.setColorAt(0,col1);
|
|
gradient.setColorAt(1,col2);
|
|
painter.fillRect(x1, py-h, barw, h, QBrush(gradient));
|
|
|
|
// quads->add(x1, py, x1, py - h, col1);
|
|
// quads->add(x2, py - h, x2, py, col2);
|
|
|
|
if (h > 0 && barw > 2) {
|
|
painter.setPen(QColor(Qt::black));
|
|
painter.drawLine(x1, py, x1, py - h);
|
|
painter.drawLine(x1, py - h, x2, py - h);
|
|
painter.drawLine(x1, py, x2, py);
|
|
painter.drawLine(x2, py, x2, py - h);
|
|
} // if (bar
|
|
|
|
py -= h;
|
|
} else if (graphtype == GT_LINE) { // if (m_graphtype==GT_BAR
|
|
QColor col1 = col;
|
|
QColor col2 = m_colors[j];
|
|
px2 = px + barw;
|
|
py2 = (top + height - 1) - h;
|
|
|
|
// If more than 1 day between records, skip the vertical crud.
|
|
if ((px2 - lastX[j]) > barw + 1) {
|
|
lastdaygood = false;
|
|
}
|
|
|
|
if (lastdaygood) {
|
|
if (lastY[j] != py2) { // vertical line
|
|
painter.setPen(QPen(col2, lineThickness));
|
|
painter.drawLine(lastX[j], lastY[j], px, py2);
|
|
}
|
|
|
|
painter.setPen(QPen(col1, lineThickness));
|
|
painter.drawLine(px, py2, px2, py2);
|
|
} else {
|
|
painter.setPen(QPen(col1, lineThickness));
|
|
painter.drawLine(x1, py2, x2, py2);
|
|
}
|
|
|
|
lastX[j] = px2;
|
|
lastY[j] = py2;
|
|
} else if (graphtype == GT_POINTS) {
|
|
QColor col1 = col;
|
|
QColor col2 = m_colors[j];
|
|
px2 = px + barw;
|
|
py2 = (top + height - 2) - h;
|
|
|
|
// If more than 1 day between records, skip the vertical crud.
|
|
if ((px2 - lastX[j]) > barw + 1) {
|
|
lastdaygood = false;
|
|
}
|
|
|
|
if (zd == hl_day) {
|
|
painter.setPen(QPen(brighten(col2),10));
|
|
painter.drawPoint(px2 - barw / 2, py2);
|
|
}
|
|
|
|
if (lastdaygood) {
|
|
painter.setPen(QPen(col2, lineThickness));
|
|
painter.drawLine(lastX[j] - barw / 2, lastY[j], px2 - barw / 2, py2);
|
|
} else {
|
|
painter.setPen(QPen(col1, lineThickness));
|
|
painter.drawLine(px + barw / 2 - 1, py2, px + barw / 2 + 1, py2);
|
|
}
|
|
|
|
lastX[j] = px2;
|
|
lastY[j] = py2;
|
|
}
|
|
} // for(QHash<short
|
|
}
|
|
|
|
lastdaygood = true;
|
|
// if (Q>maxx+extra) break;
|
|
} else {
|
|
if (Q < maxx) {
|
|
incompliant++;
|
|
}
|
|
|
|
lastdaygood = false;
|
|
}
|
|
|
|
jumpnext:
|
|
|
|
if (px >= left + width + barw) {
|
|
break;
|
|
}
|
|
|
|
px += barw;
|
|
|
|
daynum++;
|
|
//lastQ=Q;
|
|
}
|
|
painter.setClipping(false);
|
|
|
|
// Draw Ledgend
|
|
px = left + width - 3;
|
|
py = top - 5;
|
|
int legendx = px;
|
|
QString a, b;
|
|
int x, y;
|
|
|
|
QSize size = QFontMetrics(*defaultfont).size(Qt::TextSingleLine ,"X");
|
|
int bw = size.width();
|
|
int bh = size.height() / 1.8;
|
|
//QFontMetrics fm(*defaultfont);
|
|
//int bw = fm.width('X');
|
|
//int bh = fm.height() / 1.8;
|
|
|
|
// bool ishours = false;
|
|
int good = 0;
|
|
|
|
for (int j = 0; j < m_codes.size(); j++) {
|
|
if (!goodcodes[j]) { continue; }
|
|
|
|
good++;
|
|
SummaryType type = m_type[j];
|
|
ChannelID code = m_codes[j];
|
|
EventDataType tval = m_typeval[j];
|
|
|
|
switch (type) {
|
|
case ST_WAVG:
|
|
b = "Avg";
|
|
break;
|
|
|
|
case ST_AVG:
|
|
b = "Avg";
|
|
break;
|
|
|
|
case ST_90P:
|
|
b = "90%";
|
|
break;
|
|
|
|
case ST_PERC:
|
|
if (tval >= 0.99) { b = STR_TR_Max; }
|
|
else if (tval == 0.5) { b = STR_TR_Med; }
|
|
else { b = QString("%1%").arg(tval * 100.0, 0, 'f', 0); }
|
|
|
|
break;
|
|
|
|
//b=QString("%1%").arg(tval*100.0,0,'f',0); break;
|
|
case ST_MIN:
|
|
b = STR_TR_Min;
|
|
break;
|
|
|
|
case ST_MAX:
|
|
b = STR_TR_Max;
|
|
break;
|
|
|
|
case ST_SETMIN:
|
|
b = STR_TR_Min;
|
|
break;
|
|
|
|
case ST_SETMAX:
|
|
b = STR_TR_Max;
|
|
break;
|
|
|
|
case ST_CPH:
|
|
b = "";
|
|
break;
|
|
|
|
case ST_SPH:
|
|
b = "%";
|
|
break;
|
|
|
|
case ST_HOURS:
|
|
b = STR_UNIT_Hours;
|
|
break;
|
|
|
|
case ST_SESSIONS:
|
|
b = STR_TR_Sessions;
|
|
break;
|
|
|
|
default:
|
|
b = "";
|
|
break;
|
|
}
|
|
|
|
a = schema::channel[code].label();
|
|
|
|
if (a == w.title() && !b.isEmpty()) { a = b; }
|
|
else { a += " " + b; }
|
|
|
|
QString val;
|
|
float f = 0;
|
|
|
|
if (totalcounts[j] > 0) {
|
|
if ((type == ST_MIN) || (type == ST_MAX) || (type == ST_SETMIN) || (type == ST_SETMAX)) {
|
|
f = totalvalues[j];
|
|
} else {
|
|
f = totalvalues[j] / totalcounts[j];
|
|
}
|
|
}
|
|
|
|
if (type == ST_HOURS) {
|
|
int h = f;
|
|
int m = int(f * 60) % 60;
|
|
val = QString::asprintf("%02i:%02i", h, m);
|
|
// ishours = true;
|
|
} else {
|
|
val = QString::number(f, 'f', 2);
|
|
}
|
|
|
|
a += ": " + val;
|
|
//GetTextExtent(a,x,y);
|
|
//float wt=20*w.printScaleX();
|
|
//px-=wt+x;
|
|
//w.renderText(a,px+wt,py+1);
|
|
//quads->add(px+wt-y/4-y,py-y,px+wt-y/4,py-y,px+wt-y/4,py+1,px+wt-y/4-y,py+1,m_colors[j].rgba());
|
|
|
|
|
|
//QString text=schema::channel[code].label();
|
|
|
|
int wid, hi;
|
|
GetTextExtent(a, wid, hi);
|
|
legendx -= wid;
|
|
w.renderText(a, legendx, top - 4);
|
|
// legendx-=bw/2;
|
|
|
|
painter.fillRect(legendx - bw-4, top-w.marginTop()-1, bh, w.marginTop(), QBrush(m_colors[j]));
|
|
legendx -= bw * 2;
|
|
|
|
|
|
//lines->add(px,py,px+20,py,m_colors[j]);
|
|
//lines->add(px,py+1,px+20,py+1,m_colors[j]);
|
|
}
|
|
|
|
if ((m_graphtype == GT_BAR) && (good > 0)) {
|
|
|
|
if (m_type.size() > 1) {
|
|
float val = total_val / float(total_hours);
|
|
a = m_label + ": " + QString::number(val, 'f', 2) + " ";
|
|
GetTextExtent(a, x, y);
|
|
legendx -= x;
|
|
w.renderText(a, legendx, py + 1);
|
|
}
|
|
}
|
|
|
|
a = "";
|
|
/*if (m_graphtype==GT_BAR) {
|
|
if (m_type.size()>1) {
|
|
float val=total_val/float(total_days);
|
|
a+=m_label+": "+QString::number(val,'f',2)+" ";
|
|
//
|
|
}
|
|
}*/
|
|
a += QString(QObject::tr("Days: %1")).arg(total_days, 0);
|
|
|
|
//GetTextExtent(a,x,y);
|
|
//legendx-=30+x;
|
|
//w.renderText(a,px+24,py+5);
|
|
w.renderText(a, left, py + 1);
|
|
}
|
|
|
|
QString formatTime(EventDataType v, bool show_seconds = false, bool duration = false,
|
|
bool show_12hr = false)
|
|
{
|
|
int h = int(v);
|
|
|
|
if (!duration) {
|
|
h %= 24;
|
|
} else { show_12hr = false; }
|
|
|
|
int m = int(v * 60) % 60;
|
|
int s = int(v * 3600) % 60;
|
|
|
|
char pm[3] = {"am"};
|
|
|
|
if (show_12hr) {
|
|
h >= 12 ? pm[0] = 'p' : pm[0] = 'a';
|
|
h %= 12;
|
|
|
|
if (h == 0) { h = 12; }
|
|
|
|
} else {
|
|
pm[0] = 0;
|
|
}
|
|
|
|
if (show_seconds) {
|
|
return QString::asprintf("%i:%02i:%02i%s", h, m, s, pm);
|
|
} else {
|
|
return QString::asprintf("%i:%02i%s", h, m, pm);
|
|
}
|
|
}
|
|
|
|
bool gOverviewGraph::mouseMoveEvent(QMouseEvent *event, gGraph *graph)
|
|
{
|
|
graph->timedRedraw(0);
|
|
int xposLeft = event->x();
|
|
int yPosTop = event->y();
|
|
|
|
if (!m_rect.contains(xposLeft, yPosTop)) {
|
|
// if ((x<0 || y<0 || x>l_width || y>l_height)) {
|
|
hl_day = -1;
|
|
//graph->timedRedraw(2000);
|
|
return false;
|
|
}
|
|
|
|
xposLeft -= m_rect.left();
|
|
yPosTop -= m_rect.top();
|
|
|
|
Q_UNUSED(yPosTop)
|
|
|
|
double xx = l_maxx - l_minx;
|
|
|
|
double xmult = xx / double(l_width + barw);
|
|
|
|
qint64 mx = ceil(xmult * double(xposLeft - offset));
|
|
mx += l_minx;
|
|
mx = mx + l_offset; //-86400000L;
|
|
int zd = mx / 86400000L;
|
|
|
|
Day *day;
|
|
//if (hl_day!=zd) // This line is an optimization
|
|
|
|
{
|
|
hl_day = zd;
|
|
graph->Trigger(2000);
|
|
|
|
auto d = m_values.find(hl_day);
|
|
|
|
QMap<short, EventDataType> &valhash = d.value();
|
|
|
|
xposLeft += m_rect.left(); //gYAxis::Margin+gGraphView::titleWidth; //graph->m_marginleft+
|
|
int y = event->y() - m_rect.top() + rtop - 15;
|
|
//QDateTime dt1=QDateTime::fromTime_t(hl_day*86400).toLocalTime();
|
|
QDateTime dt2 = QDateTime::fromTime_t(hl_day * 86400).toUTC();
|
|
// QDateTime dt2 = QDateTime::fromTime_t(hl_day * 86400).toLocalTime();
|
|
|
|
//QTime t1=dt1.time();
|
|
//QTime t2=dt2.time();
|
|
|
|
QDate dt = dt2.date();
|
|
day = m_days[zd];
|
|
|
|
if ((d != m_values.end()) && (day != nullptr)) {
|
|
bool summary_only = day->summaryOnly();
|
|
|
|
QString strTooltip = dt.toString(Qt::SystemLocaleShortDate);
|
|
|
|
// Day * day=m_days[hl_day];
|
|
//EventDataType val;
|
|
QString val;
|
|
|
|
if (m_graphtype == GT_SESSIONS) {
|
|
if (m_type[0] == ST_HOURS) {
|
|
|
|
int t = m_hours[zd] * 3600.0;
|
|
int h = t / 3600;
|
|
int m = (t / 60) % 60;
|
|
//int s=t % 60;
|
|
val = QString::asprintf("%02i:%02i", h, m);
|
|
} else {
|
|
val = QString::number(d.value()[0], 'f', 2);
|
|
}
|
|
|
|
strTooltip += "\r\n" + m_label + ": " + val;
|
|
|
|
if (m_type[1] == ST_SESSIONS) {
|
|
strTooltip += " "+QString(QObject::tr("(Sess: %1)")).arg(day->size(), 0);
|
|
}
|
|
|
|
EventDataType v = m_times[zd][0];
|
|
int lastt = m_times[zd].size() - 1;
|
|
|
|
if (lastt < 0) { lastt = 0; }
|
|
|
|
strTooltip += "\r\n"+QString(QObject::tr("Bedtime: %1")).arg(formatTime(v, false, false, true));
|
|
v = m_times[zd][lastt] + m_values[zd][lastt];
|
|
strTooltip += "\r\n"+QString(QObject::tr("Waketime: %1")).arg(formatTime(v, false, false, true));
|
|
|
|
} else
|
|
if (m_graphtype == GT_BAR) {
|
|
if (m_type[0] == ST_HOURS) {
|
|
int t = d.value()[0] * 3600.0;
|
|
int h = t / 3600;
|
|
int m = (t / 60) % 60;
|
|
//int s=t % 60;
|
|
val = QString::asprintf("%02i:%02i", h, m);
|
|
} else {
|
|
val = QString::number(d.value()[0], 'f', 2);
|
|
}
|
|
|
|
strTooltip += "\r\n" + m_label + ": " + val;
|
|
//z+="\r\nMode="+QString::number(day->settings_min("FlexSet"),'f',0);
|
|
|
|
} else {
|
|
QString strDataType;
|
|
|
|
for (int i = 0; i < m_type.size(); i++) {
|
|
if (!m_goodcodes[i]) {
|
|
continue;
|
|
}
|
|
|
|
if (!valhash.contains(i + 1)) {
|
|
continue;
|
|
}
|
|
|
|
EventDataType tval = m_typeval[i];
|
|
|
|
switch (m_type[i]) {
|
|
case ST_WAVG:
|
|
strDataType = STR_TR_WAvg;
|
|
break;
|
|
|
|
case ST_AVG:
|
|
strDataType = STR_TR_Avg;
|
|
break;
|
|
|
|
case ST_90P:
|
|
strDataType = QString("90%");
|
|
break;
|
|
|
|
case ST_PERC:
|
|
if (tval >= 0.99) { strDataType = STR_TR_Max; }
|
|
else if (tval == 0.5) { strDataType = STR_TR_Med; }
|
|
else { strDataType = QString("%1%").arg(tval * 100.0, 0, 'f', 0); }
|
|
|
|
break;
|
|
|
|
case ST_MIN:
|
|
strDataType = STR_TR_Min;
|
|
break;
|
|
|
|
case ST_MAX:
|
|
strDataType = STR_TR_Max;
|
|
break;
|
|
|
|
case ST_CPH:
|
|
strDataType = "";
|
|
break;
|
|
|
|
case ST_SPH:
|
|
strDataType = "%";
|
|
break;
|
|
|
|
case ST_HOURS:
|
|
strDataType = STR_UNIT_Hours;
|
|
break;
|
|
|
|
case ST_SESSIONS:
|
|
strDataType = STR_TR_Sessions;
|
|
break;
|
|
|
|
case ST_SETMIN:
|
|
strDataType = STR_TR_Min;
|
|
break;
|
|
|
|
case ST_SETMAX:
|
|
strDataType = STR_TR_Max;
|
|
break;
|
|
|
|
default:
|
|
strDataType = "";
|
|
break;
|
|
}
|
|
|
|
if (m_type[i] == ST_SESSIONS) {
|
|
val = QString::number(d.value()[i + 1], 'f', 0);
|
|
strTooltip += "\r\n" + strDataType + ": " + val;
|
|
} else {
|
|
//if (day && (day->channelExists(m_codes[i]) || day->settingExists(m_codes[i]))) {
|
|
schema::Channel &chan = schema::channel[m_codes[i]];
|
|
EventDataType v;
|
|
|
|
if (valhash.contains(i + 1)) {
|
|
v = valhash[i + 1];
|
|
} else { v = 0; }
|
|
|
|
if (m_codes[i] == Journal_Weight) {
|
|
val = weightString(v, p_profile->general->unitSystem());
|
|
} else {
|
|
val = QString::number(v, 'f', 2);
|
|
}
|
|
strTooltip += "\r\n" + chan.label() + " " + strDataType + ": " + val;
|
|
//}
|
|
}
|
|
}
|
|
|
|
}
|
|
if (summary_only) {
|
|
strTooltip += "\r\n"+QObject::tr("(Summary Only)");
|
|
}
|
|
|
|
graph->ToolTip(strTooltip, xposLeft, y - 15);
|
|
return false;
|
|
} else {
|
|
QString z = dt.toString(Qt::SystemLocaleShortDate) + "\r\n"+QObject::tr("No Data");
|
|
graph->ToolTip(z, xposLeft, y - 15);
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool gOverviewGraph::mousePressEvent(QMouseEvent *event, gGraph *graph)
|
|
{
|
|
if (event->modifiers() & Qt::ShiftModifier) {
|
|
//qDebug() << "Jump to daily view?";
|
|
return true;
|
|
}
|
|
|
|
Q_UNUSED(graph)
|
|
return false;
|
|
}
|
|
|
|
bool gOverviewGraph::keyPressEvent(QKeyEvent *event, gGraph *graph)
|
|
{
|
|
Q_UNUSED(event)
|
|
Q_UNUSED(graph)
|
|
//qDebug() << "Summarychart Keypress";
|
|
return false;
|
|
}
|
|
|
|
#include "mainwindow.h"
|
|
extern MainWindow *mainwin;
|
|
bool gOverviewGraph::mouseReleaseEvent(QMouseEvent *event, gGraph *graph)
|
|
{
|
|
if (event->modifiers() & Qt::ShiftModifier) {
|
|
if (hl_day < 0) {
|
|
mouseMoveEvent(event, graph);
|
|
}
|
|
|
|
if (hl_day > 0) {
|
|
QDateTime d = QDateTime::fromTime_t(hl_day * 86400).toUTC();
|
|
// QDateTime d = QDateTime::fromTime_t(hl_day * 86400).toLocalTime();
|
|
mainwin->getDaily()->LoadDate(d.date());
|
|
mainwin->JumpDaily();
|
|
//qDebug() << "Jump to daily view?" << d;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
Q_UNUSED(event)
|
|
hl_day = -1;
|
|
graph->timedRedraw(2000);
|
|
return false;
|
|
}
|
|
#endif
|