Skip to content
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

Implement Lazy Table Visualisation UI integration. #5806

Conversation

MichaelMauderer
Copy link
Contributor

@MichaelMauderer MichaelMauderer commented Mar 3, 2023

Pull Request Description

Adds a new table visualization that is lazily retrieving data from the engine. Closes #5002.

Peek.2023-03-03.14-08.mp4

Important Notes

  • Leaves the old table visualization in place, but renames it to "Table (legacy)".
  • Visual design will need to be matched to a reference design.

Checklist

Please include the following checklist in your PR:

  • The documentation has been updated if necessary.
  • All code conforms to the
    Scala,
    Java,
    and
    Rust
    style guides.
  • All code has been tested:
    • Unit tests have been written where possible.
    • If GUI codebase was changed: Enso GUI was tested when built using BOTH
      ./run ide build and ./run ide watch.

@MichaelMauderer MichaelMauderer self-assigned this Mar 3, 2023
@MichaelMauderer MichaelMauderer force-pushed the wip/MichaelMauderer/Lazy_Table_Visualisation_UI_Integration_5002 branch from 3588df0 to b4489af Compare March 3, 2023 15:57
@MichaelMauderer MichaelMauderer marked this pull request as ready for review March 3, 2023 16:00
Comment on lines 72 to 74
const PADDING_TEXT: f32 = 5.0;

const TEXT_PADDING: f32 = 1.5;
Copy link
Contributor

Choose a reason for hiding this comment

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

Could use docs--the difference between these is not obvious.

/// Position of the cell in the table in columns and rows.
pub cell: GridPosition,
/// Position of the chunk in the cell in chunk index and lines.
pub chunk: GridPosition,
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be a ChunkCoordinate?

}
}

/// Position and size of a sub-grid within a grid.
Copy link
Contributor

Choose a reason for hiding this comment

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

Docs should distinguish this from GridWindow.

Copy link
Contributor

@kazcw kazcw left a comment

Choose a reason for hiding this comment

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

Approved, with some comments.

As I understand it, we are calculating what text we need based on Unicode character counts, although the displayed size of the text depends on the number of resulting glyphs. Please check & document behavior when the number of glyphs is very different from the number of characters, particularly when the input contains many Unicode combining characters.

Comment on lines 303 to 304
color::Lcha::new(0.95, 0.0, 0.0, 1.0),
_ => color::Lcha::new(1.0, 0.0, 0.0, 0.0),
Copy link
Contributor

Choose a reason for hiding this comment

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

These could be constants.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They should probably be taken form the theme. Changed it to that.

