Skip to content

Intermediate representation for layered draw #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 21 commits into
base: master
Choose a base branch
from

Conversation

M2-TE
Copy link
Contributor

@M2-TE M2-TE commented Dec 2, 2024

I used the Pixel_B structure from #8 to create a fragment struct utilizing unions instead of std::variant, due to the latter being larger in total size (3 vs 8 bytes). Alongside the character data bit field, it's only 4 bytes in size.
The intermediate representation is really just a std::vector<fragment>, which makes it very easy to work with.

I've chosen fragment as a name, as every fragment can store multiple pixels, i.e. data points. This allows us to store the underlying fragment data using a 8-bit bit field to store either 255 different values per fragment or 8 different data point states (useful for the 2x4 braille), which completely decouples it from the character set. In this example, the bit field is used to store values from 0-8 as the bar character has that many states.

This is supposed to simply show the base idea of how I would imagine the intermediate representation to be built. Of course, I would still change the formatting to fit yours once you are happy with the state of the implementation.

It currently lacks color merging (and use of 24-bit color, for that matter), which is what I plan on doing next. After that comes the generalization of characters during draw, in order to work purely on "data points" rather than characters.

@tdulcet tdulcet added the enhancement New feature or request label Dec 3, 2024
@tdulcet tdulcet linked an issue Dec 3, 2024 that may be closed by this pull request
@tdulcet
Copy link
Owner

tdulcet commented Dec 3, 2024

Thanks for the PR! It looks like you are making good progress. Your comments are very helpful to understand the changes. I know the existing code could probably use more comments and better variable naming. Anyway, once you get the intermediate representation fully working, I am assuming you are planning to integrate it into the existing functions. Just let me know when you are ready for a review or if there is anything I could help with.

@M2-TE
Copy link
Contributor Author

M2-TE commented Dec 6, 2024

I think I've found a good signature to use and wanted to ask your take on it, as it's similar but not exactly the same as what you had:

struct Options {
        size_t width = 0; // Width in terminal characters. Set to 0 for automatic size based on terminal.
        size_t height = 0; // Height in terminal characters. Set to 0 for automatic size based on terminal.
        
        struct Axis {
	        double min = 0; // Start of axis. Set to 0 for automatic size based on data.
	        double max = 0; // End of axis. Set to 0 for automatic size based on data.
	        bool drawn = true;
	        bool label = true; // TODO: allow setting custom label string?
	        bool ticks = true;
	        bool tick_labels = true;
	        units_type tick_label_type = units_fracts;
        };
        Axis x = {};
        Axis y = {};
        
        type_type character_set = type_braille;
        style_type style = style_light;
        ColorBits color_type = ColorBits::e4; // bit depth of color representation
        bool validate = true; // validate sizes for graph draw
        bool border = false; // draw border around the graph
        /* 5 bytes padding */
        
        // graph-specific options
        struct Histogram {
	        size_t bar_width = 1; // size of each bar in x-axis (in data points, e.g. braille has width of 2 per terminal character)
        } histogram;
        
        std::ostream &ostr = std::cout;
        const char* title = nullptr;
};
struct Fragment {
        Color color;
        uint8_t data; // stores up to 8 data points or up to 255 values
};
using Texture = std::unique_ptr<std::vector<Fragment>>;
auto histogram_experimental(const std::vector<T>& data, const Options& options = {}, const Color& color = {color_red}) -> Texture&&;
auto histogram_experimental(const std::vector<T>& data, Texture&& texture, const Options& options = {}, const Color& color = {color_red}) -> Texture&&;

auto histogram_experimental(const std::vector<std::vector<T>>& datasets, const Options& options = {}, const std::vector<Color>& colors = {}) -> Texture&&;
auto histogram_experimental(const std::vector<std::vector<T>>& datasets, Texture&& texture, const Options& options = {}, const std::vector<Color>& colors = {}) -> Texture&&;
  1. Currently, the data is provided as a std::vector instead of pointer, but I quite honestly think that pointers like before are the better solution. C++ 20 has std::span, which would be absolutely perfect, but i reckon you want to stay within C++ 17.

  2. I provide an optional parameter via Texture, which is the intermediate representation. I feel like this is better than a static context without any additional memory management pain.

  3. I've changed the options a little. There is now an Options struct, which contains the previously explicit parameters such as size and axis min/max. This allows configuring each axis individually without creating gigantic function signatures. It also holds graph-specific data, which could be turned into a union. To restore explicit width/height and min/max parameters, another function signature could simply be added.

  4. I pulled the Color and std::vector<color> options into a separate parameter, as it's specific to whether single or multiple graphs are drawn, while also likely being the only option that changes between histogram calls when users want to graph multiple data sets into the same graph.

@tdulcet
Copy link
Owner

tdulcet commented Dec 6, 2024

