nteract / semiotic

A data visualization framework combining React & D3
https://semioticv1.nteract.io/
Other
2.43k stars 133 forks source link

Frames' space allocation behavior can be generalized #601

Open alexeyraspopov opened 2 years ago

alexeyraspopov commented 2 years ago

For every distinct type of frame (XYFrame, NetworkFrame, OrdinalFrame) there is Responsive* and Spark* variants which main goal is to define particular space allocation behavior while the original frame assumes strict size being defined by the author. The need for explicit size definition doesn't exist without a reason: both SVG and Canvas require the size to be defined before they render the content within. Physical size being a type of visual values directly affects how exactly the content within should be rendered.

(following rationale is based on my experience and expectations in developing web UIs)

A piece of data visualization is often not a single unit of content on the web page: one might need a column or two of text content, or the viz is a part of application UI, which has plenty of controls, other sources of information and quite possibly a couple of more pieces of data viz. Before a developer steps into adding a data visualization piece there already plenty of work that is done. Given the variety of the devices used for browsing the internet one can never assume a single since of a viewport the app should work within. When you get to adding SVG or Canvas that start requiring to specify very explicit sizes, this is where you can get stuck figuring out all the possible scenarios and outcomes of choosing the wrong size.

This type of consideration never exists when you work with typical text or illustrations content and use flexbox or grid layout. Either the content fluidly defines the behavior of container, or the containers (based on its context) defines how children suppose to allocate space within. Spark* and Responsive* frames seem to be fitting this sort of inversion of control scenarios: despite having a content within (e.g. XYFrame) that was initially working only with predefined size, Responsive frame expects parent container in user space to define how the content should behave thus delegating the control over to the user.

Having this sort of control outside of any library or framework means the user has all the potential and flexibility of existing tools and their experience. This means no artificial limitation to how the library or framework can be integrated in an existing UI.

I still need to learn more about Spark* frames but I'd make a wild guess that the same reasoning is applicable to them as well.

Generalized behavior

Basically, the question is: what if we assume responsive behavior as a default space allocation behavior? size prop can still exist if the strict size is needed. The main reason for this is that it would mean top level Semiotic components are easily integrate-able into whatever UI users already have: notebooks, applications, web documents. It means no additional handling or size control needed within Semiotic components, as well as special API: the implementation of necessary space allocation behavior happens in the most basic way of how things behave on the web page. The users don't need to learn about additional semantics since they just use the basics of the environment they're working with. This also means that the documentation can be simplified to describe the common way of doing things.

It might be too verbose, so here is another look on this problem: if we assume responsive behavior as a default, we still can define a visualization frame that has static size, just because this is how the platform works and we don't even need a special top level API for this.

There plenty of small implementation details that go into this idea, I'll be trying to define them here:

Implementation

A lot of small implementation details that need to considered and some that I may not be familiar with, but in my mind the described behavior comes down to the following:

function XYFrame(props) {
  let ref = useRef();
  let size = useElementSize(ref);

  // as an option we can pass down some more styles applied to the wrapper
  // however it might be advised to just to keep those on the user side
  let style = { display: props.inline ? "inline-block" : "block", ...props.style };

  // in the same way as it happens now, computing all props that Frame needs
  // and the size is already available on this level, regardless of frame type
  let computedProps = { /* whatever <Frame /> needs */ }

  return (
    <div style={style} ref={ref}>
      {/* to ensure we don't render Frame with [0, 0] as it would be just waste of time */}
      {size != null ? <Frame size={size} {...computedProps} /> : null}
    </div>
  )
}

Quite possibly we can get rid of the notion of SpanOrDiv as a result of these changes.

Benefits

It is worth discussing the rationale behind suggested behavior. One might say that generalization can go even further and Semiotic could just export Frame as the only piece of API needed. Yet, it is about the amount of concepts necessary to get the task done.

Drawbacks

Things like SparkNetworkFrame may include very specific defaults for its original frame implementation which means there are plenty of cases where things can't really "just work".

How do we teach it

I believe the suggested behavior streamlines the teaching aspect of Semiotic. Frames become no different from other flexible/static containers (e.g. images) and the same rules apply: you either set an explicit size, or assume the fluid container to define available space. If an inline frame behavior needed, one simply set the style to "display: inline-block" and the rest of expected rules apply. I guess it still sounds as over simplification, so having a quick guide with visual examples should definitely help.

Breaking changes

The suggested behavior means not only some parts of API being removed but also the fact that frames no longer have default size. While I don't expect much of users to be relying on the default size, it is still reasonable to mention this change in changelog and migration guide. The basic suggestion can be something like "if you Semiotic v1 frame doesn't have size prop, it means it uses the default value, so set it to [500, 500] when migrating to avoid layout issues". More testing possibly required.

(the issue includes definitions of done that we can use to track related pull requests and the whole initiative progress)

@emeeks, @willingc, looking forward to your feedback and questions. I'm thinking about opening a draft PR with some preliminary implementation changes, to see how far we can actually go.