@@ -264,12 +386,14 @@ impl<T: TextProvider + 'static> TextGrid<T> {
theme_update <- all(init, font_name, font_size);
text_grid.set_entries_params <+ theme_update.map(
f!([dom_entry_root]((_, font_name, font_size)) {
let font_name = font_name.clone();
let font_size = *font_size;
let parent = Some(dom_entry_root.clone_ref());

dom_entry_root.set_style_or_warn("font-family", font_name.clone());
Copy link
Contributor

Choose a reason for hiding this comment

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

(Not new code, but) this can be passed by reference.

visualization::Definition::new(
visualization::Signature::new(
path,
"Standard.Table.Data.Table.Table",
Copy link
Contributor

Choose a reason for hiding this comment

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

Named constant.

@@ -18,10 +19,50 @@ use std::fmt::Write;
// === Model ===
// =============


Copy link
Contributor

Choose a reason for hiding this comment

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

Extra line.

pub content: Content,
pub bg_color: color::Rgba,
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Extra line.

+ chunk.x as usize;
let chunk_y =
self.rows.iter().take(row).fold(0, |acc, x| acc + x.unwrap_or(0)) + chunk.y as usize;
// error!("Converting {table_position:?} to {chunk_x} {chunk_y}");
Copy link
Contributor

Choose a reason for hiding this comment

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

Commented code.


/// Return the table item at the given position in the grid. The given grid coordinates address
/// the grid in chunks/lines.
pub fn grid_view_position_to_table_item(
Copy link
Contributor

Choose a reason for hiding this comment

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

This function is called for every displayed cell, and it performs O(rows + cols) string-scans and allocations. What about caching the results of the iter_row_items_per_line and iter_column_items_per_chunk operations?

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 don't usually have too many cells at once (probably a few dozen or a few hundreds at most). And keeping the cache correct could be a bit tricky as we can get updates to the table layout, so we need to carefully consider the cache invalidation there. Not a huge issue, but a bit of extra work for sure.

So, while this is a good comment, I think before implementing this, we should profile the actual impact of this.

Comment on lines 946 to 952
fn try_from(data: serde_json::Value) -> Result<Self, Self::Error> {
Ok(serde_json::from_value(data.clone()).unwrap_or_else(|_| {
let data_str = serde_json::to_string_pretty(&data);
let data_str = data_str.unwrap_or_else(|e| format!("<Cannot render data: {e}.>"));
data_str.into()
}))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Why return Ok if the input can't be deserialized?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The whole implementation is actually no longer needed. Removed it.

@@ -335,11 +991,61 @@ fn fill_emtpy_chunks(chunks: Vec<Chunk>) -> Vec<Chunk> {
.collect()
}

// =========================
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing newlines.

Err(visualization::DataError::BinaryNotSupported)
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing newlines after.

@MichaelMauderer
Copy link
Contributor Author

Approved, with some comments.

As I understand it, we are calculating what text we need based on Unicode character counts, although the displayed size of the text depends on the number of resulting glyphs. Please check & document behavior when the number of glyphs is very different from the number of characters, particularly when the input contains many Unicode combining characters.

That is a pretty good point that has not been accounted for in the current design for both lazy table and lazy text.

What we need to consider are graphemes. We can assume that we use as fixed width font, so all graphemes should be the same size. But on both the backend and the frontend we need to work with chunks of graphemes, to not split something up between chunks.
The first thing to look in is, how Enso supports graphemes and then splits strings based on them. And then also update the GUI part to use the unicode-segementation crate to work with graphemes on the GUI side.

I think this is mostly a “bookkeeping” issue for chunking and retrieving the data. Rendering them should then not be any different.

Copy link
Member

@jdunkerley jdunkerley left a comment

Choose a reason for hiding this comment

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

Just approving the slight Enso changes.

@radeusgd
Copy link
Member

radeusgd commented Mar 6, 2023

What we need to consider are graphemes. We can assume that we use as fixed width font, so all graphemes should be the same size. But on both the backend and the frontend we need to work with chunks of graphemes, to not split something up between chunks. The first thing to look in is, how Enso supports graphemes and then splits strings based on them.

On the library side, grapheme clusters are first-class citizens for Enso strings - i.e. by default Text.length, Text.at, Text.take (to take a substring) will all use grapheme-cluster-based indexing, i.e. the length of a single grapheme cluster that spans multiple codepoints but is a single grapheme will be 1 and indexing is also taking that into account. So on this side we should already be all good.

@radeusgd
Copy link
Member

radeusgd commented Mar 6, 2023

What we need to consider are graphemes. We can assume that we use as fixed width font, so all graphemes should be the same size. But on both the backend and the frontend we need to work with chunks of graphemes, to not split something up between chunks. The first thing to look in is, how Enso supports graphemes and then splits strings based on them.

On the library side, grapheme clusters are first-class citizens for Enso strings - i.e. by default Text.length, Text.at, Text.take (to take a substring) will all use grapheme-cluster-based indexing, i.e. the length of a single grapheme cluster that spans multiple codepoints but is a single grapheme will be 1 and indexing is also taking that into account. So on this side we should already be all good.

However, we need rendering to be consistent with grapheme cluster handling. An interesting and potentially problematic part are emojis with combining characters, for example on the web a facepalm with a color and gender modifier will be correctly rendered as a single grapheme:🤦🏼‍♂️.

At least on my machine it shows up as:

image

And Enso does treat it as having length == 1. But in my terminal it does show up as 3 graphemes actually:
image

Copy link
Contributor

@farmaazon farmaazon left a comment

Choose a reason for hiding this comment

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

Code looks good, however I miss important information in the docs here and there.

Comment on lines 51 to 52
use text_provider::BackendTextProvider;
use text_provider::TextProvider;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think some of those are imported with full crate::... path, and here are short paths. Or is text_provider also a crate?

Comment on lines 87 to 96

/// Position of a chunk in a table. Addressed by table cell first, then by chunk index/line within
/// the cell.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct TablePosition {
/// Position of the cell in the table in columns and rows.
pub cell: GridPosition,
/// Position of the chunk in the cell in chunk index and lines.
pub chunk: ChunkCoordinate,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

What is a table in this context? The module documentation mentions grid only. Is Table same as Grid?

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've updated the module docs. "Table" in this context refers to the data in the backend, which consists of strings organized in rows/columns.

Comment on lines 51 to 52
/// Column Width in lines
type RowHeight = usize;
Copy link
Contributor

Choose a reason for hiding this comment

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

Copied docs.

Comment on lines 73 to 75
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Copy)]
/// A table item and its position. Only used for working on the layout, not for individual cells
/// or accessing content.
Copy link
Contributor

Choose a reason for hiding this comment

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

We usually put docs before derives.

Comment on lines 205 to 207
/// Iterate over the item in the table. Only returns the actual content items. Does not return
/// the dividers or the headings.
fn iter_content_columns(&self) -> impl Iterator<Item = TableItem> + '_ {
Copy link
Contributor

Choose a reason for hiding this comment

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

The documentation does not explain the method's name.

Comment on lines 215 to 217
/// Iterate over the item in the table. Only returns the actual content items. Does not return
/// the dividers or the headings.
fn iter_content_rows(&self) -> impl Iterator<Item = TableItem> + '_ {
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto.

Comment on lines +243 to +245
/// Convert the given cell / chunk position from the backend data into a chunk position for the
/// text cache.
pub fn backend_table_position_to_grid_content_position(
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be a good idea to add information about time complexity - here and in next methods. We iterate over all columns on the left (if I understand the code correctly), and that might be substantial.

Copy link
Contributor

Choose a reason for hiding this comment

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

Or are those displayed columns only? If yes, then I miss that important information here or in the structure's docs.

Comment on lines 53 to 62
type RowIndex = usize;

/// Specification of a table layout. Contains the column and row widths, as well as the column and
/// row names. Provides methods for updating the specification and for accessing layout properties.
/// We allow for a table specification to be partially specified, i.e., some of the information may
/// be `None`. This is used to allow for lazy initialisation of the layout and incremental updates
/// as the table is updated.
#[derive(Clone, Debug, Deserialize, Serialize, Default)]
pub struct TableSpecification {
/// The width of each column in characters.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please make subsection for TableSpecification, and the subsection for define_endpoints_2 below.

Comment on lines 56 to 63
impl Model {
/// Get the content of the model.
pub fn text(&self, width: usize) -> String {
match &self.content {
Content::Text { content, .. } => content.to_string(),
_ => " ".repeat(width),
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

What is width exactly? Why once we return string of given width, but in other case return content without adjusting width?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the parameter. This was used earlier for some formatting but is not needed any more. The current use is obsolete and can return an empty string instead.

@wdanilo
Copy link
Member

wdanilo commented Mar 15, 2023

What's the status of this PR? :)

@farmaazon
Copy link
Contributor

farmaazon commented Mar 15, 2023

Fixing bugs and then QA is still to be done AFAIK

…r/Lazy_Table_Visualisation_UI_Integration_5002

# Conflicts:
#	CHANGELOG.md
@enso-bot
Copy link

enso-bot bot commented Mar 19, 2023

Michael Mauderer reports a new STANDUP for the provided date (2023-03-17):

Progress: Worked on cleaning up code for the Lazy Table PR after fixing a regression affecting the Lazy Text visualization and doing additional testing. It should be finished by 2023-03-20.

Next Day: Next day I will be working on the #5931 task. Start on the "A drop-down that allows changing the execution mode" task.

…r/Lazy_Table_Visualisation_UI_Integration_5002

# Conflicts:
#	CHANGELOG.md
@enso-bot
Copy link

enso-bot bot commented Mar 20, 2023

Michael Mauderer reports a new STANDUP for today (2023-03-20):

Progress: Finished the bug fixes and cleanup on the Lazy Table PR. Will now go James for testing. Starting on "A drop-down that allows changing the execution mode". It should be finished by 2023-03-20.

Next Day: Next day I will be working on the #5931 task. Continue work on setting up all the entities for a demo scene with the new dropdown menu.

@vitvakatu
Copy link
Contributor

QA Report 🔴

Unfortunately, I found some issues.

When opening lazy vis, it does not display most of the entries

It only displays a field of a single entry, and you need to scroll for a while to allow the rest of the entries to load.

This does not happen when you reload the project, though – so if the lazy vis was open, it displays a full list after it's loaded. The way to reproduce it is to switch between the lazy vis and the legacy table.

2023-04-04.17-56-51.mp4

Scrolling feels more laggy than legacy vis

It takes a while to load new entries when I scroll.

2023-04-04.17-54-48.mp4

For comparison, legacy vis works more smoothly:

2023-04-04.17-55-52.mp4

Scrolling with two fingers on the touchpad does not work

Only dragging the scrollbar works. I can't see any scrollbars in full-screen mode, so it's impossible to scroll fullscreen vis without a scroll wheel.

When opening the full-screen vis, part of the entries is not displayed

2023-04-04.18-00-43.mp4

In full-screen vis, nodes are scrolled when you scroll

Most definitely not the issue of this particular PR.

It requires clicking on the visualization before scrolling to trigger this behavior.

@xvcgreg xvcgreg closed this Jun 13, 2023
@farmaazon farmaazon reopened this Jun 13, 2023
@farmaazon farmaazon requested a review from GregoryTravis as a code owner June 13, 2023 11:41
@farmaazon farmaazon marked this pull request as draft June 13, 2023 11:42
@xvcgreg
Copy link

xvcgreg commented Jun 26, 2023

@MichaelMauderer what's the status on this PR and what's the holdup?

@MichaelMauderer
Copy link
Contributor Author

@MichaelMauderer what's the status on this PR and what's the holdup?

It's not currently scheduled to be worked on due to other tasks being prioritized. Once it gets picked up again, it will need to be updated to develop and QA feedback needs to be addressed.

@hubertp
Copy link
Collaborator

hubertp commented Sep 22, 2023

Obsolete now, closing.

@hubertp hubertp closed this Sep 22, 2023
@farmaazon farmaazon deleted the wip/MichaelMauderer/Lazy_Table_Visualisation_UI_Integration_5002 branch May 9, 2024 07:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement a new GridView based multi-column Text Visualization (Table Visualization)
10 participants