1. Currently, the data is provided as a std::vector instead of pointer, but I quite honestly think that pointers like before are the better solution. C++ 20 has std::span, which would be absolutely perfect, but i reckon you want to stay within C++ 17.

Yes, I would love to use std::span here, but we would need to wait a couple more years... Actually, I think we might need std::mdspan as well for the multidimensional graphs, which would require C++23. We cannot hardcode std::vector as the datatype, as we need to support std::array and C style arrays as well. See the test suite in the graphs.cpp file.

3. I've changed the options a little. There is now an Options struct, which contains the previously explicit parameters such as size and axis min/max. This allows configuring each axis individually without creating gigantic function signatures.

Nice, allowing users to configure the options per axis should be a big improvement. Originally, I thought it would be good to put the options that users would most likely want to change in the function signature, but this will likely make the libraries easier to use. I have wanted to do something similar with the tables library, to allow users to set options per table column or row. Anyway, note that the bar_width should be constant based on the character_set, so I am not sure that it would need to be stored in the Options struct.

4. I pulled the Color and std::vector<color> options into a separate parameter, as it's specific to whether single or multiple graphs are drawn, while also likely being the only option that changes between histogram calls when users want to graph multiple data sets into the same graph.

Should this Color vector be added to the Options struct instead? Regardless, this will be very useful for the graphs and plots, but note that the histograms (currently) only support graphing a single dataset. We would need the color mixing in order to support multiple datasets and I am still not sure that it that would be readable due to the likely significant overlap for most sets of histograms.

Here is a slightly simplified version of your proposal, which includes all of the existing function signatures that would need to be updated:

union Color {
	color_type color4; // 4-bit color
	uint8_t color8; // 8-bit color
	struct {
		uint8_t red;
		uint8_t green;
		uint8_t blue;
	} color24; // 24-bit true color
};

struct Axis {
	long double min = 0;
	long double max = 0;
	bool labels = true;
	bool ticks = true;
	bool units_label = true;
	units_type units = units_fracts;
};

struct Options {
	size_t width = 0;
	size_t height = 0;
	
	Axis x = {};
	Axis y = {};
	
	bool border = false;
	bool axis = true;
	type_type type = type_braille;
	// plot_type plot = plot_scatter;
	mark_type mark = mark_dot;
	// graph_type graph = graph_dot;
	const char *title = nullptr; // Maybe: string &title;
	style_type style = style_light;
	Color color = color_red;
	ostream &ostr = cout;
	bool check = true;
	
	// Internal // Is this needed?
	// size_t bar_width = 1;
};

struct Fragment {
        Color color;
        uint8_t data;
};

inline int graph(const vector<vector<Fragment>> &texture, const Options &options);

template <typename T>
int histogram(const T &array, const Options &options = {});

template <typename T>
int histogram(const size_t rows, T *array, const Options &options = {});

template <typename T>
int plots(const T &arrays, const Options &options = {}, const vector<Color> &colors = {});

template <typename T>
int plot(const T &array, const Options &options = {});

template <typename T>
int plot(const size_t rows, T **array, const Options &options = {});

template <typename T>
int functions(const size_t numfunctions, function<T(T)> functions[], const Options &options = {}, const vector<Color> &colors = {});

template <typename T>
int function(const function<T(T)> &afunction, const Options &options = {});

template <typename T>
int function(T afunction(T), const Options &options = {});

I tried to retain my existing names as much as possible, to make it easier for users to upgrade.

@M2-TE
Copy link
Contributor Author

M2-TE commented Dec 9, 2024

The bar width was basically just a placeholder for a graph-specific option. Thought it might be nice to draw each histogram bar 2 characters wide in some cases.

Should this Color vector be added to the Options struct instead?

I think it's actually better to leave color in the parameter, as users may wanna reuse option structs from before and only change color + data.

You are right that mixing histograms does not make too much sense. It is pretty much just a proof of concept for mixing graphs and I should have probably used plots instead.

As for the texture parameter, I would leave it in there to make it easier to layer the graph draws (both internally and for users not wanting to create a multi-dimensional array of input data). Regarding textures, is there a reason you prefer std::vector<std::vector<T>> over std::vector<T>? If it is the indexing convenience, it might be best to create a dedicated texture class with 2D indexing functions.

I have added the option draw_immediately = true to control whether a graph is drawn immediately upon calling e.g. plots(...), or if the user wants to manually call graph(...) or draw(...). This would give the user full control over when stuff is drawn and would allow them to create e.g. an animation.

@tdulcet
Copy link
Owner

tdulcet commented Dec 10, 2024

Regarding textures, is there a reason you prefer std::vector<std::vector<T>> over std::vector<T>?

Yeah, I used a 2D vector mainly for indexing convenience. Is there a reason you prefer the 1D version? Feel free to use the 1D version if it does not add too much complexity.

