1 #include <math.h>
2 #include <float.h>
3 #include <limits.h>
4 #include <string.h>
5 #include <inttypes.h>
6 #include "internal.h"
7 
8 // common elements of type-parameterized plots
9 typedef struct ncplot {
10   ncplane* ncp;
11   ncplane* pixelp; // only used for NCBLIT_PIXEL
12   /* sloutcount-element circular buffer of samples. the newest one (rightmost)
13      is at slots[slotstart]; they get older as you go back (and around).
14      elements. slotcount is max(columns, rangex), less label room. */
15   int64_t slotx; /* x value corresponding to slots[slotstart] (newest x) */
16   uint64_t maxchannels;
17   uint64_t minchannels;
18   uint16_t legendstyle;
19   bool vertical_indep; /* not yet implemented FIXME */
20   unsigned chancount; // channel count (can change on cell-pixel geom change)
21   uint64_t* channels; // computed in calculate_gradient_vector() (constructor)
22   const struct blitset* bset;
23   char* title;
24   /* requested number of slots. 0 for automatically setting the number of slots
25      to span the horizontal area. if there are more slots than there are
26      columns, we prefer showing more recent slots to less recent. if there are
27      fewer slots than there are columns, they prefer the left side. */
28   unsigned rangex;
29   /* domain minimum and maximum. if detectdomain is true, these are
30      progressively enlarged/shrunk to fit the sample set. if not, samples
31      outside these bounds are counted, but the displayed range covers only this. */
32   unsigned slotcount;
33   int slotstart; /* index of most recently-written slot */
34   bool labelaxisd; /* label dependent axis (consumes NCPREFIXCOLUMNS columns) */
35   bool exponentiali; /* exponential independent axis */
36   bool detectdomain; /* is domain detection in effect (stretch the domain)? */
37   bool detectonlymax; /* domain detection applies only to max, not min */
38   bool printsample; /* print the most recent sample */
39 } ncplot;
40 
41 static inline int
create_pixelp(ncplot * p,ncplane * n)42 create_pixelp(ncplot *p, ncplane* n){
43   if(((p->pixelp = ncplane_dup(n, NULL)) == NULL)){
44     return -1;
45   }
46   if(ncplane_set_name(p->pixelp, "pmap")){
47     ncplane_destroy(p->pixelp);
48     return -1;
49   }
50   ncplane_reparent(p->pixelp, n);
51   ncplane_move_below(p->pixelp, n);
52   uint64_t basechan = 0;
53   ncchannels_set_bg_alpha(&basechan, NCALPHA_TRANSPARENT);
54   ncchannels_set_fg_alpha(&basechan, NCALPHA_TRANSPARENT);
55   ncplane_set_base(n, "", 0, basechan);
56   return 0;
57 }
58 
59 // we have some color gradient across the life of the plot (almost; it gets
60 // recalculated if the cell-pixel geometry changes and we're using
61 // NCBLIT_PIXEL). if we're using cell blitting, we only get one channel pair
62 // per row, no matter what height we have. with pixels, we get cellpxy * rows.
63 static int
calculate_gradient_vector(ncplot * p,unsigned pixelp)64 calculate_gradient_vector(ncplot* p, unsigned pixelp){
65   const int dimy = ncplane_dim_y(p->ncp);
66   const unsigned states = dimy * (pixelp ? ncplane_pile(p->ncp)->cellpxy : 1);
67   if(states == p->chancount){ // no need to recalculate
68     return 0;
69   }
70   uint64_t* tmp = realloc(p->channels, states * sizeof(*p->channels));
71   if(tmp == NULL){
72     return -1;
73   }
74   p->channels = tmp;
75   p->chancount = states;
76   for(unsigned y = 0 ; y < p->chancount ; ++y){ \
77     calc_gradient_channels(&p->channels[y], p->minchannels, p->minchannels,
78                            p->maxchannels, p->maxchannels,
79                            y, 0, p->chancount, 0);
80   }
81   return 0;
82 }
83 
84 #define MAXWIDTH 2
85 #define CREATE(T, X) \
86 typedef struct nc##X##plot { \
87   T* slots; \
88   T miny, maxy; \
89   ncplot plot; \
90 } nc##X##plot; \
91 \
92 int redraw_pixelplot_##T(nc##X##plot* ncp){ \
93   if(calculate_gradient_vector(&ncp->plot, 1)){ \
94     return -1; \
95   } \
96   const int scale = ncplane_pile_const(ncp->plot.ncp)->cellpxx; \
97   ncplane_erase(ncp->plot.ncp); \
98   unsigned dimy, dimx; \
99   ncplane_dim_yx(ncp->plot.ncp, &dimy, &dimx); \
100   const unsigned scaleddim = dimx * scale; \
101   /* each transition is worth this much change in value */ \
102   const size_t states = ncplane_pile_const(ncp->plot.ncp)->cellpxy; \
103   /* FIXME can we not rid ourselves of this meddlesome double? either way, the \
104      interval is one row's range (for linear plots), or the base \
105      (base^slots == maxy-miny) of the range (for exponential plots). */ \
106   double interval; \
107   if(ncp->plot.exponentiali){ \
108     if(ncp->maxy > ncp->miny){ \
109       interval = pow(ncp->maxy - ncp->miny, (double)1 / (dimy * states)); \
110 /* fprintf(stderr, "miny: %ju maxy: %ju dimy: %d states: %zu\n", miny, maxy, dimy, states); */ \
111     }else{ \
112       interval = 0; \
113     } \
114   }else{ \
115     interval = ncp->maxy < ncp->miny ? 0 : (ncp->maxy - ncp->miny) / ((double)dimy * states); \
116   } \
117   const int startx = ncp->plot.labelaxisd ? NCPREFIXCOLUMNS : 0; /* plot cols begin here */ \
118   /* if we want fewer slots than there are available columns, our final column \
119      will be other than the plane's final column. most recent x goes here. */ \
120   const unsigned finalx = (ncp->plot.slotcount < scaleddim - 1 - (startx * scale) ? \
121                           startx + (ncp->plot.slotcount / scale) - 1 : dimx - 1); \
122   ncplane_set_styles(ncp->plot.ncp, ncp->plot.legendstyle); \
123   if(ncp->plot.labelaxisd){ \
124     /* show the *top* of each interval range */ \
125     for(unsigned y = 0 ; y < dimy ; ++y){ \
126       ncplane_set_channels(ncp->plot.ncp, ncp->plot.channels[y * states]); \
127       char buf[NCPREFIXSTRLEN + 1]; \
128       if(ncp->plot.exponentiali){ \
129         if(y == dimy - 1){ /* we cheat on the top row to exactly match maxy */ \
130           ncqprefix(ncp->maxy * 100, 100, buf, 0); \
131         }else{ \
132           ncqprefix(pow(interval, (y + 1) * states) * 100, 100, buf, 0); \
133         } \
134       }else{ \
135         ncqprefix((ncp->maxy - interval * states * (dimy - y - 1)) * 100, 100, buf, 0); \
136       } \
137       if(y == dimy - 1 && strlen(ncp->plot.title)){ \
138         ncplane_printf_yx(ncp->plot.ncp, dimy - y - 1, 0, "%*.*s %s", \
139                           NCPREFIXSTRLEN, NCPREFIXSTRLEN, buf, ncp->plot.title); \
140       }else{ \
141         ncplane_printf_yx(ncp->plot.ncp, dimy - y - 1, 0, "%*.*s", \
142                           NCPREFIXSTRLEN, NCPREFIXSTRLEN, buf); \
143       } \
144     } \
145   }else if(strlen(ncp->plot.title)){ \
146     ncplane_set_channels(ncp->plot.ncp, ncp->plot.channels[(dimy - 1) * states]); \
147     ncplane_printf_yx(ncp->plot.ncp, 0, NCPREFIXCOLUMNS - strlen(ncp->plot.title), "%s", ncp->plot.title); \
148   } \
149   ncplane_set_styles(ncp->plot.ncp, NCSTYLE_NONE); \
150   if((int)finalx < startx){ /* exit on pathologically narrow planes */ \
151     return 0; \
152   } \
153   if(!interval){ \
154     interval = 1; \
155   } \
156   uint32_t* pixels = malloc(dimy * dimx * states * scale * sizeof(*pixels)); \
157   if(pixels == NULL){ \
158     return -1; \
159   } \
160   /* FIXME just zero out as we copy to each */ \
161   memset(pixels, 0, dimy * dimx * states * scale * sizeof(*pixels)); \
162   /* a column corresponds to |scale| slots' worth of samples. prepare the working gval set. */ \
163   T* gvals = malloc(sizeof(*gvals) * scale); \
164   if(gvals == NULL){ \
165     free(pixels); \
166     return -1; \
167   } \
168   int idx = ncp->plot.slotstart; /* idx holds the real slot index; we move backwards */ \
169   /* iterate backwards across the plot from the final (rightmost) x being \
170      plotted (finalx) to the first (leftmost) x being plotted (startx).   */ \
171   for(int x = finalx ; x >= startx ; --x){ \
172     /* load gvals retaining the same ordering we have in the actual array */ \
173     for(int i = scale - 1 ; i >= 0 ; --i){ \
174       gvals[i] = ncp->slots[idx]; /* clip the value at the limits of the graph */ \
175       if(gvals[i] < ncp->miny){ \
176         gvals[i] = ncp->miny; \
177       } \
178       if(gvals[i] > ncp->maxy){ \
179         gvals[i] = ncp->maxy; \
180       } \
181       /* FIXME if there are an odd number, only go up through the valid ones... */ \
182       if(--idx < 0){ \
183         idx = ncp->plot.slotcount - 1; \
184       } \
185     } \
186     /* starting from the least-significant row, progress in the more significant \
187        direction, prepping pixels, aborting early if we can't draw anything in a \
188        given cell. */ \
189     T intervalbase = ncp->miny; \
190     bool done = !ncp->plot.bset->fill; \
191     for(unsigned y = 0 ; y < dimy ; ++y){ \
192       /* if we've got at least one interval's worth on the number of positions \
193         times the number of intervals per position plus the starting offset, \
194         we're going to print *something* */ \
195       for(int i = 0 ; i < scale ; ++i){ \
196         size_t egcidx; \
197         if(intervalbase < gvals[i]){ \
198           if(ncp->plot.exponentiali){ \
199             /* we want the log-base-interval of gvals[i] */ \
200             double scaled = log(gvals[i] - ncp->miny) / log(interval); \
201             double sival = intervalbase ? log(intervalbase) / log(interval) : 0; \
202             egcidx = scaled - sival; \
203           }else{ \
204             egcidx = (gvals[i] - intervalbase) / interval; \
205           } \
206           if(egcidx >= states){ \
207             egcidx = states; \
208             done = false; \
209           } \
210         }else{ \
211           egcidx = 0; \
212         } \
213 /*fprintf(stderr, "WRITING TO y/x %d/%d (%zu)\n", y, x, dimx * dimy * scale * states); */\
214         for(size_t yy = 0 ; yy < egcidx ; ++yy){ \
215           int poff = x * scale + i + (((dimy - 1 - y) * states + (states - 1 - yy)) * dimx * scale); \
216           uint32_t color = ncchannels_fg_rgb(ncp->plot.channels[y * states + yy]); \
217           ncpixel_set_a(&color, 0xff); \
218           pixels[poff] = color; \
219         } \
220       } \
221       if(done){ \
222         break; \
223       } \
224       if(ncp->plot.exponentiali){ \
225         intervalbase = ncp->miny + pow(interval, (y + 1) * states - 1); \
226       }else{ \
227         intervalbase += (states * interval); \
228       } \
229     } \
230   } \
231   if(ncp->plot.printsample){ \
232     ncplane_set_styles(ncp->plot.ncp, ncp->plot.legendstyle); \
233     ncplane_set_channels(ncp->plot.ncp, ncp->plot.maxchannels); \
234     /* FIXME is this correct for double? */ \
235     /* we use idx, and thus get an immediate count, changing as we load it.
236      * if you want a stable summary, print the previous slot */ \
237     ncplane_printf_aligned(ncp->plot.ncp, 0, NCALIGN_RIGHT, "%" PRIu64, (uint64_t)ncp->slots[idx]); \
238   } \
239   ncplane_home(ncp->plot.ncp); \
240   struct ncvisual* ncv = ncvisual_from_rgba(pixels, dimy * states, dimx * scale * 4, dimx * scale); \
241   free(pixels); \
242   free(gvals); \
243   if(ncv == NULL){ \
244     return -1; \
245   } \
246   struct ncvisual_options vopts = { \
247     .n = ncp->plot.pixelp, \
248     .blitter = NCBLIT_PIXEL, \
249     .flags = NCVISUAL_OPTION_NODEGRADE, \
250   }; \
251   if(ncvisual_blit(ncplane_notcurses(ncp->plot.ncp), ncv, &vopts) == NULL){ \
252     ncvisual_destroy(ncv); \
253     return -1; \
254   } \
255   ncvisual_destroy(ncv); \
256   return 0; \
257 } \
258 \
259 int redraw_plot_##T(nc##X##plot* ncp){ \
260   if(ncp->plot.bset->geom == NCBLIT_PIXEL){ \
261     return redraw_pixelplot_##T(ncp); \
262   } \
263   if(calculate_gradient_vector(&ncp->plot, 0)){ \
264     return -1; \
265   } \
266   ncplane_erase(ncp->plot.ncp); \
267   const unsigned scale = ncp->plot.bset->width; \
268   unsigned dimy, dimx; \
269   ncplane_dim_yx(ncp->plot.ncp, &dimy, &dimx); \
270   const unsigned scaleddim = dimx * scale; \
271   /* each transition is worth this much change in value */ \
272   const size_t states = ncp->plot.bset->height + 1; \
273   /* FIXME can we not rid ourselves of this meddlesome double? either way, the \
274      interval is one row's range (for linear plots), or the base \
275      (base^slots == maxy-miny) of the range (for exponential plots). */ \
276   double interval; \
277   if(ncp->plot.exponentiali){ \
278     if(ncp->maxy > ncp->miny){ \
279       interval = pow(ncp->maxy - ncp->miny, (double)1 / (dimy * states)); \
280 /* fprintf(stderr, "miny: %ju maxy: %ju dimy: %d states: %zu\n", miny, maxy, dimy, states); */ \
281     }else{ \
282       interval = 0; \
283     } \
284   }else{ \
285     interval = ncp->maxy < ncp->miny ? 0 : (ncp->maxy - ncp->miny) / ((double)dimy * states); \
286   } \
287   const int startx = ncp->plot.labelaxisd ? NCPREFIXCOLUMNS : 0; /* plot cols begin here */ \
288   /* if we want fewer slots than there are available columns, our final column \
289      will be other than the plane's final column. most recent x goes here. */ \
290   const unsigned finalx = (ncp->plot.slotcount < scaleddim - 1 - (startx * scale) ? \
291                           startx + (ncp->plot.slotcount / scale) - 1 : dimx - 1); \
292   ncplane_set_styles(ncp->plot.ncp, ncp->plot.legendstyle); \
293   if(ncp->plot.labelaxisd){ \
294     /* show the *top* of each interval range */ \
295     for(unsigned y = 0 ; y < dimy ; ++y){ \
296       ncplane_set_channels(ncp->plot.ncp, ncp->plot.channels[y]); \
297       char buf[NCPREFIXSTRLEN + 1]; \
298       if(ncp->plot.exponentiali){ \
299         if(y == dimy - 1){ /* we cheat on the top row to exactly match maxy */ \
300           ncqprefix(ncp->maxy * 100, 100, buf, 0); \
301         }else{ \
302           ncqprefix(pow(interval, (y + 1) * states) * 100, 100, buf, 0); \
303         } \
304       }else{ \
305         ncqprefix((ncp->maxy - interval * states * (dimy - y - 1)) * 100, 100, buf, 0); \
306       } \
307       if(y == dimy - 1 && strlen(ncp->plot.title)){ \
308         ncplane_printf_yx(ncp->plot.ncp, dimy - y - 1, NCPREFIXCOLUMNS - strlen(buf), "%s %s", buf, ncp->plot.title); \
309       }else{ \
310         ncplane_printf_yx(ncp->plot.ncp, dimy - y - 1, NCPREFIXCOLUMNS - strlen(buf), "%s", buf); \
311       } \
312     } \
313   }else if(strlen(ncp->plot.title)){ \
314     ncplane_set_channels(ncp->plot.ncp, ncp->plot.channels[dimy - 1]); \
315     ncplane_printf_yx(ncp->plot.ncp, 0, NCPREFIXCOLUMNS - strlen(ncp->plot.title), "%s", ncp->plot.title); \
316   } \
317   ncplane_set_styles(ncp->plot.ncp, NCSTYLE_NONE); \
318   if((int)finalx < startx){ /* exit on pathologically narrow planes */ \
319     return 0; \
320   } \
321   if(!interval){ \
322     interval = 1; \
323   } \
324   int idx = ncp->plot.slotstart; /* idx holds the real slot index; we move backwards */ \
325   for(int x = finalx ; x >= startx ; --x){ \
326     /* a single column might correspond to more than 1 ('scale', up to \
327        MAXWIDTH) slots' worth of samples. prepare the working gval set. */ \
328     T gvals[MAXWIDTH]; \
329     /* load it retaining the same ordering we have in the actual array */ \
330     for(int i = scale - 1 ; i >= 0 ; --i){ \
331       gvals[i] = ncp->slots[idx]; /* clip the value at the limits of the graph */ \
332       if(gvals[i] < ncp->miny){ \
333         gvals[i] = ncp->miny; \
334       } \
335       if(gvals[i] > ncp->maxy){ \
336         gvals[i] = ncp->maxy; \
337       } \
338       /* FIXME if there are an odd number, only go up through the valid ones... */ \
339       if(--idx < 0){ \
340         idx = ncp->plot.slotcount - 1; \
341       } \
342     } \
343     /* starting from the least-significant row, progress in the more significant \
344        direction, drawing egcs from the grid specification, aborting early if \
345        we can't draw anything in a given cell. */ \
346     T intervalbase = ncp->miny; \
347     const wchar_t* egc = ncp->plot.bset->plotegcs; \
348     bool done = !ncp->plot.bset->fill; \
349     for(unsigned y = 0 ; y < dimy ; ++y){ \
350       ncplane_set_channels(ncp->plot.ncp, ncp->plot.channels[y]); \
351       size_t egcidx = 0, sumidx = 0; \
352       /* if we've got at least one interval's worth on the number of positions \
353         times the number of intervals per position plus the starting offset, \
354         we're going to print *something* */ \
355       for(unsigned i = 0 ; i < scale ; ++i){ \
356         sumidx *= states; \
357         if(intervalbase < gvals[i]){ \
358           if(ncp->plot.exponentiali){ \
359             /* we want the log-base-interval of gvals[i] */ \
360             double scaled = log(gvals[i] - ncp->miny) / log(interval); \
361             double sival = intervalbase ? log(intervalbase) / log(interval) : 0; \
362             egcidx = scaled - sival; \
363           }else{ \
364             egcidx = (gvals[i] - intervalbase) / interval; \
365           } \
366           if(egcidx >= states){ \
367             egcidx = states - 1; \
368             done = false; \
369           } \
370           sumidx += egcidx; \
371         }else{ \
372           egcidx = 0; \
373         } \
374 /* printf(stderr, "y: %d i(scale): %d gvals[%d]: %ju egcidx: %zu sumidx: %zu interval: %f intervalbase: %ju\n", y, i, i, gvals[i], egcidx, sumidx, interval, intervalbase); */ \
375       } \
376       /* if we're not UTF8, we can only arrive here via NCBLIT_1x1 (otherwise \
377         we would have errored out during construction). even then, however, \
378         we need handle ASCII differently, since it can't print full block. \
379         in ASCII mode, sumidx != 0 means swap colors and use space. in all \
380         modes, sumidx == 0 means don't do shit, since we erased earlier. */ \
381 /* if(sumidx)fprintf(stderr, "dimy: %d y: %d x: %d sumidx: %zu egc[%zu]: %lc\n", dimy, y, x, sumidx, sumidx, egc[sumidx]); */ \
382       if(sumidx){ \
383         uint64_t chan = ncp->plot.channels[y]; \
384         if(notcurses_canutf8(ncplane_notcurses(ncp->plot.ncp))){ \
385           char utf8[MB_LEN_MAX + 1]; \
386           int bytes = wctomb(utf8, egc[sumidx]); \
387           if(bytes < 0){ \
388             return -1; \
389           } \
390           utf8[bytes] = '\0'; \
391           nccell* c = ncplane_cell_ref_yx(ncp->plot.ncp, dimy - y - 1, x); \
392           cell_set_bchannel(c, ncchannels_bchannel(chan)); \
393           cell_set_fchannel(c, ncchannels_fchannel(chan)); \
394           nccell_set_styles(c, NCSTYLE_NONE); \
395           if(pool_blit_direct(&ncp->plot.ncp->pool, c, utf8, bytes, 1) <= 0){ \
396             return -1; \
397           } \
398         }else{ \
399           const uint64_t swapbg = ncchannels_bchannel(chan); \
400           const uint64_t swapfg = ncchannels_fchannel(chan); \
401           ncchannels_set_bchannel(&chan, swapfg); \
402           ncchannels_set_fchannel(&chan, swapbg); \
403           ncplane_set_channels(ncp->plot.ncp, chan); \
404           if(ncplane_putchar_yx(ncp->plot.ncp, dimy - y - 1, x, ' ') <= 0){ \
405             return -1; \
406           } \
407           ncchannels_set_bchannel(&chan, swapbg); \
408           ncchannels_set_fchannel(&chan, swapfg); \
409           ncplane_set_channels(ncp->plot.ncp, chan); \
410         } \
411       } \
412       if(done){ \
413         break; \
414       } \
415       if(ncp->plot.exponentiali){ \
416         intervalbase = ncp->miny + pow(interval, (y + 1) * states - 1); \
417       }else{ \
418         intervalbase += (states * interval); \
419       } \
420     } \
421   } \
422   if(ncp->plot.printsample){ \
423     ncplane_set_styles(ncp->plot.ncp, ncp->plot.legendstyle); \
424     ncplane_set_channels(ncp->plot.ncp, ncp->plot.maxchannels); \
425     ncplane_printf_aligned(ncp->plot.ncp, 0, NCALIGN_RIGHT, "%" PRIu64, (uint64_t)ncp->slots[idx]); \
426   } \
427   ncplane_home(ncp->plot.ncp); \
428   return 0; \
429 } \
430 \
431 static const struct blitset* \
432 create_##T(nc##X##plot* ncpp, ncplane* n, const ncplot_options* opts, const T miny, const T maxy, \
433            const T trueminy, const T truemaxy){ \
434   /* set up ->plot.ncp first so it gets destroyed on error */ \
435   ncpp->plot.ncp = n; \
436   if(ncplane_set_widget(ncpp->plot.ncp, ncpp, (void(*)(void*))nc##X##plot_destroy)){ \
437     return NULL; \
438   } \
439   ncplot_options zeroed = {}; \
440   if(!opts){ \
441     opts = &zeroed; \
442   } \
443   if(opts->flags >= (NCPLOT_OPTION_PRINTSAMPLE << 1u)){ \
444     logwarn("Provided unsupported flags %016" PRIx64 "\n", opts->flags); \
445   } \
446   /* if miny == maxy (enabling domain detection), they both must be equal to 0 */ \
447   if(miny == maxy && miny){ \
448     return NULL; \
449   } \
450   if(opts->rangex < 0){ \
451     logerror("error: supplied negative independent range %d\n", opts->rangex); \
452     return NULL; \
453   } \
454   if(maxy < miny){ \
455     logerror("error: supplied maxy < miny\n"); \
456     return NULL; \
457   } \
458   /* DETECTMAXONLY can't be used without domain detection */ \
459   if(opts->flags & NCPLOT_OPTION_DETECTMAXONLY && (miny != maxy)){ \
460     logerror("Supplied DETECTMAXONLY without domain detection"); \
461     return NULL; \
462   } \
463   const notcurses* notc = ncplane_notcurses(n); \
464   ncblitter_e blitfxn = opts ? opts->gridtype : NCBLIT_DEFAULT; \
465   if(blitfxn == NCBLIT_DEFAULT){ \
466     blitfxn = ncplot_defblitter(notc); \
467   } \
468   bool degrade_blitter = !(opts && (opts->flags & NCPLOT_OPTION_NODEGRADE)); \
469   const struct blitset* bset = lookup_blitset(&notc->tcache, blitfxn, degrade_blitter); \
470   if(bset == NULL){ \
471     return NULL; \
472   } \
473   unsigned sdimy, sdimx; \
474   ncplane_dim_yx(n, &sdimy, &sdimx); \
475   if(sdimx <= 0){ \
476     return NULL; \
477   } \
478   unsigned dimx = sdimx; \
479   ncpp->plot.title = strdup(opts->title ? opts->title : ""); \
480   ncpp->plot.rangex = opts->rangex; \
481   /* if we're sizing the plot based off the plane dimensions, scale it by the \
482      plot geometry's width for all calculations */ \
483   const unsigned scaleddim = dimx * (bset->geom == NCBLIT_PIXEL ? ncplane_pile_const(n)->cellpxx : bset->width); \
484   const unsigned scaledprefixlen = NCPREFIXCOLUMNS * (bset->geom == NCBLIT_PIXEL ? ncplane_pile_const(n)->cellpxx : bset->width); \
485   if((ncpp->plot.slotcount = ncpp->plot.rangex) == 0){ \
486     ncpp->plot.slotcount = scaleddim; \
487   } \
488   if(dimx < ncpp->plot.rangex){ \
489     ncpp->plot.slotcount = scaleddim; \
490   } \
491   ncpp->plot.legendstyle = opts->legendstyle; \
492   if( (ncpp->plot.labelaxisd = opts->flags & NCPLOT_OPTION_LABELTICKSD) ){ \
493     if(ncpp->plot.slotcount + scaledprefixlen > scaleddim){ \
494       if(scaleddim > scaledprefixlen){ \
495         ncpp->plot.slotcount = scaleddim - scaledprefixlen; \
496       } \
497     } \
498   } \
499   size_t slotsize = sizeof(*ncpp->slots) * ncpp->plot.slotcount; \
500   ncpp->slots = malloc(slotsize); \
501   if(ncpp->slots == NULL){ \
502     return NULL; \
503   } \
504   memset(ncpp->slots, 0, slotsize); \
505   ncpp->plot.maxchannels = opts->maxchannels; \
506   ncpp->plot.minchannels = opts->minchannels; \
507   ncpp->plot.bset = bset; \
508   ncpp->miny = miny; \
509   ncpp->maxy = maxy; \
510   ncpp->plot.vertical_indep = opts->flags & NCPLOT_OPTION_VERTICALI; \
511   ncpp->plot.exponentiali = opts->flags & NCPLOT_OPTION_EXPONENTIALD; \
512   ncpp->plot.detectonlymax = opts->flags & NCPLOT_OPTION_DETECTMAXONLY; \
513   ncpp->plot.printsample = opts->flags & NCPLOT_OPTION_PRINTSAMPLE; \
514   if( (ncpp->plot.detectdomain = (miny == maxy)) ){ \
515     ncpp->maxy = trueminy; \
516     if(!ncpp->plot.detectonlymax){ \
517       ncpp->miny = truemaxy; \
518     } \
519   } \
520   ncpp->plot.slotstart = 0; \
521   ncpp->plot.slotx = 0; \
522   ncpp->plot.chancount = 0; \
523   ncpp->plot.channels = NULL; \
524   if(bset->geom == NCBLIT_PIXEL){ \
525     if(create_pixelp(&ncpp->plot, n)){ \
526       return NULL; \
527     } \
528   } \
529   redraw_plot_##T(ncpp); \
530   return bset; \
531 } \
532 /* if x is less than the window, return -1, as the sample will be thrown away. \
533    if the x is within the current window, find the proper slot and update it. \
534    otherwise, the x is the newest sample. if it is obsoletes all existing slots, \
535    reset them, and write the new sample anywhere. otherwise, write it to the \
536    proper slot based on the current newest slot. */ \
537 int window_slide_##T(nc##X##plot* ncp, int64_t x){ \
538   if(x <= ncp->plot.slotx){ /* x is within window, do nothing */ \
539     return 0; \
540   } /* x is newest; we might be keeping some, might not */ \
541   int64_t xdiff = x - ncp->plot.slotx; /* the raw amount we're advancing */ \
542   ncp->plot.slotx = x; \
543   if(xdiff >= ncp->plot.slotcount){ /* we're throwing away all old samples, write to 0 */ \
544     memset(ncp->slots, 0, sizeof(*ncp->slots) * ncp->plot.slotcount); \
545     ncp->plot.slotstart = 0; \
546     return 0; \
547   } \
548   /* we're throwing away only xdiff slots, which is less than slotcount. \
549      first, we'll try to clear to the right...number to reset on the right of \
550      the circular buffer. min of (available at current or to right, xdiff) */ \
551   int slotsreset = ncp->plot.slotcount - ncp->plot.slotstart - 1; \
552   if(slotsreset > xdiff){ \
553     slotsreset = xdiff; \
554   } \
555   if(slotsreset){ \
556     memset(ncp->slots + ncp->plot.slotstart + 1, 0, slotsreset * sizeof(*ncp->slots)); \
557   } \
558   ncp->plot.slotstart = (ncp->plot.slotstart + xdiff) % ncp->plot.slotcount; \
559   xdiff -= slotsreset; \
560   if(xdiff){ /* throw away some at the beginning */ \
561     memset(ncp->slots, 0, xdiff * sizeof(*ncp->slots)); \
562   } \
563   return 0; \
564 } \
565 \
566 static int update_domain_##T(nc##X##plot* ncp, uint64_t x); \
567 static void update_sample_##T(nc##X##plot* ncp, int64_t x, T y, bool reset); \
568 \
569 /* Add to or set the value corresponding to this x. If x is beyond the current \
570    x window, the x window is advanced to include x, and values passing beyond \
571    the window are lost. The first call will place the initial window. The plot \
572    will be redrawn, but notcurses_render() is not called. */ \
573 int add_sample_##T(nc##X##plot* ncpp, int64_t x, T y){ \
574   if(x < ncpp->plot.slotx - (ncpp->plot.slotcount - 1)){ /* x is behind window, won't be counted */ \
575     return -1; \
576   } \
577   if(y == 0 && x <= ncpp->plot.slotx){ \
578     return 0; /* no need to redraw plot; nothing changed */ \
579   } \
580   if(window_slide_##T(ncpp, x)){ \
581     return -1; \
582   } \
583   update_sample_##T(ncpp, x, y, false); \
584   if(update_domain_##T(ncpp, x)){ \
585     return -1; \
586   } \
587   return redraw_plot_##T(ncpp); \
588 } \
589 int sample_##T(const nc##X##plot* ncp, int64_t x, T* y){ \
590   if(x < ncp->plot.slotx - (ncp->plot.slotcount - 1)){ /* x is behind window */ \
591     return -1; \
592   }else if(x > ncp->plot.slotx){ /* x is ahead of window */ \
593     return -1; \
594   } \
595   *y = ncp->slots[x % ncp->plot.slotcount]; \
596   return 0; \
597 }
598 
CREATE(uint64_t,u)599 CREATE(uint64_t, u)
600 CREATE(double, d)
601 
602 static void
603 ncplot_destroy(ncplot* n){
604   free(n->title);
605   if(ncplane_set_widget(n->ncp, NULL, NULL) == 0){
606     ncplane_destroy(n->ncp);
607   }
608   ncplane_destroy(n->pixelp);
609   free(n->channels);
610 }
611 
612 /* if we're doing domain detection, update the domain to reflect the value we
613    just set. if we're not, check the result against the known ranges, and
614    return -1 if the value is outside of that range. */
update_domain_uint64_t(ncuplot * ncp,uint64_t x)615 int update_domain_uint64_t(ncuplot* ncp, uint64_t x){
616   const uint64_t val = ncp->slots[x % ncp->plot.slotcount];
617   if(ncp->plot.detectdomain){
618     if(val > ncp->maxy){
619       ncp->maxy = val;
620     }
621     if(!ncp->plot.detectonlymax){
622       if(val < ncp->miny){
623         ncp->miny = val;
624       }
625     }
626     return 0;
627   }
628   if(val > ncp->maxy || val < ncp->miny){
629     return -1;
630   }
631   return 0;
632 }
633 
update_domain_double(ncdplot * ncp,uint64_t x)634 int update_domain_double(ncdplot* ncp, uint64_t x){
635   const double val = ncp->slots[x % ncp->plot.slotcount];
636   if(ncp->plot.detectdomain){
637     if(val > ncp->maxy){
638       ncp->maxy = val;
639     }
640     if(!ncp->plot.detectonlymax){
641       if(val < ncp->miny){
642         ncp->miny = val;
643       }
644     }
645     return 0;
646   }
647   if(val > ncp->maxy || val < ncp->miny){
648     return -1;
649   }
650   return 0;
651 }
652 
653 /* x must be within n's window at this point */
654 static void
update_sample_uint64_t(ncuplot * ncp,int64_t x,uint64_t y,bool reset)655 update_sample_uint64_t(ncuplot* ncp, int64_t x, uint64_t y, bool reset){
656   const int64_t diff = ncp->plot.slotx - x; /* amount behind */
657   const int idx = (ncp->plot.slotstart + ncp->plot.slotcount - diff) % ncp->plot.slotcount;
658   if(reset){
659     ncp->slots[idx] = y;
660   }else{
661     ncp->slots[idx] += y;
662   }
663 }
664 
665 /* x must be within n's window at this point */
666 static void
update_sample_double(ncdplot * ncp,int64_t x,double y,bool reset)667 update_sample_double(ncdplot* ncp, int64_t x, double y, bool reset){
668   const int64_t diff = ncp->plot.slotx - x; /* amount behind */
669   const int idx = (ncp->plot.slotstart + ncp->plot.slotcount - diff) % ncp->plot.slotcount;
670   if(reset){
671     ncp->slots[idx] = y;
672   }else{
673     ncp->slots[idx] += y;
674   }
675 }
676 
677 // takes ownership of n on all paths
ncuplot_create(ncplane * n,const ncplot_options * opts,uint64_t miny,uint64_t maxy)678 ncuplot* ncuplot_create(ncplane* n, const ncplot_options* opts, uint64_t miny, uint64_t maxy){
679   ncuplot* ret = malloc(sizeof(*ret));
680   if(ret == NULL){
681     ncplane_destroy(n);
682     return NULL;
683   }
684   memset(ret, 0, sizeof(*ret));
685   const struct blitset* bset = create_uint64_t(ret, n, opts, miny, maxy, 0, UINT64_MAX);
686   if(bset == NULL){ // create_uint64_t() destroys n on error
687     ncuplot_destroy(ret);
688     return NULL;
689   }
690   return ret;
691 }
692 
ncuplot_plane(ncuplot * n)693 ncplane* ncuplot_plane(ncuplot* n){
694   return n->plot.ncp;
695 }
696 
ncuplot_add_sample(ncuplot * n,uint64_t x,uint64_t y)697 int ncuplot_add_sample(ncuplot* n, uint64_t x, uint64_t y){
698   return add_sample_uint64_t(n, x, y);
699 }
700 
ncuplot_set_sample(ncuplot * n,uint64_t x,uint64_t y)701 int ncuplot_set_sample(ncuplot* n, uint64_t x, uint64_t y){
702   if(window_slide_uint64_t(n, x)){
703     return -1;
704   }
705   update_sample_uint64_t(n, x, y, true);
706   if(update_domain_uint64_t(n, x)){
707     return -1;
708   }
709   return redraw_plot_uint64_t(n);
710 }
711 
ncuplot_destroy(ncuplot * n)712 void ncuplot_destroy(ncuplot* n){
713   if(n){
714     ncplot_destroy(&n->plot);
715     free(n->slots);
716     free(n);
717   }
718 }
719 
720 // takes ownership of n on all paths
ncdplot_create(ncplane * n,const ncplot_options * opts,double miny,double maxy)721 ncdplot* ncdplot_create(ncplane* n, const ncplot_options* opts, double miny, double maxy){
722   ncdplot* ret = malloc(sizeof(*ret));
723   if(ret == NULL){
724     ncplane_destroy(n);
725     return NULL;
726   }
727   memset(ret, 0, sizeof(*ret));
728   const struct blitset* bset = create_double(ret, n, opts, miny, maxy, -DBL_MAX, DBL_MAX);
729   if(bset == NULL){ // create_double() destroys n on error
730     ncdplot_destroy(ret);
731     return NULL;
732   }
733   return ret;
734 }
735 
ncdplot_plane(ncdplot * n)736 ncplane* ncdplot_plane(ncdplot* n){
737   return n->plot.ncp;
738 }
739 
ncdplot_add_sample(ncdplot * n,uint64_t x,double y)740 int ncdplot_add_sample(ncdplot* n, uint64_t x, double y){
741   return add_sample_double(n, x, y);
742 }
743 
ncdplot_set_sample(ncdplot * n,uint64_t x,double y)744 int ncdplot_set_sample(ncdplot* n, uint64_t x, double y){
745   if(window_slide_double(n, x)){
746     return -1;
747   }
748   update_sample_double(n, x, y, true);
749   if(update_domain_double(n, x)){
750     return -1;
751   }
752   return redraw_plot_double(n);
753 }
754 
ncuplot_sample(const ncuplot * n,uint64_t x,uint64_t * y)755 int ncuplot_sample(const ncuplot* n, uint64_t x, uint64_t* y){
756   return sample_uint64_t(n, x, y);
757 }
758 
ncdplot_sample(const ncdplot * n,uint64_t x,double * y)759 int ncdplot_sample(const ncdplot* n, uint64_t x, double* y){
760   return sample_double(n, x, y);
761 }
762 
ncdplot_destroy(ncdplot * n)763 void ncdplot_destroy(ncdplot* n) {
764   if(n){
765     ncplot_destroy(&n->plot);
766     free(n->slots);
767     free(n);
768   }
769 }
770