Minsky
plotWidget.cc
Go to the documentation of this file.
1 /*
2  @copyright Steve Keen 2012
3  @author Russell Standish
4  This file is part of Minsky.
5 
6  Minsky is free software: you can redistribute it and/or modify it
7  under the terms of the GNU General Public License as published by
8  the Free Software Foundation, either version 3 of the License, or
9  (at your option) any later version.
10 
11  Minsky is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  GNU General Public License for more details.
15 
16  You should have received a copy of the GNU General Public License
17  along with Minsky. If not, see <http://www.gnu.org/licenses/>.
18 */
19 #include "minsky.h"
20 #include "plotWidget.h"
21 #include "variable.h"
22 #include "cairoItems.h"
23 #include "latexMarkup.h"
24 #include "pango.h"
25 #include <timer.h>
26 #include <cairo/cairo-ps.h>
27 #include <cairo/cairo-pdf.h>
28 #include <cairo/cairo-svg.h>
29 
30 #include "itemT.rcd"
31 #include "plotOptions.rcd"
32 #include "plot.rcd"
33 #include "plot.xcd"
34 #include "tensorInterface.rcd"
35 #include "tensorInterface.xcd"
36 #include "tensorVal.rcd"
37 #include "tensorVal.xcd"
38 #include "plotWidget.rcd"
39 #include "minsky_epilogue.h"
40 
41 #include <algorithm>
42 #include <numeric>
43 using namespace ecolab::cairo;
44 using namespace ecolab;
45 using namespace std;
46 using namespace boost;
47 using namespace boost::posix_time;
48 using namespace boost::gregorian;
49 
50 namespace minsky
51 {
52  namespace
53  {
54  // orientation of bounding box ports
55  const double orient[PlotWidget::nBoundsPorts]={-0.4*M_PI, -0.6*M_PI, -0.2*M_PI, 0.2*M_PI, 1.2*M_PI, 0.8*M_PI};
56  // x coordinates of bounding box ports
57  const float boundX[PlotWidget::nBoundsPorts]={-0.46,0.45,-0.49,-0.49, 0.48, 0.48};
58  // y coordinates of bounding box ports
59  const float boundY[PlotWidget::nBoundsPorts]={0.49,0.49,0.47,-0.49, 0.47, -0.49};
60 
61  // height of title, as a fraction of overall widget height
62  const double titleHeight=0.05;
63 
64  }
65 
66  PlotWidget::PlotWidget()
67  {
68  m_width=m_height=150;
69  nxTicks=nyTicks=10;
70  fontScale=2;
71  leadingMarker=true;
72  grid=true;
73  legendLeft=0.1; // override ecolab's default value
74  legendSide=boundingBox;
75  addPorts();
76 
77  yvars.resize(2*m_numLines);
78  xvars.resize(m_numLines);
79  }
80 
81  void PlotWidget::addPorts()
82  {
83  m_ports.clear();
84  unsigned i=0;
85  for (; i<nBoundsPorts; ++i) // bounds ports
86  m_ports.emplace_back(make_shared<InputPort>(*this));
87  for (; i<2*m_numLines+nBoundsPorts; ++i) // y data input ports
88  m_ports.emplace_back(make_shared<MultiWireInputPort>(*this));
89  for (; i<4*m_numLines+nBoundsPorts; ++i) // x data input ports
90  m_ports.emplace_back(make_shared<InputPort>(*this));
91  }
92 
93  void PlotWidget::draw(cairo_t* cairo) const
94  {
95  CairoSave cs(cairo);
96  const double z=Item::zoomFactor();
97  cairo_scale(cairo,z,z);
98  const double w=iWidth();
99  double h=iHeight();
100 
101  // if any titling, draw an extra bounding box (ticket #285)
102  if (!title.empty()||!xlabel().empty()||!ylabel().empty()||!y1label().empty())
103  {
104  cairo_rectangle(cairo,-0.5*w+10,-0.5*h,w-20,h-10);
105  cairo_set_line_width(cairo,1);
106  cairo_stroke(cairo);
107  }
108 
109  cairo_translate(cairo,-0.5*w,-0.5*h);
110 
111  yoffs=0;
112  if (!title.empty())
113  {
114  const CairoSave cs(cairo);
115  const double fy=titleHeight*iHeight();
116 
117  Pango pango(cairo);
118  pango.setFontSize(fabs(fy));
119  pango.setMarkup(latexToPango(title));
120  cairo_set_source_rgb(cairo,0,0,0);
121  cairo_move_to(cairo,0.5*(w-pango.width()), 0);
122  pango.show();
123 
124  // allow some room for the title
125  yoffs=pango.height();
126  h-=yoffs;
127  }
128 
129  // draw bounding box ports
130 
131  size_t i=0;
132  // draw bounds input ports
133  for (; i<nBoundsPorts; ++i)
134  {
135  const float x=boundX[i]*w, y=boundY[i]*h;
136  if (!justDataChanged)
137  m_ports[i]->moveTo(x*z + this->x(), y*z + this->y()+0.5*yoffs);
138  drawTriangle(cairo, x+0.5*w, y+0.5*h+yoffs, palette[(i/2)%palette.size()].colour, orient[i]);
139 
140  }
141 
142  const float xLeft = -0.5*w, dx=w/(2*m_numLines+1); // x location of ports
143  const float dy = h/m_numLines;
144  // draw y data ports
145  for (; i<m_numLines+nBoundsPorts; ++i)
146  {
147  const float y=0.5*(dy-h) + (i-nBoundsPorts)*dy;
148  if (!justDataChanged)
149  m_ports[i]->moveTo(xLeft*z + this->x(), y*z + this->y()+0.5*yoffs);
150  drawTriangle(cairo, xLeft+0.5*w, y+0.5*h+yoffs, palette[(i-nBoundsPorts)%palette.size()].colour, 0);
151  }
152 
153  // draw RHS y data ports
154  for (; i<2*m_numLines+nBoundsPorts; ++i)
155  {
156  const float y=0.5*(dy-h) + (i-m_numLines-nBoundsPorts)*dy, x=0.5*w;
157  if (!justDataChanged)
158  m_ports[i]->moveTo(x*z + this->x(), y*z + this->y()+0.5*yoffs);
159  drawTriangle(cairo, x+0.5*w, y+0.5*h+yoffs, palette[(i-nBoundsPorts)%palette.size()].colour, M_PI);
160  }
161 
162  // draw x data ports
163  const float yBottom=0.5*h;
164  for (; i<4*m_numLines+nBoundsPorts; ++i)
165  {
166  const float x=dx-0.5*w + (i-2*m_numLines-nBoundsPorts)*dx;
167  if (!justDataChanged)
168  m_ports[i]->moveTo(x*z + this->x(), yBottom*z + this->y()+0.5*yoffs);
169  drawTriangle(cairo, x+0.5*w, yBottom+0.5*h+yoffs, palette[(i-2*m_numLines-nBoundsPorts)%palette.size()].colour, -0.5*M_PI);
170  }
171 
172  cairo_translate(cairo, portSpace, yoffs);
173  cairo_set_line_width(cairo,1);
174  const double gw=w-2*portSpace, gh=h-portSpace;
175 
176  Plot::draw(cairo,gw,gh);
177  if (mouseFocus && legend)
178  {
179  double width,height,x,y;
180  legendSize(x,y,width,height,gw,gh);
181  // following code puts x,y at centre point of legend
182  x+=0.5*width;
183  const double arrowLength=6;
184  y=(h-portSpace)-y+0.5*height;
185  cairo_move_to(cairo,x-arrowLength,y);
186  cairo_rel_line_to(cairo,2*arrowLength,0);
187  cairo_move_to(cairo,x,y-arrowLength);
188  cairo_rel_line_to(cairo,0,2*arrowLength);
189 
190  cairo_move_to(cairo,x,y+0.5*height);
191  cairo_rel_line_to(cairo,0,arrowLength);
192  cairo_stroke(cairo);
193  drawTriangle(cairo,x-arrowLength,y,{0,0,0,1},M_PI);
194  drawTriangle(cairo,x+arrowLength,y,{0,0,0,1},0);
195  drawTriangle(cairo,x,y-arrowLength,{0,0,0,1},3*M_PI/2);
196  drawTriangle(cairo,x,y+arrowLength,{0,0,0,1},M_PI/2);
197  drawTriangle(cairo,x,y+0.5*height+arrowLength,{0,0,0,1},M_PI/2);
198 
199  cairo_rectangle(cairo,x-0.5*width,y-0.5*height,width,height);
200  }
201  cs.restore();
202  if (mouseFocus)
203  {
204  drawPorts(cairo);
205  displayTooltip(cairo,tooltip());
206  // Resize handles always visible on mousefocus. For ticket 92.
207  drawResizeHandles(cairo);
208  }
209  justDataChanged=false;
210 
211  cairo_new_path(cairo);
212  cairo_rectangle(cairo,-0.5*w,-0.5*h,w,h);
213  cairo_clip(cairo);
214  if (selected) drawSelected(cairo);
215 
216  }
217 
218  void PlotWidget::scalePlot()
219  {
220  // set any scale overrides
221  setMinMax();
222  if (xminVar && xminVar->idx()>-1)
223  {
224  if (xIsSecsSinceEpoch && (xminVar->units.empty() || xminVar->units==Units("year")))
225  minx=yearToPTime(xminVar->value());
226  else
227  minx=xminVar->value();
228  }
229  else if (isfinite(xmin))
230  {
231  if (xIsSecsSinceEpoch && (cminsky().timeUnit.empty() || cminsky().timeUnit=="year"))
232  minx=yearToPTime(xmin);
233  else
234  minx=xmin;
235  }
236 
237  if (xmaxVar && xmaxVar->idx()>-1)
238  {
239  if (xIsSecsSinceEpoch && (xmaxVar->units.empty() || xmaxVar->units==Units("year")))
240  maxx=yearToPTime(xmaxVar->value());
241  else
242  maxx=xmaxVar->value();
243  }
244  else if (isfinite(xmax))
245  {
246  if (xIsSecsSinceEpoch && (cminsky().timeUnit.empty() || cminsky().timeUnit=="year"))
247  maxx=yearToPTime(xmax);
248  else
249  maxx=xmax;
250  }
251 
252  if (yminVar && yminVar->idx()>-1)
253  miny=yminVar->value();
254  else if (isfinite(ymin))
255  miny=ymin;
256  if (ymaxVar && ymaxVar->idx()>-1)
257  maxy=ymaxVar->value();
258  else if (isfinite(ymax))
259  maxy=ymax;
260 
261  if (y1minVar && y1minVar->idx()>-1)
262  miny1=y1minVar->value();
263  else if (isfinite(y1min))
264  miny1=ymin;
265  if (y1maxVar && y1maxVar->idx()>-1)
266  maxy1=y1maxVar->value();
267  else if (isfinite(y1max))
268  maxy1=y1max;
269  autoscale=false;
270 
271  if (!justDataChanged)
272  {
273  // label pens. In order or priority:
274  // 1. wire tooltip
275  // 2. from item tooltip
276  // 3. attached variable tooltip
277  // 4. attached variable name
278  size_t pen=0;
279  penLabels.clear();
280  assert(m_ports.size()>=2*m_numLines+nBoundsPorts);
281  for (auto portNo=nBoundsPorts; portNo<2*m_numLines+nBoundsPorts; ++portNo)
282  {
283  if (portNo<m_ports.size())
284  for (size_t i=0; i<m_ports[portNo]->wires().size(); ++i, ++pen)
285  {
286  auto wire=m_ports[portNo]->wires()[i];
287  if (!wire->tooltip().empty())
288  {
289  labelPen(pen, wire->tooltip());
290  continue;
291  }
292  if (auto from=wire->from(); !from->item().tooltip().empty())
293  {
294  labelPen(pen, from->item().tooltip());
295  continue;
296  }
297  if (portNo-nBoundsPorts<yvars.size() && i<yvars[portNo-nBoundsPorts].size())
298  if (auto v=yvars[portNo-nBoundsPorts][i]; !v->name.empty())
299  labelPen(pen, uqName(v->name));
300  }
301  }
302  }
303  }
304 
305  void PlotWidget::mouseDown(float x,float y)
306  {
307  clickX=x;
308  clickY=y;
309  ct=clickType(x,y);
310  const double z=Item::zoomFactor();
311  const double gw=iWidth()*z-2*portSpace;
312  double gh=iHeight()*z-portSpace;
313  if (!title.empty()) gh=iHeight()*z-portSpace-titleHeight;
314  oldLegendLeft=legendLeft*gw+portSpace;
315  oldLegendTop=legendTop*gh;
316  oldLegendFontSz=legendFontSz;
317  }
318 
319  void PlotWidget::mouseMove(float x,float y)
320  {
321  const double z=Item::zoomFactor();
322  //const double w=0.5*iWidth()*z, h=0.5*iHeight()*z;
323  //const double dx=x-this->x(), dy=y-this->y();
324  const double gw=iWidth()*z-2*portSpace;
325  double gh=iHeight()*z-portSpace;
326  if (!title.empty()) gh=iHeight()*z-portSpace-titleHeight;
327  const double yoffs=this->y()-(legendTop-0.5)*iHeight()*z;
328  switch (ct)
329  {
330  case ClickType::legendMove:
331  legendLeft = (oldLegendLeft + x - clickX-portSpace)/gw;
332  legendTop = (oldLegendTop + clickY - y)/gh;
333  if (!title.empty()) legendTop = (oldLegendTop + clickY - y + titleHeight)/gh;
334  break;
335  case ClickType::legendResize:
336  legendFontSz = oldLegendFontSz * (y-yoffs)/(clickY-yoffs);
337  if (!title.empty()) legendFontSz = oldLegendFontSz * (y-yoffs+titleHeight)/(clickY-yoffs);
338  break;
339  default:
340  {
341  auto& f=frameArgs();
342  if (Plot::mouseMove((x-f.offsetLeft)/f.childWidth, (f.childHeight-f.offsetTop-y)/f.childHeight,
343  10.0/std::max(f.childWidth,f.childHeight)))
344  requestRedraw();
345  }
346  break;
347  }
348  }
349 
350  bool PlotWidget::onMouseOver(float x,float y)
351  {
352  const double z=Item::zoomFactor();
353  // coordinate system runs from bottom left to top right. Vertical coordinates must be flipped
354  const double dx=x-this->x()+0.5*iWidth()*z-portSpace;
355  const double dy=this->y()-y+0.5*iHeight()*z-portSpace;
356  const double gw=iWidth()*z-2*portSpace;
357  const double gh=iHeight()*z-portSpace-yoffs*z;
358  const double loffx=lh(gw,gh)*!Plot::ylabel.empty(), loffy=lh(gw,gh)*!Plot::xlabel.empty();
359  return Plot::mouseMove((dx-loffx)/gw, (dy-loffy)/gh, 10.0/std::max(gw,gh),formatter);
360  }
361 
362 
363  void PlotWidget::requestRedraw()
364  {
365  justDataChanged=true; // assume plot same size, don't do unnecessary stuff
366  // store previous min/max values to determine if plot scale changes
367  scalePlot();
368  if (surface.get())
369  surface->requestRedraw();
370  }
371 
372  bool PlotWidget::redraw(int x0, int y0, int width, int height)
373  {
374  if (surface.get())
375  {
376  auto sf=RenderNativeWindow::scaleFactor();
377  Plot::draw(surface->cairo(),width/sf,height/sf);
378  surface->blit();
379  }
380  return surface.get();
381  }
382 
383 
384  void PlotWidget::makeDisplayPlot() {
385  if (auto g=group.lock())
386  g->displayPlot=dynamic_pointer_cast<PlotWidget>(g->findItem(*this));
387  }
388 
389  void PlotWidget::resize(const LassoBox& x)
390  {
391  const float invZ=1/Item::zoomFactor();
392  iWidth(abs(x.x1-x.x0)*invZ);
393  iHeight(abs(x.y1-x.y0)*invZ);
394  Item::moveTo(0.5*(x.x0+x.x1), 0.5*(x.y0+x.y1));
395  bb.update(*this);
396  }
397 
398  // specialisation to avoid rerendering plots (potentially expensive)
399  ClickType::Type PlotWidget::clickType(float x, float y) const
400  {
401  // firstly, check whether a port has been selected
402  const double z=Item::zoomFactor();
403  for (auto& p: m_ports)
404  {
405  if (hypot(x-p->x(), y-p->y()) < portRadius*z)
406  return ClickType::onPort;
407  }
408 
409  double legendWidth, legendHeight, lx, ly;
410  legendSize(lx, ly, legendWidth, legendHeight, z*(iWidth()-2*portSpace), z*(iHeight()-portSpace-yoffs));
411  // xx & yy are in plot user coordinates
412  const double xx= x-this->x() + z*(0.5*iWidth()-portSpace) - lx;
413  const double yy= z*(0.5*iHeight()-portSpace)-y+this->y() - ly+legendHeight;
414 
415  if (legend && xx>0 && xx<legendWidth)
416  {
417  if (yy>0.2*legendHeight && yy<legendHeight)
418  return ClickType::legendMove;
419  if (yy>-6*z && yy<=0.2*legendHeight) // allow a bit of extra height for resize arrow
420  return ClickType::legendResize;
421  }
422 
423  if (onResizeHandle(x,y)) return ClickType::onResize;
424 
425  const double dx=x-this->x(), dy=y-this->y();
426  const double w=0.5*iWidth()*z, h=0.5*iHeight()*z;
427  return (abs(dx)<w && abs(dy)<h)?
428  ClickType::onItem: ClickType::outside;
429  }
430 
431  static ptime epoch=microsec_clock::local_time(), accumulatedBlitTime=epoch;
432 
433  static const size_t maxNumTensorElementsToPlot=10;
434 
435  size_t PlotWidget::numLines(size_t n)
436  {
437  if (m_numLines!=n)
438  {
439  m_numLines=n;
440  addPorts();
441  }
442  return n;
443  }
444 
445 
446  double PlotWidget::barWidth() const
447  {
448  return accumulate(palette.begin(), palette.end(), 1.0,
449  [](double acc, const LineStyle& ls) {return std::min(acc, ls.barWidth);});
450  }
451 
452  double PlotWidget::barWidth(double w)
453  {
454  for (auto& ls: palette) ls.barWidth=w;
455  return w;
456  }
457 
458  void PlotWidget::addPlotPt(double t)
459  {
460  size_t pen=0;
461  for (size_t port=0; port<yvars.size(); ++port)
462  for (auto& yvar: yvars[port])
463  for (size_t i=0; i<min(maxNumTensorElementsToPlot,yvar->size()); ++i)
464  {
465  double x,y;
466  switch (xvars.size())
467  {
468  case 0: // use t, when x variable not attached
469  if (xIsSecsSinceEpoch && (cminsky().timeUnit.empty()||cminsky().timeUnit=="year"))
470  x=yearToPTime(t);
471  else
472  x=t;
473  y=(*yvar)[i];
474  break;
475  case 1: // use the value of attached variable
476  assert(xvars[0] && xvars[0]->idx()>=0); // xvars also vector of shared pointers and null derefencing error can likewise cause crash. for ticket 1248
477  if (xvars[0]->size()>1)
478  throw_error("Tensor valued x inputs not supported");
479  x=(*xvars[0])[0];
480  y=(*yvar)[i];
481  break;
482  default:
483  if (port < xvars.size() && xvars[port] && xvars[port]->idx()>=0) // xvars also vector of shared pointers and null derefencing error can likewise cause crash. for ticket 1248
484  {
485  if (xvars[port]->size()>1)
486  throw_error("Tensor valued x inputs not supported");
487  x=(*xvars[port])[0];
488  y=(*yvar)[i];
489  }
490  else
491  throw_error("x input not wired for port "+to_string(port+1));
492  break;
493  }
494  addPt(pen++, x, y);
495  }
496 
497  // add markers
498  if (isfinite(miny) && isfinite(maxy))
499  {
500  for (auto& m: horizontalMarkers)
501  if (auto v=cminsky().variableValues[valueId(group.lock(), ':'+m)])
502  {
503  auto eps=1e-4*(maxx-minx);
504  double x[]{minx+eps,miny-eps};
505  double y[]{v->value(),v->value()};
506  setPen(pen,x,y,2);
507  assignSide(pen,marker);
508  labelPen(pen++,v->tooltip);
509  }
510  for (auto& m: verticalMarkers)
511  if (auto v=cminsky().variableValues[valueId(group.lock(), ':'+m)])
512  {
513  auto eps=1e-4*(maxy-miny);
514  auto value=v->value();
515  if (xIsSecsSinceEpoch && (v->units.empty() || v->units==Units("year")))
516  value=yearToPTime(value);
517  double x[]{value,value};
518  double y[]{miny+eps,maxy-eps};
519  setPen(pen,x,y,2);
520  assignSide(pen,marker);
521  labelPen(pen++,v->tooltip);
522  }
523  }
524 
525 
526  // throttle plot redraws
527  static const time_duration maxWait=milliseconds(1000);
528  if ((microsec_clock::local_time()-(ptime&)lastAdd) >
529  min((accumulatedBlitTime-(ptime&)lastAccumulatedBlitTime) * 2, maxWait))
530  {
531  const ptime timerStart=microsec_clock::local_time();
532  requestRedraw();
533  lastAccumulatedBlitTime = accumulatedBlitTime;
534  lastAdd=microsec_clock::local_time();
535  accumulatedBlitTime += lastAdd - timerStart;
536  }
537  }
538 
539  namespace {
541  {
542  std::string format;
543  TimeFormatter(const std::string& format): format(format) {}
544  std::string operator()(const string& label,double x,double y) const
545  {
546  ostringstream r;
547  r.precision(3);
548  r<<label<<":("<<str(ptime(date(1970,Jan,1))+microseconds(static_cast<long long>(1E6*x)),format)
549  <<","<<y<<")";
550  return r.str();
551  }
552  };
553  }
554 
555  void PlotWidget::addConstantCurves()
556  {
557  std::vector<std::vector<std::pair<double,std::string>>> newXticks;
558 
559  // determine if any of the incoming vectors has a ptime-based xVector
560  xIsSecsSinceEpoch=false;
561  for (auto& yv: yvars)
562  for (auto& i: yv)
563  {
564  if (i && !i->hypercube().xvectors.empty())
565  {
566  const auto& xv=i->hypercube().xvectors[0];
567  if (xv.dimension.type==Dimension::time)
568  {
569  xIsSecsSinceEpoch=true;
570  break;
571  }
572  }
573  }
574 
575  formatter=defaultFormatter;
576 
577  if (plotType!=automatic)
578  Plot::plotType=plotType;
579 
580  size_t pen=0;
581  bool noLhsPens=true; // track whether any left had side ports are connected
582  clearPensOnLabelling=true; // arrange for penLabels to be cleared first time an entry is added
583  const OnStackExit setClearPensOnLabellingFalse([this]{clearPensOnLabelling=false;});
584 
585  for (size_t port=0; port<yvars.size(); ++port)
586  for (size_t i=0; i<yvars[port].size(); ++i)
587  if (yvars[port][i])
588  {
589  if (port<m_numLines) noLhsPens=false;
590  auto& yv=yvars[port][i];
591  if (yv->size()>0) (*yv)[0]; // ensure cachedTensors are up to date
592  auto d=yv->hypercube().dims();
593  if (d.empty())
594  {
595  if (Plot::plotType==Plot::bar)
596  addPlotPt(0);
597  pen++;
598  continue;
599  }
600  // work out a reference to the x data
601  vector<double> xdefault;
602  double* x;
603  if (port<xvars.size() && xvars[port])
604  {
605  if (xvars[port]->hypercube().xvectors[0].size()!=d[0])
606  throw error("x vector not same length as y vectors");
607  if (xvars[port]->index().empty())
608  x=xvars[port]->begin();
609  else
610  {
611  xdefault.reserve(d[0]);
612  for (size_t i=0; i<d[0]; ++i)
613  xdefault.push_back(xvars[port]->atHCIndex(i));
614  x=xdefault.data();
615  }
616  }
617  else
618  {
619  xdefault.reserve(d[0]);
620  newXticks.emplace_back();
621  if (yv->hypercube().rank()) // yv carries its own x-vector
622  {
623  const auto& xv=yv->hypercube().xvectors[0];
624  assert(xv.size()==d[0]);
625  switch (xv.dimension.type)
626  {
627  case Dimension::string:
628  for (size_t i=0; i<xv.size(); ++i)
629  {
630  newXticks.back().emplace_back(i, str(xv[i]));
631  xdefault.push_back(i);
632  }
633  if (plotType==automatic)
634  Plot::plotType=Plot::bar;
635  break;
636  case Dimension::value:
637  if (xIsSecsSinceEpoch && xv.dimension.units=="year")
638  // interpret "year" as Gregorian year date
639  for (const auto& i: xv)
640  {
641  xdefault.push_back(yearToPTime(i.value));
642  if (abs(i.value-int(i.value))<0.05) // only label years
643  newXticks.back().emplace_back(xdefault.back(), str(int(i.value)));
644  }
645  else
646  for (const auto& i: xv)
647  xdefault.push_back(i.value);
648  if (plotType==automatic)
649  Plot::plotType=Plot::line;
650  break;
651  case Dimension::time:
652  {
653  const string format=xv.timeFormat();
654  formatter=TimeFormatter(xv.dimension.units);
655  for (const auto& i: xv)
656  {
657  const double tv=(i.time-ptime(date(1970,Jan,1))).total_microseconds()*1E-6;
658  newXticks.back().emplace_back(tv,str(i,format));
659  xdefault.push_back(tv);
660  }
661  }
662  if (plotType==automatic)
663  Plot::plotType=Plot::line;
664  break;
665  }
666  }
667  else // by default, set x to 0..d[0]-1
668  for (size_t i=0; i<d[0]; ++i)
669  xdefault.push_back(i);
670  x=xdefault.data();
671  }
672 
673  const auto& idx=yv->index();
674  if (yv->rank()==1)
675  {
676  // 1D data's pen attributes should match the input port
677  if (idx.empty())
678  setPen(pen, x, yv->begin(), d[0]);
679  else
680  for (size_t j=0; j<idx.size(); ++j)
681  addPt(pen,x[idx[j]], (*yv)[j]);
682  pen++;
683  }
684  else
685  {
686  // higher rank y objects treated as multiple y vectors to plot
687  size_t startPen=pen;
688  if (idx.empty())
689  for (size_t j=0 /*d[0]*/; j<std::min(maxNumTensorElementsToPlot*d[0], yv->size()); j+=d[0])
690  {
691  setPen(pen++, x, yv->begin()+j, d[0]);
692  }
693  else // data is sparse
694  for (size_t j=0; j<idx.size(); ++j)
695  {
696  auto div=lldiv(idx[j], d[0]);
697  if (size_t(div.quot)<maxNumTensorElementsToPlot)
698  {
699  addPt(startPen+div.quot, x[div.rem], (*yv)[j]);
700  if (pen<=startPen+div.quot) pen=startPen+div.quot+1; // track highest pen used
701  }
702  }
703  // compute the pen labels
704  for (int j=0; startPen<pen; ++startPen, ++j)
705  {
706  string label;
707  size_t stride=1;
708  for (size_t i=1; i<yv->hypercube().rank(); ++i)
709  {
710  label+=str(yv->hypercube().xvectors[i][(j/stride)%d[i]])+" ";
711  stride*=d[i];
712  }
713  assignSide(startPen,port<m_numLines? Side::left: Side::right);
714  labelPen(startPen,defang(label));
715  }
716  }
717  }
718  justDataChanged=true;
719  scalePlot();
720 
721  if (noLhsPens)
722  {
723  // set scale to RHS
724  miny=miny1;
725  maxy=maxy1;
726  }
727 
728  // add markers
729  if (isfinite(miny) && isfinite(maxy))
730  {
731  for (auto& m: horizontalMarkers)
732  if (auto v=cminsky().variableValues[valueId(group.lock(), ':'+m)])
733  {
734  auto eps=1e-4*(maxx-minx);
735  addPt(pen,minx+eps,v->value());
736  addPt(pen,maxx-eps,v->value());
737  assignSide(pen,marker);
738  if (!v->tooltip.empty())
739  labelPen(pen++,v->tooltip);
740  else
741  labelPen(pen++,m);
742  }
743  for (auto& m: verticalMarkers)
744  if (auto v=cminsky().variableValues[valueId(group.lock(), ':'+m)])
745  {
746  auto eps=1e-4*(maxy-miny);
747  auto value=v->value();
748  if (xIsSecsSinceEpoch && (v->units.empty() || v->units==Units("year")))
749  value=yearToPTime(value);
750  addPt(pen,value,miny+eps);
751  addPt(pen,value,maxy-eps);
752  assignSide(pen,marker);
753  if (!v->tooltip.empty())
754  labelPen(pen++,v->tooltip);
755  else
756  labelPen(pen++,m);
757  }
758  }
759 
760  if (newXticks.size()==1) // nothing to disambiguate
761  xticks=std::move(newXticks.front());
762  else
763  {
764  xticks.clear();
765  // now work out which xticks we'll use See Ravel #173
766  for (auto& i: newXticks)
767  if (i.empty())
768  {
769  xticks.clear();
770  break; // value axes trump all
771  }
772  else if (xticks.empty())
773  xticks=std::move(i);
774  else
775  {// expand range of tick labels by each pen's tick labels in turn
776  auto j=i.begin();
777  for (; j!=i.end(); ++j)
778  if (j->first>=xticks.front().first)
779  break;
780  xticks.insert(xticks.begin(), i.begin(), j);
781  j=i.end();
782  for (; j!=i.begin(); --j)
783  if ((j-1)->first<=xticks.back().first)
784  break;
785  xticks.insert(xticks.end(), j, i.end());
786  }
787  }
788 
789  }
790 
791 
792  size_t PlotWidget::startPen(size_t port) const
793  {
794  size_t pen=0;
795  for (size_t p=0; p<std::min(port, yvars.size()); ++p)
796  pen+=yvars[p].size();
797  return pen;
798  }
799 
800 
801  void PlotWidget::connectVar(const shared_ptr<VariableValue>& var, unsigned port)
802  {
803  assert(var);
804  if (port<nBoundsPorts)
805  switch (port)
806  {
807  case 0: xminVar=var; return;
808  case 1: xmaxVar=var; return;
809  case 2: yminVar=var; return;
810  case 3: ymaxVar=var; return;
811  case 4: y1minVar=var; return;
812  case 5: y1maxVar=var; return;
813  }
814  if (port-nBoundsPorts<2*m_numLines)
815  {
816  yvars.resize(port-nBoundsPorts+1);
817  yvars[port-nBoundsPorts].push_back(var);
818  // assign Side::right to pens belonging to the RHS
819  assignSide(startPen(port-nBoundsPorts+1)-1,port-nBoundsPorts<m_numLines? Side::left: Side::right);
820  }
821  else if (port-nBoundsPorts<4*m_numLines)
822  {
823  xvars.resize(port-nBoundsPorts-2*m_numLines+1);
824  xvars[port-nBoundsPorts-2*m_numLines]=var;
825  }
826  justDataChanged=false;
827  scalePlot();
828  }
829 
830  void PlotWidget::disconnectAllVars()
831  {
832  xvars.clear();
833  yvars.clear();
834  xminVar=xmaxVar=yminVar=ymaxVar=y1minVar=y1maxVar=nullptr;
835  }
836 
837  set<string> PlotWidget::availableMarkers() const
838  {
839  // search upwards through group heirarchy, looking for variable to add
840  set<string> r;
841  for (auto g=group.lock(); g; g=g->group.lock())
842  for (auto& i: g->items)
843  if (auto v=i->variableCast())
844  r.insert(uqName(v->rawName()));
845  return r;
846  }
847 }
#define M_PI
some useful geometry types, defined from boost::geometry
Definition: geometry.h:29
string defang(char c)
Definition: latexMarkup.cc:842
function f
Definition: canvas.m:1
std::string latexToPango(const char *s)
Definition: latexMarkup.h:30
const float boundX[PlotWidget::nBoundsPorts]
Definition: plotWidget.cc:57
static const size_t maxNumTensorElementsToPlot
Definition: plotWidget.cc:433
represents rectangular region of a lasso operation
Definition: lasso.h:28
STL namespace.
const float boundY[PlotWidget::nBoundsPorts]
Definition: plotWidget.cc:59
std::string uqName(const std::string &name)
extract unqualified portion of name
Definition: valueId.cc:135
string valueId(const string &name)
construct a valueId from fully qualified name @ name should not be canonicalised
Definition: valueId.cc:75
std::string operator()(const string &label, double x, double y) const
Definition: plotWidget.cc:544
static ptime epoch
Definition: plotWidget.cc:431
arrange for a functional to be called on stack exit
Definition: str.h:77
mouseDownid x y X Y
Definition: godley.tcl:137
const double orient[PlotWidget::nBoundsPorts]
Definition: plotWidget.cc:55
Creation and access to the minskyTCL_obj object, which has code to record whenever Minsky&#39;s state cha...
Definition: constMap.h:22
std::string timeUnit
Definition: simulation.h:35
double yearToPTime(double x)
Definition: plotWidget.h:38
represents the units (in sense of dimensional analysis) of a variable.
Definition: units.h:34
std::string str(T x)
utility function to create a string representation of a numeric type
Definition: str.h:33
const Minsky & cminsky()
const version to help in const correctness
Definition: minsky.h:549
static ptime accumulatedBlitTime
Definition: plotWidget.cc:431
float x0
Definition: lasso.h:30
float y1
Definition: lasso.h:30
float x1
Definition: lasso.h:30
void drawTriangle(cairo_t *cairo, double x, double y, const ecolab::cairo::Colour &col, double angle=0)
UnitsExpressionWalker timeUnit
float y0
Definition: lasso.h:30
Definition: group.tcl:84
string to_string(CONST84 char *x)
Definition: minskyTCLObj.h:33
a container item for a plot widget
Definition: plotWidget.h:46
constexpr float portRadius
radius of circle marking ports at zoom=1
Definition: item.h:69
CLASSDESC_ACCESS_EXPLICIT_INSTANTIATION(minsky::PlotWidget)