I have added the option draw_immediately = true to control whether a graph is drawn immediately upon calling e.g. plots(...), or if the user wants to manually call graph(...) or draw(...). This would give the user full control over when stuff is drawn and would allow them to create e.g. an animation.

Nice, that will be a great improvement. The colorful example on the README (the visual result of k-means clustering) was created before this library supported multidimensional arrays, so I actually had to implement a custom version of plots() and then manually call graph(). Your proposed changes would have been very helpful for that, as one could then use the library entirely.

@tdulcet
Copy link
Owner

tdulcet commented Dec 17, 2024

I pushed a commit to the main branch to add support for some new block types. I do not believe this will conflict with any of your changes, but it does slightly change how it outputs the graph characters. I also added a new function to generate the color strings, which might be helpful. See ba19fd9 for the full changes.

@M2-TE
Copy link
Contributor Author

M2-TE commented Jan 13, 2025

They won't cause any conflict, since I've not yet implemented proper drawing. Gonna use those once I do!

I've put some more thought into the intermediate representation and decided to put both Options and Texture into a single Intermediate struct, which will also encapsulate the eventual draw. The reason behind this is that it makes the function signatures much easier to understand and differentiate, while also passing options, which may allow (e.g. on width/height = 0) for automatic resizing of the drawing texture for following data sets, not just the first one.

I've removed the histogram portion for now, as it makes much more sense to prototype with simple plots instead. One thing I'm not quite sure about is how the Braille characters should be indexed..

@tdulcet
Copy link
Owner

tdulcet commented Jan 14, 2025

One thing I'm not quite sure about is how the Braille characters should be indexed..

The Braille characters are currently listed in codepoint order in the library. See the Wikipedia page and the existing library code:

const char *const dots[] = {"⠀", "⠁", "⠂", "⠃", "⠄", "⠅", "⠆", "⠇", "⠈", "⠉", "⠊", "⠋", "⠌", "⠍", "⠎", "⠏", "⠐", "⠑", "⠒", "⠓", "⠔", "⠕", "⠖", "⠗", "⠘", "⠙", "⠚", "⠛", "⠜", "⠝", "⠞", "⠟", "⠠", "⠡", "⠢", "⠣", "⠤", "⠥", "⠦", "⠧", "⠨", "⠩", "⠪", "⠫", "⠬", "⠭", "⠮", "⠯", "⠰", "⠱", "⠲", "⠳", "⠴", "⠵", "⠶", "⠷", "⠸", "⠹", "⠺", "⠻", "⠼", "⠽", "⠾", "⠿", "⡀", "⡁", "⡂", "⡃", "⡄", "⡅", "⡆", "⡇", "⡈", "⡉", "⡊", "⡋", "⡌", "⡍", "⡎", "⡏", "⡐", "⡑", "⡒", "⡓", "⡔", "⡕", "⡖", "⡗", "⡘", "⡙", "⡚", "⡛", "⡜", "⡝", "⡞", "⡟", "⡠", "⡡", "⡢", "⡣", "⡤", "⡥", "⡦", "⡧", "⡨", "⡩", "⡪", "⡫", "⡬", "⡭", "⡮", "⡯", "⡰", "⡱", "⡲", "⡳", "⡴", "⡵", "⡶", "⡷", "⡸", "⡹", "⡺", "⡻", "⡼", "⡽", "⡾", "⡿", "⢀", "⢁", "⢂", "⢃", "⢄", "⢅", "⢆", "⢇", "⢈", "⢉", "⢊", "⢋", "⢌", "⢍", "⢎", "⢏", "⢐", "⢑", "⢒", "⢓", "⢔", "⢕", "⢖", "⢗", "⢘", "⢙", "⢚", "⢛", "⢜", "⢝", "⢞", "⢟", "⢠", "⢡", "⢢", "⢣", "⢤", "⢥", "⢦", "⢧", "⢨", "⢩", "⢪", "⢫", "⢬", "⢭", "⢮", "⢯", "⢰", "⢱", "⢲", "⢳", "⢴", "⢵", "⢶", "⢷", "⢸", "⢹", "⢺", "⢻", "⢼", "⢽", "⢾", "⢿", "⣀", "⣁", "⣂", "⣃", "⣄", "⣅", "⣆", "⣇", "⣈", "⣉", "⣊", "⣋", "⣌", "⣍", "⣎", "⣏", "⣐", "⣑", "⣒", "⣓", "⣔", "⣕", "⣖", "⣗", "⣘", "⣙", "⣚", "⣛", "⣜", "⣝", "⣞", "⣟", "⣠", "⣡", "⣢", "⣣", "⣤", "⣥", "⣦", "⣧", "⣨", "⣩", "⣪", "⣫", "⣬", "⣭", "⣮", "⣯", "⣰", "⣱", "⣲", "⣳", "⣴", "⣵", "⣶", "⣷", "⣸", "⣹", "⣺", "⣻", "⣼", "⣽", "⣾", "⣿"};
const int dotvalues[][4] = {{0x1, 0x2, 0x4, 0x40}, {0x8, 0x10, 0x20, 0x80}};
case type_braille:
dot += dotvalues[k][l];
break;
for how to convert a value to the correct Braille character.

