/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * vim: set ts=8 sts=4 et sw=4 tw=99: * * gSummaryChart Implementation * * Copyright (c) 2011-2014 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 Linux * distribution for more details. */ #include #include #include #include "gYAxis.h" #include "gSummaryChart.h" extern QLabel * qstatus2; SummaryChart::SummaryChart(QString label,GraphType type) :Layer(NoChannel),m_label(label),m_graphtype(type) { //QColor color=Qt::black; addVertexBuffer(quads=new gVertexBuffer(20000,GL_QUADS)); addVertexBuffer(lines=new gVertexBuffer(20000,GL_LINES)); addVertexBuffer(outlines=new gVertexBuffer(20000,GL_LINES)); addVertexBuffer(points=new gVertexBuffer(20000,GL_POINTS)); quads->forceAntiAlias(true); outlines->setSize(1); //lines->setBlendFunc(GL_SRC_COLOR, GL_ZERO); //lines->forceAntiAlias(false); 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; } SummaryChart::~SummaryChart() { } void SummaryChart::SetDay(Day * nullday) { Day * day=nullday; Layer::SetDay(day); m_values.clear(); m_times.clear(); m_days.clear(); m_hours.clear(); m_goodcodes.clear(); m_miny=999999999; m_maxy=-999999999; m_physmaxy=0; m_physminy=0; m_minx=0; m_maxx=0; int dn; EventDataType tmp,tmp2,total; ChannelID code; CPAPMode cpapmode=(CPAPMode)(int)PROFILE.calcSettingsMax(CPAP_Mode,MT_CPAP,PROFILE.FirstDay(MT_CPAP),PROFILE.LastDay(MT_CPAP)); if (m_label==STR_TR_Pressure) { m_codes.clear(); m_colors.clear(); m_type.clear(); m_typeval.clear(); float perc=PROFILE.general->prefCalcPercentile()/100.0; int mididx=PROFILE.general->prefCalcMiddle(); SummaryType mid; if (mididx==0) mid=ST_PERC; if (mididx==1) mid=ST_WAVG; if (mididx==2) 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_BIPAP) { 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_IPAP,QColor("blue"),ST_SETMAX); } else if (cpapmode>=MODE_APAP) { addSlice(CPAP_PressureMin,QColor("orange"),ST_SETMIN); addSlice(CPAP_Pressure,QColor("dark green"),mid,0.5); addSlice(CPAP_Pressure,QColor("grey"),ST_PERC,perc); addSlice(CPAP_PressureMax,QColor("red"),ST_SETMAX); } else { addSlice(CPAP_Pressure,QColor("dark green"),ST_SETWAVG); } } m_goodcodes.resize(m_codes.size()); for (int i=0;i >::iterator d=PROFILE.daylist.begin();d!=PROFILE.daylist.end();d++) { tt=QDateTime(d.key(),QTime(0,0,0),Qt::UTC).toTime_t(); dn=tt/86400; tt*=1000L; if (!m_minx || ttm_maxx) m_maxx=tt; total=0; bool fnd=false; if (m_graphtype==GT_SESSIONS) { for (int i=0;imachine_type()!=m_machinetype) 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 (int s=0;ssize();s++) { Session *sess=(*day)[s]; if (!sess->enabled()) continue; tmp=sess->hours(); m_values[dn][s]=tmp; total+=tmp; zt=qint64(sess->first())/1000L; zt+=tz_offset; tmp2=zt-dn*86400; tmp2/=3600.0; m_times[dn][s]=tmp2; 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; } } if (total>0) { m_days[dn]=day; m_hours[dn]=total; m_empty=false; } } } else { for (int j=0;jsettings_max(CPAP_Mode); if (day->machine_type()!=m_machinetype) 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->cph(code); break; case ST_SPH: tmp=day->sph(code); break; case ST_HOURS: tmp=day->hours(); 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 (tmpm_maxy) m_maxy=tmp; m_goodcodes[j]=true; fnd=true; break; } } } if (fnd) { if (!m_fday) m_fday=dn; m_values[dn][0]=total; m_hours[dn]=day->hours(); if (m_graphtype==GT_BAR) { if (totalm_maxy) m_maxy=total; } } } } m_empty=true; for (int i=0;i255) cr=255; if (cg>255) cg=255; if (cb>255) cb=255; return QColor(cr,cg,cb,255); } void SummaryChart::paint(gGraph & w,int left, int top, int width, int height) { if (!m_visible) return; points->setSize(10); GraphType graphtype=m_graphtype; if (graphtype==GT_LINE || graphtype==GT_POINTS) { bool pts=PROFILE.appearance->overviewLinechartMode()==OLC_Lines; graphtype=pts ? GT_POINTS : GT_LINE; } if (graphtype==GT_POINTS) { lines->setSize(4); } else lines->setSize(1.5); rtop=top; gVertexBuffer *outlines2=w.lines(); // outlines2->setColor(Qt::black); outlines2->add(left, top, left, top+height, left, top+height, left+width,top+height,QColor("black").rgba()); outlines2->add(left+width,top+height, left+width, top, left+width, top, left, top,QColor("black").rgba()); //if (outlines->full()) qDebug() << "WTF??? Outlines full in SummaryChart::paint()"; qint64 minx=w.min_x, maxx=w.max_x; qint64 xx=maxx - minx; float days=double(xx)/86400000.0; EventDataType maxy; EventDataType miny; if (w.zoomY()==0 && PROFILE.appearance->allowYAxisScaling()) { maxy=m_physmaxy; miny=m_physminy; w.roundY(miny,maxy); } else { maxy=m_maxy; miny=m_miny; w.roundY(miny,maxy); } EventDataType yy=maxy-miny; EventDataType ymult=float(height-2)/yy; barw=(float(width)/float(days)); float dpr=w.graphView()->devicePixelRatio(); 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,tmp2; 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 totalcounts; QVector totalvalues; QVector lastX; QVector lastY; int numcodes=m_codes.size(); totalcounts.resize(numcodes); totalvalues.resize(numcodes); lastX.resize(numcodes); lastY.resize(numcodes); int zd=minx/86400000L; zd--; QHash >::iterator d=m_values.find(zd); QVector goodcodes; goodcodes.resize(m_goodcodes.size()); points->setSize(5.0*dpr); lastdaygood=true; for (int i=0;ishowComplianceInfo()) { compliance_hours=PROFILE.cpap->complianceHours(); } int incompliant=0; Day * day; EventDataType hours; //quint32 * tptr; //EventStoreType * dptr; short px2,py2; const qint64 ms_per_day=86400000L; for (qint64 Q=minx;Q<=maxx+ms_per_day;Q+=ms_per_day) { zd=Q/ms_per_day; d=m_values.find(zd); if (Qleft+width) x2=left+width; // if (x2add(x1-1,top,x1-1,top+height,x2,top+height,x2,top,col.rgba()); } else { 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; QHash >::iterator times=m_times.find(zd); QColor col=m_colors[0]; //if (hourssetColor(Qt::black); int np=d.value().size(); if (np > 0) { for (int i=0;iadd(x1,py,x1,py-h,x2,py-h,x2,py,col1,col2); if (h>0 && barw>2) { outlines->add(x1,py,x1,py-h,x1,py-h,x2,py-h,QColor("black").rgba()); outlines->add(x1,py,x2,py,x2,py,x2,py-h,QColor("black").rgba()); } 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 (QHash::iterator g=d.value().begin();g!=d.value().end();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 (tmptmp) 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) { GLuint col1=col.rgba(); GLuint col2=brighten(col).rgba(); quads->add(x1,py,x1,py-h,col1); quads->add(x2,py-h,x2,py,col2); if (h>0 && barw>2) { outlines->add(x1,py,x1,py-h,x1,py-h,x2,py-h,QColor("black").rgba()); outlines->add(x1,py,x2,py,x2,py,x2,py-h,QColor("black").rgba()); if (outlines->full()) qDebug() << "WTF??? Outlines full in SummaryChart::paint()"; } // if (bar py-=h; } else if (graphtype==GT_LINE) { // if (m_graphtype==GT_BAR GLuint col1=col.rgba(); GLuint col2=m_colors[j].rgba(); 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 (lastdaygood) { if (lastY[j]!=py2) {// vertical line lines->add(lastX[j],lastY[j],px,py2,col2); } lines->add(px-1,py2,px2+1,py2,col1); } else { lines->add(x1-1,py2,x2+1,py2,col1); } lastX[j]=px2; lastY[j]=py2; } else if (graphtype==GT_POINTS) { GLuint col1=col.rgba(); GLuint col2=m_colors[j].rgba(); 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) { points->add(px2-barw/2,py2,col2); } if (lastdaygood) { lines->add(lastX[j]-barw/2,lastY[j],px2-barw/2,py2,col2); } else { lines->add(px+barw/2-1,py2,px+barw/2+1,py2,col1); } lastX[j]=px2; lastY[j]=py2; } } // for(QHashmaxx+extra) break; } else { if (Q=left+width+barw) break; px+=barw; daynum++; //lastQ=Q; } #if QT_VERSION >= QT_VERSION_CHECK(5,0,0) quads->scissor(left*dpr,w.flipY(top+height+2)*dpr,(width)*dpr,(height+1)*dpr); lines->scissor(left*dpr,w.flipY(top+height+2)*dpr,(width+1)*dpr,(height+1)*dpr); outlines->scissor(left*dpr,w.flipY(top+height+2)*dpr,(width)*dpr,(height+1)*dpr); #else lines->scissor(left,w.flipY(top+height+2),width+1,height+2); outlines->scissor(left,w.flipY(top+height+2),width,height+2); quads->scissor(left,w.flipY(top+height+2),width,height+2); #endif // Draw Ledgend px=left+width-3; py=top-5; int legendx=px; QString a,b; int x,y; 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=0.99) b="Max"; else if (tval==0.5) b="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="Min"; break; case ST_MAX: b="Max"; break; case ST_SETMIN: b="Min"; break; case ST_SETMAX: b="Max"; break; case ST_CPH: b=""; break; case ST_SPH: b="%"; break; case ST_HOURS: b=STR_UNIT_Hours; break; case ST_SESSIONS: b="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.sprintf("%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; int tp=top-5-bh/2; w.quads()->add(legendx-bw,tp+bh/2,legendx,tp+bh/2,legendx,tp-bh/2,legendx-bw,tp-bh/2,m_colors[j].rgba()); 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+="Days="+QString::number(total_days,'f',0); if (PROFILE.cpap->showComplianceInfo()) { if (ishours && incompliant>0) { a+=" Low Usage Days="+QString::number(incompliant,'f',0)+" (%"+QString::number((1.0/daynum)*(total_days-incompliant)*100.0,'f',2)+" compliant, defined as >"+QString::number(compliance_hours,'f',1)+" hours)"; } } //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().sprintf("%i:%02i:%02i%s",h,m,s,pm); else return QString().sprintf("%i:%02i%s",h,m,pm); } bool SummaryChart::mouseMoveEvent(QMouseEvent *event,gGraph * graph) { int x=event->x(); int y=event->y(); if (!m_rect.contains(x,y)) { // if ((x<0 || y<0 || x>l_width || y>l_height)) { hl_day=-1; //graph->timedRedraw(2000); return false; } x-=m_rect.left(); y-=m_rect.top(); double xx=l_maxx-l_minx; double xmult=xx/double(l_width+barw); qint64 mx=ceil(xmult*double(x-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); QHash >::iterator d=m_values.find(hl_day); QHash & valhash=d.value(); x+=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(); //QTime t1=dt1.time(); //QTime t2=dt2.time(); QDate dt=dt2.date(); if (d!=m_values.end()) { day=m_days[zd]; QString z=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.sprintf("%02i:%02i",h,m); } else val=QString::number(d.value()[0],'f',2); z+="\r\n"+m_label+"="+val; if (m_type[1]==ST_SESSIONS){ z+=" (Sess="+QString::number(day->size(),'f',0)+")"; } EventDataType v=m_times[zd][0]; int lastt=m_times[zd].size()-1; if (lastt<0) lastt=0; z+="\r\nBedtime="+formatTime(v,false,false,true); v=m_times[zd][lastt]+m_values[zd][lastt]; z+="\r\nWaketime="+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.sprintf("%02i:%02i",h,m); } else val=QString::number(d.value()[0],'f',2); z+="\r\n"+m_label+"="+val; //z+="\r\nMode="+QString::number(day->settings_min("FlexSet"),'f',0); } else { QString a; for (int i=0;i=0.99) a="Max"; else if (tval==0.5) a="Med"; else a=QString("%1%").arg(tval*100.0,0,'f',0); break; case ST_MIN: a="Min"; break; case ST_MAX: a="Max"; break; case ST_CPH: a=""; break; case ST_SPH: a="%"; break; case ST_HOURS: a=STR_UNIT_Hours; break; case ST_SESSIONS: a="Sessions"; break; case ST_SETMIN: a="Min"; break; case ST_SETMAX: a="Max"; break; default: a=""; break; } if (m_type[i]==ST_SESSIONS) { val=QString::number(d.value()[i+1],'f',0); z+="\r\n"+a+"="+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,PROFILE.general->unitSystem()); } else { val=QString::number(v,'f',2); } z+="\r\n"+chan.label()+" "+a+"="+val; //} } } } graph->ToolTip(z,x,y-15,p_profile->general->tooltipTimeout()); return true; } else { QString z=dt.toString(Qt::SystemLocaleShortDate)+"\r\nNo Data"; graph->ToolTip(z,x,y-15,p_profile->general->tooltipTimeout()); return true; } } return false; } bool SummaryChart::mousePressEvent(QMouseEvent * event,gGraph * graph) { if (event->modifiers() & Qt::ShiftModifier) { //qDebug() << "Jump to daily view?"; return true; } Q_UNUSED(graph) return false; } bool SummaryChart::keyPressEvent(QKeyEvent * event,gGraph * graph) { Q_UNUSED(event) Q_UNUSED(graph) //qDebug() << "Summarychart Keypress"; return false; } #include "mainwindow.h" extern MainWindow *mainwin; bool SummaryChart::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(); 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; }