Floating-point ncplot, genericize ncplot (#531)

* compile ncplot as c++ generic #446
* add floating-point plots #446
This commit is contained in:
Nick Black 2020-04-23 03:52:07 -04:00 committed by GitHub
parent 5a017574f6
commit 0e73b9d3d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 564 additions and 443 deletions

View File

@ -1,6 +1,11 @@
This document attempts to list user-visible changes and any major internal
rearrangements of Notcurses.
* 1.3.3 (not yet released)
* The `ncdplot` type has been added for plots based on `double`s rather than
`uint64_t`s. The `ncplot` type and all `ncplot_*` functions were renamed
`ncuplot` for symmetry.
* 1.3.2 (2020-04-19)
* `ncdirect_cursor_push()`, `notcurses_cursor_pop()`, and
`ncdirect_cursor_yx()` have been added. These are not supported on all

View File

@ -37,8 +37,8 @@ if(${USE_TESTS})
find_package(doctest 2.3.5 REQUIRED)
endif()
# libnotcurses
file(GLOB NCSRCS CONFIGURE_DEPENDS src/lib/*.c)
# libnotcurses (core shared library and static library)
file(GLOB NCSRCS CONFIGURE_DEPENDS src/lib/*.c src/lib/*.cpp)
add_library(notcurses SHARED ${NCSRCS})
add_library(notcurses-static STATIC ${NCSRCS})
set_target_properties(
@ -599,7 +599,7 @@ if(${USE_PYTHON})
OUTPUT
"${CMAKE_CURRENT_BINARY_DIR}/build/pytimestamp"
COMMAND
env LDFLAGS="-L${CMAKE_CURRENT_BINARY_DIR}" "${Python3_EXECUTABLE}" ${SETUP_PY} build &&
env LDFLAGS="-Wl,-soname,_notcurses.so.1 -L${CMAKE_CURRENT_BINARY_DIR}" "${Python3_EXECUTABLE}" ${SETUP_PY} build &&
"${Python3_EXECUTABLE}" ${SETUP_PY} egg_info
DEPENDS
${PYSRC} ${SETUP_PY} ${SETUP_CFG} notcurses

View File

@ -12,11 +12,13 @@ notcurses_plot - high level widget for plotting
```c
typedef enum {
NCPLOT_1x1, // full block █
NCPLOT_2x1, // full/lower blocks █▄
NCPLOT_1x1x4, // shaded full blocks █▓▒░
NCPLOT_4x1, // four vert levels █▆▄▂
NCPLOT_8x1, // eight vert levels █▇▆▅▄▃▂▁
NCPLOT_1x1, // full block █
NCPLOT_2x1, // full/(upper|left) blocks ▄█
NCPLOT_1x1x4, // shaded full blocks ▓▒░█
NCPLOT_2x2, // quadrants ▗▐ ▖▄▟▌▙█
NCPLOT_4x1, // four vert/horz levels █▆▄▂ / ▎▌▊█
NCPLOT_4x2, // 4x2-way braille ⡀⡄⡆⡇⢀⣀⣄⣆⣇⢠⣠⣤⣦⣧⢰⣰⣴⣶⣷⢸⣸⣼⣾⣿
NCPLOT_8x1, // eight vert/horz levels █▇▆▅▄▃▂▁ / ▏▎▍▌▋▊▉█
} ncgridgeom_e;
typedef struct ncplot_options {
@ -28,38 +30,74 @@ typedef struct ncplot_options {
ncgridgeom_e gridtype;
// independent variable is a contiguous range
int rangex;
// dependent min and max. set both equal to 0 to
// use domain autodiscovery.
uint64_t miny, maxy;
bool labelaxisd; // label dependent axis
bool exponentialy; // is dependent exponential?
bool exponentially; // is dependent exponential?
bool vertical_indep; // vertical independent variable
} ncplot_options;
```
**struct ncplot* ncplot_create(struct ncplane* n, const ncplot_options* opts);**
**struct ncuplot* ncuplot_create(struct ncplane* n, const ncplot_options* opts, uint64_t miny, uint64_t maxy);**
**struct ncdplot* ncdplot_create(struct ncplane* n, const ncplot_options* opts, double miny, double maxy);**
**struct ncplane* ncplot_plane(struct ncplot* n);**
**struct ncplane* ncuplot_plane(struct ncuplot* n);**
**struct ncplane* ncdplot_plane(struct ncdplot* n);**
**int ncplot_add_sample(struct ncplot* n, uint64_t x, uint64_t y);**
**int ncplot_set_sample(struct ncplot* n, uint64_t x, uint64_t y);**
**int ncuplot_add_sample(struct ncuplot* n, uint64_t x, uint64_t y);**
**int ncdplot_add_sample(struct ncdplot* n, uint64_t x, double y);**
**void ncplot_destroy(struct ncplot* n);**
**int ncuplot_set_sample(struct ncuplot* n, uint64_t x, uint64_t y);**
**int ncdplot_set_sample(struct ncdplot* n, uint64_t x, double y);**
**void ncuplot_destroy(struct ncuplot* n);**
**void ncdplot_destroy(struct ncdplot* n);**
# DESCRIPTION
These functions support histograms. The independent variable is always an
**uint64_t**. The samples are either **uint64_t**s (**ncuplot**) or **double**s
(**ncdplot**). Only a window over the samples is retained at any given time,
and this window can only move towards larger values of the independent
variable. The window is moved forward whenever an **x** larger than the current
window's maximum is supplied to **add_sample** or **set_sample**.
**add_sample** increments the current value corresponding to this **x** by
**y**. **set_sample** replaces the current value corresponding to this **x**.
If **rangex** is 0, or larger than the bound plane will support, it is capped
to the available space. The domain can either be specified as **miny** and
**maxy**, or domain autodetection can be invoked via setting both to 0. If the
domain is specified, samples outside the domain are an error, and do not
contribute to the plot. Supplying an **x** below the current window is an
error, and has no effect.
The different **ncgridgeom_e** values select from among available glyph sets:
* **NCPLOT_1x1**: Full block (█) or empty glyph
* **NCPLOT_2x1**: Adds the lower half block (▄) to **NCPLOT_1x1**.
* **NCPLOT_1x1x4**: Adds three shaded full blocks (▓▒░) to **NCPLOT_1x1**.
* **NCPLOT_2x2**: Adds left and right half blocks (▌▐) and quadrants (▖▗▟▙) to **NCPLOT_2x1**.
* **NCPLOT_4x1**: Adds ¼ and ¾ blocks (▂▆) to **NCPLOT_2x1**.
* **NCPLOT_4x2**: 4 rows and 2 columns of braille (⡀⡄⡆⡇⢀⣀⣄⣆⣇⢠⣠⣤⣦⣧⢰⣰⣴⣶⣷⢸⣸⣼⣾⣿).
* **NCPLOT_8x1**: Adds ⅛, ⅜, ⅝, and ⅞ blocks (▇▅▃▁) to **NCPLOT_4x1**.
More granular block glyphs means more resolution in your plots, but they can
be difficult to differentiate at small text sizes. Quadrants and braille allow
for more resolution on the independent variable. It can be difficult to predict
how the braille glyphs will look in a given font.
The same **ncplot_options** struct can be used with all ncplot types.
# NOTES
Neither **exponentialy** not **vertical_indep** is yet implemented.
Neither **exponentially** not **vertical_indep** is yet implemented.
# RETURN VALUES
**ncplot_create** will return an error if **miny** equals **maxy**, but they
are non-zero. It will also return an error if **maxy** < **miny**. An invalid
**create** will return an error if **miny** equals **maxy**, but they are
non-zero. It will also return an error if **maxy** < **miny**. An invalid
**gridtype** will result in an error.
**ncplot_plane** returns the **ncplane** on which the plot is drawn. It cannot
fail.
**plane** returns the **ncplane** on which the plot is drawn. It cannot fail.
# SEE ALSO

View File

@ -32,12 +32,13 @@ namespace ncpp
: Plot (const_cast<Plane*>(&plane), opts)
{}
explicit Plot (ncplane *plane, const ncplot_options *opts = nullptr)
explicit Plot (ncplane *plane, const ncplot_options *opts = nullptr,
uint64_t miny = 0, uint64_t maxy = 0)
{
if (plane == nullptr)
throw invalid_argument ("'plane' must be a valid pointer");
plot = ncplot_create (plane, opts == nullptr ? &default_options : opts);
plot = ncuplot_create (plane, opts == nullptr ? &default_options : opts, miny, maxy);
if (plot == nullptr)
throw init_error ("notcurses failed to create a new plot");
}
@ -45,23 +46,23 @@ namespace ncpp
~Plot ()
{
if (!is_notcurses_stopped ())
ncplot_destroy (plot);
ncuplot_destroy (plot);
}
bool add_sample(uint64_t x, uint64_t y) const NOEXCEPT_MAYBE
{
return error_guard (ncplot_add_sample (plot, x, y), -1);
return error_guard (ncuplot_add_sample (plot, x, y), -1);
}
bool set_sample(uint64_t x, uint64_t y) const NOEXCEPT_MAYBE
{
return error_guard (ncplot_set_sample (plot, x, y), -1);
return error_guard (ncuplot_set_sample (plot, x, y), -1);
}
Plane* get_plane () const noexcept;
private:
ncplot *plot;
ncuplot *plot;
};
}
#endif

View File

@ -31,7 +31,8 @@ struct notcurses; // notcurses state for a given terminal, composed of ncplanes
struct ncplane; // a drawable notcurses surface, composed of cells
struct cell; // a coordinate on an ncplane: an EGC plus styling
struct ncvisual; // a visual bit of multimedia opened with LibAV
struct ncplot; // a histogram, bound to a plane
struct ncuplot; // a histogram, bound to a plane (uint64_ts)
struct ncdplot; // a histogram, bound to a plane (non-negative doubles)
struct ncfdplane; // i/o wrapper to dump file descriptor to plane
struct ncsubproc; // ncfdplane wrapper with subprocess management
struct ncselector;// widget supporting selecting 1 from a list of options
@ -2618,6 +2619,9 @@ typedef enum {
//
// The 20 levels at first is a special case. When the domain is only 1 unit,
// and autoscaling is in play, assign 50%.
//
// This options structure works for both the ncuplot (uint64_t) and ncdplot
// (double) types.
typedef struct ncplot_options {
// channels for the maximum and minimum levels. linear interpolation will be
// applied across the domain between these two.
@ -2629,28 +2633,35 @@ typedef struct ncplot_options {
// resolution, the independent variable would be the range [0..3600): 3600.
// if rangex is 0, it is dynamically set to the number of columns.
int rangex;
uint64_t miny, maxy; // y axis min and max. for autodiscovery, set them equal.
bool labelaxisd; // generate labels for the dependent axis
bool exponentialy; // is y-axis exponential? (not yet implemented)
bool exponentially; // is y-axis exponential? (not yet implemented)
// independent variable is vertical rather than horizontal
bool vertical_indep;
} ncplot_options;
// Use the provided plane 'n' for plotting according to the options 'opts'.
// The plot will make free use of the entirety of the plane.
API struct ncplot* ncplot_create(struct ncplane* n, const ncplot_options* opts);
// for domain autodiscovery, set miny == maxy == 0.
API struct ncuplot* ncuplot_create(struct ncplane* n, const ncplot_options* opts,
uint64_t miny, uint64_t maxy);
API struct ncdplot* ncdplot_create(struct ncplane* n, const ncplot_options* opts,
double miny, double maxy);
// Return a reference to the ncplot's underlying ncplane.
API struct ncplane* ncplot_plane(struct ncplot* n);
API struct ncplane* ncuplot_plane(struct ncuplot* n);
API struct ncplane* ncdplot_plane(struct ncdplot* n);
// Add to or set the value corresponding to this x. If x is beyond the current
// x window, the x window is advanced to include x, and values passing beyond
// the window are lost. The first call will place the initial window. The plot
// will be redrawn, but notcurses_render() is not called.
API int ncplot_add_sample(struct ncplot* n, uint64_t x, uint64_t y);
API int ncplot_set_sample(struct ncplot* n, uint64_t x, uint64_t y);
API int ncuplot_add_sample(struct ncuplot* n, uint64_t x, uint64_t y);
API int ncdplot_add_sample(struct ncdplot* n, uint64_t x, double y);
API int ncuplot_set_sample(struct ncuplot* n, uint64_t x, uint64_t y);
API int ncdplot_set_sample(struct ncdplot* n, uint64_t x, double y);
API void ncplot_destroy(struct ncplot* n);
API void ncuplot_destroy(struct ncuplot* n);
API void ncdplot_destroy(struct ncdplot* n);
typedef int(*ncfdplane_callback)(struct ncfdplane* n, const void* buf, size_t s, void* curry);
typedef int(*ncfdplane_done_cb)(struct ncfdplane* n, int fderrno, void* curry);

View File

@ -415,26 +415,32 @@ void ncplane_translate(const struct ncplane* src, const struct ncplane* dst, int
bool ncplane_translate_abs(const struct ncplane* n, int* y, int* x);
typedef enum {
NCPLOT_1x1, // full block
NCPLOT_2x1, // full/(upper|left) blocks
NCPLOT_1x1x4, // shaded full blocks
NCPLOT_2x1, // full/(upper|left) blocks
NCPLOT_1x1x4, // shaded full blocks
NCPLOT_2x2, // quadrants
NCPLOT_4x1, // four vert/horz levels /
NCPLOT_8x1, // eight vert/horz levels /
NCPLOT_4x2, // 4 rows, 2 cols (braille)
NCPLOT_8x1, // eight vert/horz levels /
} ncgridgeom_e;
typedef struct ncplot_options {
uint64_t maxchannel;
uint64_t minchannel;
ncgridgeom_e gridtype;
uint64_t rangex;
uint64_t miny, maxy;
bool labelaxisd;
bool exponentialy;
bool exponentially;
bool vertical_indep;
} ncplot_options;
struct ncplot* ncplot_create(struct ncplane* n, const ncplot_options* opts);
struct ncplane* ncplot_plane(struct ncplot* n);
int ncplot_add_sample(struct ncplot* n, uint64_t x, uint64_t y);
int ncplot_set_sample(struct ncplot* n, uint64_t x, uint64_t y);
void ncplot_destroy(struct ncplot* n);
struct ncuplot* ncuplot_create(struct ncplane* n, const ncplot_options* opts, uint64_t miny, uint64_t maxy);
struct ncdplot* ncdplot_create(struct ncplane* n, const ncplot_options* opts, double miny, double maxy);
struct ncplane* ncuplot_plane(struct ncuplot* n);
struct ncplane* ncdplot_plane(struct ncdplot* n);
int ncuplot_add_sample(struct ncuplot* n, uint64_t x, uint64_t y);
int ncdplot_add_sample(struct ncdplot* n, uint64_t x, double y);
int ncuplot_set_sample(struct ncuplot* n, uint64_t x, uint64_t y);
int ncdplot_set_sample(struct ncdplot* n, uint64_t x, double y);
void ncuplot_destroy(struct ncuplot* n);
void ncdplot_destroy(struct ncdplot* n);
bool ncplane_set_scrolling(struct ncplane* n, bool scrollp);
""")

View File

@ -29,7 +29,7 @@ std::mutex mtx;
uint64_t start;
static int dimy, dimx;
std::atomic<bool> done;
static struct ncplot* plot;
static struct ncuplot* plot;
// return the string version of a special composed key
const char* nckeystr(char32_t spkey){
@ -187,7 +187,7 @@ dim_rows(const Plane* n){
void Tick(ncpp::NotCurses* nc, uint64_t sec) {
const std::lock_guard<std::mutex> lock(mtx);
if(ncplot_add_sample(plot, sec, 0)){
if(ncuplot_add_sample(plot, sec, 0)){
throw std::runtime_error("couldn't register timetick");
}
if(!nc->render()){
@ -220,7 +220,7 @@ int main(void){
channels_set_fg_rgb(&popts.minchannel, 0x40, 0x50, 0xb0);
channels_set_fg_rgb(&popts.maxchannel, 0x40, 0xff, 0xd0);
popts.gridtype = static_cast<ncgridgeom_e>(NCPLOT_2x2);
plot = ncplot_create(pplane, &popts);
plot = ncuplot_create(pplane, &popts, 0, 0);
if(!plot){
return EXIT_FAILURE;
}
@ -295,7 +295,7 @@ int main(void){
}
const uint64_t sec = (timenow_to_ns() - start) / NANOSECS_IN_SEC;
mtx.lock();
if(ncplot_add_sample(plot, sec, 1)){
if(ncuplot_add_sample(plot, sec, 1)){
mtx.unlock();
break;
}

303
src/lib/cpp.h Normal file
View File

@ -0,0 +1,303 @@
#include "notcurses/notcurses.h"
#include <limits>
static const struct {
ncgridgeom_e geom;
int width;
int height;
// the EGCs which form the various levels of a given geometry. if the geometry
// is wide, things are arranged with the rightmost side increasing most
// quickly, i.e. it can be indexed as height arrays of 1 + height glyphs. i.e.
// the first five braille EGCs are all 0 on the left, [0..4] on the right.
const wchar_t* egcs;
bool fill;
} geomdata[] = {
{ .geom = NCPLOT_1x1, .width = 1, .height = 2, .egcs = L"", .fill = false, },
{ .geom = NCPLOT_2x1, .width = 1, .height = 3, .egcs = L" ▄█", .fill = false, },
{ .geom = NCPLOT_1x1x4, .width = 1, .height = 5, .egcs = L" ▒░▓█", .fill = false, },
{ .geom = NCPLOT_2x2, .width = 2, .height = 3, .egcs = L" ▗▐▖▄▟▌▙█", .fill = false, },
{ .geom = NCPLOT_4x1, .width = 1, .height = 5, .egcs = L" ▂▄▆█", .fill = false, },
{ .geom = NCPLOT_4x2, .width = 2, .height = 5, .egcs = L"⠀⡀⡄⡆⡇⢀⣀⣄⣆⣇⢠⣠⣤⣦⣧⢰⣰⣴⣶⣷⢸⣸⣼⣾⣿", .fill = true, },
{ .geom = NCPLOT_8x1, .width = 1, .height = 9, .egcs = L" ▁▂▃▄▅▆▇█", .fill = false, },
};
template<typename T>
class ncppplot {
public:
// these were all originally plain C, sorry for the non-idiomatic usage FIXME
static bool create(ncppplot<T>* ncpp, ncplane* n, const ncplot_options* opts, T miny, T maxy){
// if miny == maxy, they both must be equal to 0
if(miny == maxy && miny){
return NULL;
}
if(opts->rangex < 0){
return NULL;
}
if(maxy < miny){
return NULL;
}
if(opts->gridtype < 0 || opts->gridtype >= sizeof(geomdata) / sizeof(*geomdata)){
return NULL;
}
int sdimy, sdimx;
ncplane_dim_yx(n, &sdimy, &sdimx);
if(sdimx <= 0){
return NULL;
}
int dimx = sdimx;
ncpp->rangex = opts->rangex;
// if we're sizing the plot based off the plane dimensions, scale it by the
// plot geometry's width for all calculations
const int scaleddim = dimx * geomdata[opts->gridtype].width;
const int scaledprefixlen = PREFIXSTRLEN * geomdata[opts->gridtype].width;
if((ncpp->slotcount = ncpp->rangex) == 0){
ncpp->slotcount = scaleddim;
}
if(dimx < ncpp->rangex){
ncpp->slotcount = scaleddim;
}
if( (ncpp->labelaxisd = opts->labelaxisd) ){
if(ncpp->slotcount + scaledprefixlen > scaleddim){
if(scaleddim > scaledprefixlen){
ncpp->slotcount = scaleddim - scaledprefixlen;
}
}
}
size_t slotsize = sizeof(*ncpp->slots) * ncpp->slotcount;
ncpp->slots = static_cast<T*>(malloc(slotsize));
if(ncpp->slots){
memset(ncpp->slots, 0, slotsize);
ncpp->ncp = n;
ncpp->maxchannel = opts->maxchannel;
ncpp->minchannel = opts->minchannel;
ncpp->miny = miny;
ncpp->maxy = maxy;
ncpp->vertical_indep = opts->vertical_indep;
ncpp->gridtype = opts->gridtype;
ncpp->exponentially = opts->exponentially;
if( (ncpp->detectdomain = (miny == maxy)) ){
ncpp->maxy = 0;
ncpp->miny = std::numeric_limits<T>::max();
}
ncpp->slotstart = 0;
ncpp->slotx = 0;
ncpp->redraw_plot();
return true;
}
return false;
}
// Add to or set the value corresponding to this x. If x is beyond the current
// x window, the x window is advanced to include x, and values passing beyond
// the window are lost. The first call will place the initial window. The plot
// will be redrawn, but notcurses_render() is not called.
int add_sample(uint64_t x, T y){
if(window_slide(x)){
return -1;
}
update_sample(x, y, false);
if(update_domain(x)){
return -1;
}
return redraw_plot();
}
int set_sample(uint64_t x, T y){
if(window_slide(x)){
return -1;
}
update_sample(x, y, true);
if(update_domain(x)){
return -1;
}
return redraw_plot();
}
void destroy(){
free(slots);
}
// FIXME everything below here ought be private, but it busts unit tests
int redraw_plot(){
ncplane_erase(ncp);
const int scale = geomdata[gridtype].width;
int dimy, dimx;
ncplane_dim_yx(ncp, &dimy, &dimx);
const int scaleddim = dimx * scale;
// each transition is worth this much change in value
const size_t states = geomdata[gridtype].height;
// FIXME can we not rid ourselves of this meddlesome double?
double interval = maxy < miny ? 0 : (maxy - miny) / ((double)dimy * states);
const int startx = labelaxisd ? PREFIXSTRLEN : 0; // plot cols begin here
// if we want fewer slots than there are available columns, our final column
// will be other than the plane's final column. most recent x goes here.
const int finalx = (slotcount < scaleddim - 1 - (startx * scale) ? startx + (slotcount / scale) - 1 : dimx - 1);
if(labelaxisd){
// show the *top* of each interval range
for(int y = 0 ; y < dimy ; ++y){
char buf[PREFIXSTRLEN + 1];
ncmetric(interval * states * (y + 1) * 100, 100, buf, 0, 1000, '\0');
ncplane_putstr_yx(ncp, dimy - y - 1, PREFIXSTRLEN - strlen(buf), buf);
}
}
if(finalx < startx){ // exit on pathologically narrow planes
return 0;
}
if(!interval){
interval = 1;
}
#define MAXWIDTH 2
int idx = slotstart; // idx holds the real slot index; we move backwards
for(int x = finalx ; x >= startx ; --x){
T gvals[MAXWIDTH];
// load it retaining the same ordering we have in the actual array
for(int i = scale - 1 ; i >= 0 ; --i){
gvals[i] = slots[idx]; // clip the value at the limits of the graph
if(gvals[i] < miny){
gvals[i] = miny;
}
if(gvals[i] > maxy){
gvals[i] = maxy;
}
// FIXME if there are an odd number, only go up through the valid ones...
if(--idx < 0){
idx = slotcount - 1;
}
}
// starting from the least-significant row, progress in the more significant
// direction, drawing egcs from the grid specification, aborting early if
// we can't draw anything in a given cell.
double intervalbase = miny;
const wchar_t* egc = geomdata[gridtype].egcs;
for(int y = 0 ; y < dimy ; ++y){
size_t egcidx, sumidx = 0;
// if we've got at least one interval's worth on the number of positions
// times the number of intervals per position plus the starting offset,
// we're going to print *something*
bool done = !geomdata[gridtype].fill;
for(int i = 0 ; i < scale ; ++i){
sumidx *= states;
if(intervalbase < gvals[i]){
egcidx = (gvals[i] - intervalbase) / interval;
if(egcidx >= states){
egcidx = states - 1;
}
done = false;
sumidx += egcidx;
}else{
egcidx = 0;
}
}
if(done){
break;
}
if(ncplane_putwc_yx(ncp, dimy - y - 1, x, egc[sumidx]) <= 0){
return -1;
}
intervalbase += (states * interval);
}
}
if(ncplane_cursor_move_yx(ncp, 0, 0)){
return -1;
}
if(ncplane_stain(ncp, dimy - 1, dimx - 1, maxchannel, maxchannel,
minchannel, minchannel) <= 0){
return -1;
}
return 0;
}
// if we're doing domain detection, update the domain to reflect the value we
// just set. if we're not, check the result against the known ranges, and
// return -1 if the value is outside of that range.
int update_domain(uint64_t x){
const uint64_t val = slots[x % slotcount];
if(detectdomain){
if(val > maxy){
maxy = val;
}
if(val < miny){
miny = val;
}
return 0;
}
if(val > maxy || val < miny){
return -1;
}
return 0;
}
// if x is less than the window, return -1, as the sample will be thrown away.
// if the x is within the current window, find the proper slot and update it.
// otherwise, the x is the newest sample. if it is obsoletes all existing slots,
// reset them, and write the new sample anywhere. otherwise, write it to the
// proper slot based on the current newest slot.
int window_slide(int64_t x){
if(x < slotx - (slotcount - 1)){ // x is behind window, won't be counted
return -1;
}else if(x <= slotx){ // x is within window, do nothing
return 0;
} // x is newest; we might be keeping some, might not
int64_t xdiff = x - slotx; // the raw amount we're advancing
slotx = x;
if(xdiff >= slotcount){ // we're throwing away all old samples, write to 0
memset(slots, 0, sizeof(*slots) * slotcount); // FIXME need a STL operation?
slotstart = 0;
return 0;
}
// we're throwing away only xdiff slots, which is less than slotcount.
// first, we'll try to clear to the right...number to reset on the right of
// the circular buffer. min of (available at current or to right, xdiff)
int slotsreset = slotcount - slotstart - 1;
if(slotsreset > xdiff){
slotsreset = xdiff;
}
if(slotsreset){
memset(slots + slotstart + 1, 0, slotsreset * sizeof(*slots));
}
slotstart = (slotstart + xdiff) % slotcount;
xdiff -= slotsreset;
if(xdiff){ // throw away some at the beginning
memset(slots, 0, xdiff * sizeof(*slots));
}
return 0;
}
// x must be within n's window at this point
inline void update_sample(int64_t x, T y, bool reset){
const int64_t diff = slotx - x; // amount behind
const int idx = (slotstart + slotcount - diff) % slotcount;
if(reset){
slots[idx] = y;
}else{
slots[idx] += y;
}
}
ncplane* ncp;
uint64_t maxchannel;
uint64_t minchannel;
bool vertical_indep; // not yet implemented FIXME
ncgridgeom_e gridtype;
// requested number of slots. 0 for automatically setting the number of slots
// to span the horizontal area. if there are more slots than there are
// columns, we prefer showing more recent slots to less recent. if there are
// fewer slots than there are columns, they prefer the left side.
int rangex;
// domain minimum and maximum. if detectdomain is true, these are
// progressively enlarged/shrunk to fit the sample set. if not, samples
// outside these bounds are counted, but the displayed range covers only this.
T miny, maxy;
// sloutcount-element circular buffer of samples. the newest one (rightmost)
// is at slots[slotstart]; they get older as you go back (and around).
// elements. slotcount is max(columns, rangex), less label room.
T* slots;
int slotcount;
int slotstart; // index of most recently-written slot
int64_t slotx; // x value corresponding to slots[slotstart] (newest x)
bool labelaxisd; // label dependent axis (consumes PREFIXSTRLEN columns)
bool exponentially; // not yet implemented FIXME
bool detectdomain; // is domain detection in effect (stretch the domain)?
};

View File

@ -160,33 +160,6 @@ typedef struct ncmenu_int_section {
int shortcut_offset; // column offset within name of shortcut EGC
} ncmenu_int_section;
typedef struct ncplot {
ncplane* ncp;
uint64_t maxchannel;
uint64_t minchannel;
bool vertical_indep; // not yet implemented FIXME
ncgridgeom_e gridtype;
// requested number of slots. 0 for automatically setting the number of slots
// to span the horizontal area. if there are more slots than there are
// columns, we prefer showing more recent slots to less recent. if there are
// fewer slots than there are columns, they prefer the left side.
int rangex;
// domain minimum and maximum. if detectdomain is true, these are
// progressively enlarged/shrunk to fit the sample set. if not, samples
// outside these bounds are counted, but the displayed range covers only this.
uint64_t miny, maxy;
// sloutcount-element circular buffer of samples. the newest one (rightmost)
// is at slots[slotstart]; they get older as you go back (and around).
// elements. slotcount is max(columns, rangex), less label room.
uint64_t* slots;
int slotcount;
int slotstart; // index of most recently-written slot
int64_t slotx; // x value corresponding to slots[slotstart] (newest x)
bool labelaxisd; // label dependent axis (consumes PREFIXSTRLEN columns)
bool exponentialy; // not yet implemented FIXME
bool detectdomain; // is domain detection in effect (stretch the domain)?
} ncplot;
typedef struct ncfdplane {
ncfdplane_callback cb; // invoked with fresh hot data
ncfdplane_done_cb donecb; // invoked on EOF (if !follow) or error

View File

@ -1,284 +0,0 @@
#include "internal.h"
static const struct {
ncgridgeom_e geom;
int width;
int height;
// the EGCs which form the various levels of a given geometry. if the geometry
// is wide, things are arranged with the rightmost side increasing most
// quickly, i.e. it can be indexed as height arrays of 1 + height glyphs. i.e.
// the first five braille EGCs are all 0 on the left, [0..4] on the right.
const wchar_t* egcs;
bool fill;
} geomdata[] = {
{ .geom = NCPLOT_1x1, .width = 1, .height = 2, .egcs = L"", .fill = false, },
{ .geom = NCPLOT_2x1, .width = 1, .height = 3, .egcs = L" ▄█", .fill = false, },
{ .geom = NCPLOT_1x1x4, .width = 1, .height = 5, .egcs = L" ▒░▓█", .fill = false, },
{ .geom = NCPLOT_2x2, .width = 2, .height = 3, .egcs = L" ▗▐▖▄▟▌▙█", .fill = false, },
{ .geom = NCPLOT_4x1, .width = 1, .height = 5, .egcs = L" ▂▄▆█", .fill = false, },
{ .geom = NCPLOT_4x2, .width = 2, .height = 5, .egcs = L"⠀⡀⡄⡆⡇⢀⣀⣄⣆⣇⢠⣠⣤⣦⣧⢰⣰⣴⣶⣷⢸⣸⣼⣾⣿", .fill = true, },
{ .geom = NCPLOT_8x1, .width = 1, .height = 9, .egcs = L" ▁▂▃▄▅▆▇█", .fill = false, },
};
static int
redraw_plot(ncplot* n){
ncplane_erase(ncplot_plane(n));
const int scale = geomdata[n->gridtype].width;
int dimy, dimx;
ncplane_dim_yx(ncplot_plane(n), &dimy, &dimx);
const int scaleddim = dimx * scale;
// each transition is worth this much change in value
const size_t states = geomdata[n->gridtype].height;
// FIXME can we not rid ourselves of this meddlesome double?
double interval = n->maxy < n->miny ? 0 : (n->maxy - n->miny) / ((double)dimy * states);
const int startx = n->labelaxisd ? PREFIXSTRLEN : 0; // plot cols begin here
// if we want fewer slots than there are available columns, our final column
// will be other than the plane's final column. most recent x goes here.
const int finalx = (n->slotcount < scaleddim - 1 - (startx * scale) ? startx + (n->slotcount / scale) - 1 : dimx - 1);
if(n->labelaxisd){
// show the *top* of each interval range
for(int y = 0 ; y < dimy ; ++y){
char buf[PREFIXSTRLEN + 1];
ncmetric(interval * states * (y + 1) * 100, 100, buf, 0, 1000, '\0');
ncplane_putstr_yx(ncplot_plane(n), dimy - y - 1, PREFIXSTRLEN - strlen(buf), buf);
}
}
if(finalx < startx){ // exit on pathologically narrow planes
return 0;
}
if(!interval){
interval = 1;
}
#define MAXWIDTH 2
int idx = n->slotstart; // idx holds the real slot index; we move backwards
for(int x = finalx ; x >= startx ; --x){
uint64_t gvals[MAXWIDTH];
// load it retaining the same ordering we have in the actual array
for(int i = scale - 1 ; i >= 0 ; --i){
gvals[i] = n->slots[idx]; // clip the value at the limits of the graph
if(gvals[i] < n->miny){
gvals[i] = n->miny;
}
if(gvals[i] > n->maxy){
gvals[i] = n->maxy;
}
// FIXME if there are an odd number, only go up through the valid ones...
if(--idx < 0){
idx = n->slotcount - 1;
}
}
// starting from the least-significant row, progress in the more significant
// direction, drawing egcs from the grid specification, aborting early if
// we can't draw anything in a given cell.
double intervalbase = n->miny;
const wchar_t* egc = geomdata[n->gridtype].egcs;
for(int y = 0 ; y < dimy ; ++y){
size_t egcidx, sumidx = 0;
// if we've got at least one interval's worth on the number of positions
// times the number of intervals per position plus the starting offset,
// we're going to print *something*
bool done = !geomdata[n->gridtype].fill;
for(int i = 0 ; i < scale ; ++i){
sumidx *= states;
if(intervalbase < gvals[i]){
egcidx = (gvals[i] - intervalbase) / interval;
if(egcidx >= states){
egcidx = states - 1;
}
done = false;
sumidx += egcidx;
}else{
egcidx = 0;
}
}
if(done){
break;
}
if(ncplane_putwc_yx(ncplot_plane(n), dimy - y - 1, x, egc[sumidx]) <= 0){
return -1;
}
intervalbase += (states * interval);
}
}
if(ncplane_cursor_move_yx(ncplot_plane(n), 0, 0)){
return -1;
}
if(ncplane_stain(ncplot_plane(n), dimy - 1, dimx - 1, n->maxchannel,
n->maxchannel, n->minchannel, n->minchannel) <= 0){
return -1;
}
return 0;
}
ncplot* ncplot_create(ncplane* n, const ncplot_options* opts){
// if miny == maxy, they both must be equal to 0
if(opts->miny == opts->maxy && opts->miny){
return NULL;
}
if(opts->rangex < 0){
return NULL;
}
if(opts->maxy < opts->miny){
return NULL;
}
if(opts->gridtype < 0 || opts->gridtype >= sizeof(geomdata) / sizeof(*geomdata)){
return NULL;
}
int sdimy, sdimx;
ncplane_dim_yx(n, &sdimy, &sdimx);
if(sdimx <= 0){
return NULL;
}
int dimx = sdimx;
ncplot* ret = malloc(sizeof(*ret));
if(ret){
ret->rangex = opts->rangex;
// if we're sizing the plot based off the plane dimensions, scale it by the
// plot geometry's width for all calculations
const int scaleddim = dimx * geomdata[opts->gridtype].width;
const int scaledprefixlen = PREFIXSTRLEN * geomdata[opts->gridtype].width;
if((ret->slotcount = ret->rangex) == 0){
ret->slotcount = scaleddim;
}
if(dimx < ret->rangex){
ret->slotcount = scaleddim;
}
if( (ret->labelaxisd = opts->labelaxisd) ){
if(ret->slotcount + scaledprefixlen > scaleddim){
if(scaleddim > scaledprefixlen){
ret->slotcount = scaleddim - scaledprefixlen;
}
}
}
size_t slotsize = sizeof(*ret->slots) * ret->slotcount;
ret->slots = malloc(slotsize);
if(ret->slots){
memset(ret->slots, 0, slotsize);
ret->ncp = n;
ret->maxchannel = opts->maxchannel;
ret->minchannel = opts->minchannel;
ret->miny = opts->miny;
ret->maxy = opts->maxy;
ret->vertical_indep = opts->vertical_indep;
ret->gridtype = opts->gridtype;
ret->exponentialy = opts->exponentialy;
if( (ret->detectdomain = (opts->miny == opts->maxy)) ){
ret->maxy = 0;
ret->miny = ~(uint64_t)0ull;
}
ret->slotstart = 0;
ret->slotx = 0;
redraw_plot(ret);
return ret;
}
free(ret);
}
return NULL;
}
ncplane* ncplot_plane(ncplot* n){
return n->ncp;
}
// if we're doing domain detection, update the domain to reflect the value we
// just set. if we're not, check the result against the known ranges, and
// return -1 if the value is outside of that range.
static inline int
update_domain(ncplot* n, uint64_t x){
const uint64_t val = n->slots[x % n->slotcount];
if(n->detectdomain){
if(val > n->maxy){
n->maxy = val;
}
if(val < n->miny){
n->miny = val;
}
return 0;
}
if(val > n->maxy || val < n->miny){
return -1;
}
return 0;
}
// if x is less than the window, return -1, as the sample will be thrown away.
// if the x is within the current window, find the proper slot and update it.
// otherwise, the x is the newest sample. if it is obsoletes all existing slots,
// reset them, and write the new sample anywhere. otherwise, write it to the
// proper slot based on the current newest slot.
static inline int
window_slide(ncplot* n, int64_t x){
if(x < n->slotx - (n->slotcount - 1)){ // x is behind window, won't be counted
return -1;
}else if(x <= n->slotx){ // x is within window, do nothing
return 0;
} // x is newest; we might be keeping some, might not
int64_t xdiff = x - n->slotx; // the raw amount we're advancing
n->slotx = x;
if(xdiff >= n->slotcount){ // we're throwing away all old samples, write to 0
memset(n->slots, 0, sizeof(*n->slots) * n->slotcount);
n->slotstart = 0;
return 0;
}
// we're throwing away only xdiff slots, which is less than n->slotcount.
// first, we'll try to clear to the right...number to reset on the right of
// the circular buffer. min of (available at current or to right, xdiff)
int slotsreset = n->slotcount - n->slotstart - 1;
if(slotsreset > xdiff){
slotsreset = xdiff;
}
if(slotsreset){
memset(n->slots + n->slotstart + 1, 0, slotsreset * sizeof(*n->slots));
}
n->slotstart = (n->slotstart + xdiff) % n->slotcount;
xdiff -= slotsreset;
if(xdiff){ // throw away some at the beginning
memset(n->slots, 0, xdiff * sizeof(*n->slots));
}
return 0;
}
// x must be within n's window at this point
static inline void
update_sample(ncplot* n, int64_t x, uint64_t y, bool reset){
const int64_t diff = n->slotx - x; // amount behind
const int idx = (n->slotstart + n->slotcount - diff) % n->slotcount;
if(reset){
n->slots[idx] = y;
}else{
n->slots[idx] += y;
}
}
// Add to or set the value corresponding to this x. If x is beyond the current
// x window, the x window is advanced to include x, and values passing beyond
// the window are lost. The first call will place the initial window. The plot
// will be redrawn, but notcurses_render() is not called.
int ncplot_add_sample(ncplot* n, uint64_t x, uint64_t y){
if(window_slide(n, x)){
return -1;
}
update_sample(n, x, y, false);
if(update_domain(n, x)){
return -1;
}
return redraw_plot(n);
}
int ncplot_set_sample(ncplot* n, uint64_t x, uint64_t y){
if(window_slide(n, x)){
return -1;
}
update_sample(n, x, y, true);
if(update_domain(n, x)){
return -1;
}
return redraw_plot(n);
}
void ncplot_destroy(ncplot* n){
if(n){
free(n->slots);
free(n);
}
}

73
src/lib/plot.cpp Normal file
View File

@ -0,0 +1,73 @@
#include "cpp.h"
typedef struct ncuplot {
ncppplot<uint64_t> n;
} ncuplot;
typedef struct ncdplot {
ncppplot<double> n;
} ncdplot;
extern "C" {
ncuplot* ncuplot_create(ncplane* n, const ncplot_options* opts, uint64_t miny, uint64_t maxy){
auto ret = new ncuplot;
if(ret){
if(ncppplot<uint64_t>::create(&ret->n, n, opts, miny, maxy)){
return ret;
}
free(ret);
}
return nullptr;
}
ncplane* ncuplot_plane(ncuplot* n){
return n->n.ncp;
}
int ncuplot_add_sample(ncuplot* n, uint64_t x, uint64_t y){
return n->n.add_sample(x, y);
}
int ncuplot_set_sample(ncuplot* n, uint64_t x, uint64_t y){
return n->n.set_sample(x, y);
}
void ncuplot_destroy(ncuplot* n){
if(n){
n->n.destroy();
delete n;
}
}
ncdplot* ncdplot_create(ncplane* n, const ncplot_options* opts, double miny, double maxy){
auto ret = new ncdplot;
if(ret){
if(ncppplot<double>::create(&ret->n, n, opts, miny, maxy)){
return ret;
}
free(ret);
}
return nullptr;
}
ncplane* ncdplot_plane(ncdplot* n){
return n->n.ncp;
}
int ncdplot_add_sample(ncdplot* n, uint64_t x, double y){
return n->n.add_sample(x, y);
}
int ncdplot_set_sample(ncdplot* n, uint64_t x, double y){
return n->n.set_sample(x, y);
}
void ncdplot_destroy(ncdplot* n){
if(n){
n->n.destroy();
delete n;
}
}
}

View File

@ -1,5 +1,5 @@
#include "main.h"
#include "internal.h"
#include "cpp.h"
#include <cstring>
#include <iostream>
@ -10,139 +10,134 @@ TEST_CASE("Plot") {
notcurses_options nopts{};
nopts.inhibit_alternate_screen = true;
nopts.suppress_banner = true;
FILE* outfp_ = fopen("/dev/tty", "wb");
auto outfp_ = fopen("/dev/tty", "wb");
REQUIRE(outfp_);
struct notcurses* nc_ = notcurses_init(&nopts, outfp_);
auto nc_ = notcurses_init(&nopts, outfp_);
REQUIRE(nc_);
struct ncplane* n_ = notcurses_stdplane(nc_);
auto n_ = notcurses_stdplane(nc_);
REQUIRE(n_);
REQUIRE(0 == ncplane_cursor_move_yx(n_, 0, 0));
// setting miny == maxy with non-zero domain limits is invalid
SUBCASE("DetectRangeBadY"){
ncplot_options popts{};
popts.maxy = popts.miny = -1;
ncplot* p = ncplot_create(n_, &popts);
auto p = ncuplot_create(n_, &popts, -1, -1);
CHECK(nullptr == p);
popts.miny = 1;
popts.maxy = 1;
p = ncplot_create(n_, &popts);
p = ncuplot_create(n_, &popts, 1, 1);
CHECK(nullptr == p);
popts.miny = 0;
popts.maxy = 0;
p = ncplot_create(n_, &popts);
p = ncuplot_create(n_, &popts, 0, 0);
REQUIRE(nullptr != p);
ncplot_destroy(p);
ncuplot_destroy(p);
}
// maxy < miny is invalid
SUBCASE("RejectMaxyLessMiny"){
ncplot_options popts{};
popts.miny = 2;
popts.maxy = 1;
ncplot* p = ncplot_create(n_, &popts);
auto p = ncuplot_create(n_, &popts, 2, 1);
CHECK(nullptr == p);
}
SUBCASE("SimplePlot"){
ncplot_options popts{};
ncplot* p = ncplot_create(n_, &popts);
auto p = ncuplot_create(n_, &popts, 0, 0);
REQUIRE(p);
CHECK(n_ == ncplot_plane(p));
ncplot_destroy(p);
CHECK(n_ == ncuplot_plane(p));
ncuplot_destroy(p);
}
// 5-ary slot space without any window movement
SUBCASE("AugmentSamples5"){
ncplot_options popts{};
popts.rangex = 5;
popts.maxy = 10;
popts.miny = 0;
ncplot* p = ncplot_create(n_, &popts);
REQUIRE(p);
CHECK(0 == p->slots[0]);
CHECK(0 == ncplot_add_sample(p, 0, 1));
CHECK(1 == p->slots[0]);
CHECK(0 == ncplot_add_sample(p, 0, 1));
CHECK(2 == p->slots[0]);
CHECK(0 == p->slots[1]);
CHECK(0 == p->slots[2]);
CHECK(0 == ncplot_add_sample(p, 2, 3));
CHECK(3 == p->slots[2]);
CHECK(0 == ncplot_set_sample(p, 2, 3));
CHECK(3 == p->slots[2]);
CHECK(0 == p->slots[3]);
CHECK(0 == p->slots[4]);
CHECK(0 == ncplot_add_sample(p, 4, 6));
CHECK(6 == p->slots[4]);
CHECK(2 == p->slots[0]);
CHECK(4 == p->slotx);
ncplot_destroy(p);
ncppplot<uint64_t> p;
ncppplot<uint64_t>::create(&p, n_, &popts, 0, 10);
CHECK(0 == p.slots[0]);
CHECK(0 == p.add_sample((uint64_t)0, (uint64_t)1));
CHECK(1 == p.slots[0]);
CHECK(0 == p.add_sample((uint64_t)0, (uint64_t)1));
CHECK(2 == p.slots[0]);
CHECK(0 == p.slots[1]);
CHECK(0 == p.slots[2]);
CHECK(0 == p.add_sample((uint64_t)2, (uint64_t)3));
CHECK(3 == p.slots[2]);
CHECK(0 == p.set_sample((uint64_t)2, (uint64_t)3));
CHECK(3 == p.slots[2]);
CHECK(0 == p.slots[3]);
CHECK(0 == p.slots[4]);
CHECK(0 == p.add_sample((uint64_t)4, (uint64_t)6));
CHECK(6 == p.slots[4]);
CHECK(2 == p.slots[0]);
CHECK(4 == p.slotx);
p.destroy();
}
// 2-ary slot space with window movement
SUBCASE("AugmentCycle2"){
ncplot_options popts{};
popts.rangex = 2;
popts.maxy = 10;
popts.miny = 0;
ncplot* p = ncplot_create(n_, &popts);
REQUIRE(p);
CHECK(0 == p->slots[0]);
CHECK(0 == ncplot_add_sample(p, 0, 1));
CHECK(1 == p->slots[0]);
CHECK(0 == ncplot_add_sample(p, 0, 1));
CHECK(2 == p->slots[0]);
CHECK(0 == ncplot_set_sample(p, 1, 5));
CHECK(5 == p->slots[1]);
CHECK(0 == ncplot_set_sample(p, 2, 9));
CHECK(5 == p->slots[1]);
CHECK(9 == p->slots[0]);
CHECK(0 == ncplot_add_sample(p, 3, 4));
CHECK(9 == p->slots[0]);
CHECK(4 == p->slots[1]);
CHECK(3 == p->slotx);
CHECK(0 == ncplot_add_sample(p, 5, 1));
CHECK(1 == p->slots[0]);
CHECK(0 == p->slots[1]);
CHECK(5 == p->slotx);
ncplot_destroy(p);
ncppplot<uint64_t> p;
ncppplot<uint64_t>::create(&p, n_, &popts, 0, 10);
CHECK(0 == p.slots[0]);
CHECK(0 == p.add_sample((uint64_t)0, (uint64_t)1));
CHECK(1 == p.slots[0]);
CHECK(0 == p.add_sample((uint64_t)0, (uint64_t)1));
CHECK(2 == p.slots[0]);
CHECK(0 == p.set_sample((uint64_t)1, (uint64_t)5));
CHECK(5 == p.slots[1]);
CHECK(0 == p.set_sample((uint64_t)2, (uint64_t)9));
CHECK(5 == p.slots[1]);
CHECK(9 == p.slots[0]);
CHECK(0 == p.add_sample((uint64_t)3, (uint64_t)4));
CHECK(9 == p.slots[0]);
CHECK(4 == p.slots[1]);
CHECK(3 == p.slotx);
CHECK(0 == p.add_sample((uint64_t)5, (uint64_t)1));
CHECK(1 == p.slots[0]);
CHECK(0 == p.slots[1]);
CHECK(5 == p.slotx);
p.destroy();
}
// augment past the window, ensuring everything gets zeroed
SUBCASE("AugmentLong"){
ncplot_options popts{};
popts.rangex = 5;
popts.maxy = 10;
popts.miny = 0;
ncplot* p = ncplot_create(n_, &popts);
REQUIRE(p);
ncppplot<uint64_t> p;
ncppplot<uint64_t>::create(&p, n_, &popts, 0, 10);
for(int x = 0 ; x < 5 ; ++x){
CHECK(0 == p->slots[x]);
CHECK(0 == p.slots[x]);
}
CHECK(0 == ncplot_add_sample(p, 4, 4));
CHECK(0 == p.add_sample((uint64_t)4, (uint64_t)4));
for(int x = 0 ; x < 4 ; ++x){
CHECK(0 == p->slots[x]);
CHECK(0 == p.slots[x]);
}
CHECK(4 == p->slots[4]);
CHECK(0 == ncplot_add_sample(p, 10, 5));
CHECK(5 == p->slots[0]);
CHECK(4 == p.slots[4]);
CHECK(0 == p.add_sample((uint64_t)10, (uint64_t)5));
CHECK(5 == p.slots[0]);
for(int x = 1 ; x < 4 ; ++x){
CHECK(0 == p->slots[x]);
CHECK(0 == p.slots[x]);
}
CHECK(0 == ncplot_add_sample(p, 24, 7));
CHECK(7 == p->slots[0]);
CHECK(0 == p.add_sample((uint64_t)24, (uint64_t)7));
CHECK(7 == p.slots[0]);
for(int x = 1 ; x < 5 ; ++x){
CHECK(0 == p->slots[x]);
CHECK(0 == p.slots[x]);
}
CHECK(0 == ncplot_add_sample(p, 100, 0));
CHECK(0 == p.add_sample((uint64_t)100, (uint64_t)0));
for(int x = 0 ; x < 5 ; ++x){
CHECK(0 == p->slots[x]);
CHECK(0 == p.slots[x]);
}
ncplot_destroy(p);
p.destroy();
}
// FIXME need some rendering tests, one for each geometry
// FIXME need some high-level rendering tests, one for each geometry
SUBCASE("SimpleFloatPlot"){
ncplot_options popts{};
auto p = ncdplot_create(n_, &popts, 0, 0);
REQUIRE(p);
CHECK(n_ == ncdplot_plane(p));
ncdplot_destroy(p);
}
CHECK(0 == notcurses_stop(nc_));
CHECK(0 == fclose(outfp_));