@M2-TE
Copy link
Contributor Author

M2-TE commented Jan 15, 2025

The hex value of each dot really threw me off.. I had falsely assumed it was linear, i.e. 0x1 0x2 0x4 0x8 for the first column..

Anyways, the two functions to showcase how to perform layered draws are plot_experimental(...). I've currently enabled both braille and quadrant_block characters, which both work in layered rendering and can be drawn manually using draw() on the Intermediate object. Otherwise it gets drawn automatically.

Of course, this only contains the code for drawing the plot data, not the axis or other fluff. I reckon that should mostly be copy-able from the current implementation, as they get draw at the very end.

Please do check out the current implementation!
I would propose to do color merging and adding more plot types in separate pull requests, for the sake of organization and because my current implementation does not break anything (could put my stuff into an experimental namespace?). If you prefer all the work to be completed in this PR, that would be fine too of course!

@tdulcet
Copy link
Owner

tdulcet commented Jan 15, 2025

Please do check out the current implementation!

Thanks for all your work on this. Overall, it looks great! I can add some inline comments, if you are ready for that.

I've currently enabled both braille and quadrant_block characters

The other block characters should be able to reuse the same code, although you would need a terminal that supports Unicode 16 in order to test some of them.

I would propose to do color merging and adding more plot types in separate pull requests, for the sake of organization and because my current implementation does not break anything

Feel free to break it up into multiple PRs, if that is easier for you. In that case, we should just keep your experimental labels on the function names. Before removing those, I will need to update my graphs CLI tool to be compatible with your changes. We will of course need to update the library documentation as well.

@M2-TE
Copy link
Contributor Author

M2-TE commented Jan 15, 2025

I can add some inline comments, if you are ready for that.

Sounds good!

As a side note, do you know why exactly the test on ubuntu 20.04 with clang++ fails? It seems to be a compiler-internal error with the graphs::plots(...) function, but I don't think I changed anything about it or anything related to it

@tdulcet
Copy link
Owner

tdulcet commented Jan 16, 2025

As a side note, do you know why exactly the test on ubuntu 20.04 with clang++ fails?

It started failing on the main branch as well after ba19fd9, so I had to update the CI to ignore it. I would not worry about this too much, as GitHub is removing support for Ubuntu 20.04 in a few months anyway. It looks like this older version of Clang is just busted, as I can reproduce the issue on my system with a Ubuntu 20.04 VM.

Comment on lines +848 to +867
struct Options {
size_t width = 0; // Width in terminal characters. Set to 0 for automatic size based on terminal.
size_t height = 0; // Height in terminal characters. Set to 0 for automatic size based on terminal.

Axis x = {};
Axis y = {};

type_type character_set = type_braille;
plot_type plot = plot_scatter;
style_type style = style_light;
graph_type graph = graph_dot;

std::string title;
std::ostream &ostr = std::cout;

ColorBits color_type = ColorBits::e4; // bit depth of color representation
bool check = true; // validate sizes for graph draw
bool border = false; // draw border around the graph
bool draw_immediately = true; // draw graph immediately after creation. otherwise call draw/graph with the returned texture
};
Copy link
Owner

@tdulcet tdulcet Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new Option struct is missing some of the existing options. See #9 (comment) above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was only missing bool axis when compared to the Options there right? I have added bool drawn = true to the Axis struct for that one

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are missing the mark option as well. The plot and graph options are currently unused.

Interesting about allowing the axes to be enabled independently. That will require some changes to the graph function.

const size_t index = x + y * options.width;
const auto& frag = texture[index];
// begin color draw(
cout << outputcolor(frag.color.col_4);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably update the outputcolor function to accept the entire Color struct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would need to pass the color bits as well, so the signature could look something like this?

inline string outputcolor(Color color, ColorBits color_bits)

Makes sense to copy, rather than passing references

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me! I usually keep the const quantifier unless of course I am planning to modify the argument values.

// intermediate representation of a graph texture for ease of passing around
struct Intermediate {
// use a graph texture to draw into the terminal
inline void draw() {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am guessing you would want to eventually replace this with the existing graph function, of course updating it to accept an Intermediate struct instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup that's the idea. I didn't wanna taint any functions outside my experimental area yet, so I put it into the Intermediate itself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Proposing changes to drawing backend
2 participants