Adding the Concept of Tasks to SMTK

This topic is focused on supporting the concept of Tasks in SMTK.

What is a Task?

A Task represents an activity performed by a user, and as such represents the basic building block to a workflow. Since it is user-driven, a Task is user interface focused and relies heavily on the UI (both widgets and graphical views).

A Task’s goal is the manipulation of a set of SMTK Resources until they satisfy the Task’s completion requirement. For example, in Truchas a task could be to define a casting simulation in order to create an input deck.

Possible Task Conceptual Model

The figure above shows a possible structure for a Task. At the core of the Task is a set resources. Some of these resources are given to the Task in order to satisfy its requirements. A Task can only be performed by the user iff its starting requirements are met. Similarly, a Task also has a completion condition which can also be driven by its resources though we may want to expand it to include things like Has operation X been run successfully? . Unlike the starting conditions which always indicates if the Task can be started or not, the completion condition does not always indicates that the user is done with the Task. Instead it indicates that the user has satisfied enough of the Task such that it could be marked as completed.

These conditions would be represented by C++/Python functions that take in the Task, though we probably could support a declarative approach (at least of the starting requirements, similar to how an operation works).

Other parts of a Task would include:

  • Task View - an UI representation associated with the Task, similar in concept to the Attribute Views, though they would not be limited to only working attribute resources.
  • Set of Operations - tools needed for the user to manipulate the Task’s resources in order to complete the Task
  • Resource View Requirements - one of the main changes to the CMB ModelBuilder Architecture is to make the Resource View and a subset of the 3D windows, slaves to the Task being performed, instead of being independent UI constructs. As Task would be able to determine what should be initially displayed in the Resource View (perhaps even having its own Descriptive Phrase though that might be reaching a bit).
  • 3D Window Control - similar to the Resource View Requirements, a Task should be able to influence how the geometric and mesh resources are rendered (perhaps by having its own representation?). In addition, the 3D context menu should be modifiable by the Task to allow task specific interactions within the 3D window.
  • Selection Control - a Task should be able to set (by default?) what the selection mode should be.

Below shows a possible Task control diagram.

Couple of things to note:

Selection Unity

Selection is now unified meaning that there is no longer the question, if the user selects a model component in the resource view, does it effect the selection in the attribute view? The answer is yes. There is one selection state unified under the Task.

There is an open question concerning what if the user wants to select something that is not required by the Task? For example, the user is assigning boundary conditions but wants to display a volume for reference. The user could change the selection filter, but this will result in things getting into the Selection that would be rejected by the Task.

One possible approach would be to have the ability for the Task to ask the Selection to only return the set of Persistent Objects that matches a criteria. This could also be used to indicate during the selection process what the user has selected is appropriate for the Task at hand.

Dealing with Multiple 3D Views

Due to the underlying ParaView architecture, ModelBuilder can have an “unlimited” number of 3D Views. To deal with this, I would propose that we can “link” a Task with a 3D View and when the user switches to a different Task, the current active View will automatically switch to that Task as well.

Task or Task View

The above figure has the Task doing most of the work, though we could instead put most of the functionality into Task View itself as shown below.

In this case, the Task would only know about the Task View, while the Task View would deal with the majority of the UI interactions.

Relating Tasks with Current Views

In many ways we have already implemented some of the key features of tasks. Mainly the ability to tell the user if they have work to do in a current View or if they have sufficiently filled in enough information as shown below.

Here you can see the user still has work todo in the Modules Tab with respects to Enclosure Radiation Enclosures.

Lets assume for a moment that each Group View (represented by each Tab Level) represented a Parallel Task View (a group of tasks that could be performed in any order). Therefore each Tab would represent a Task. Lets also imagine for a movement we expand the idea of marking Tabs with icons. For example, we could do the following:

  • Ready to be Performed - red
  • Ready to be Marked Completed - green
  • Marked Completed - blue
    In addition we could disable the Tab if the Task is not ready to be performed. Here is a possible rendering of what the above could look like if it was a set of tasks.

If the top was a serial task group instead then the initial workflow could look like this.

Here the user must choose complete specifying the analysis before the user is allowed to continue.

Here is a possible workflow that explicitly includes exporting the analysis as a task.

In this mockup, the export task is initially marked not ready to run (using a black marker in this example). When the attribute resource is properly filled out, the export task goes to ready. When it is run then the top level group task of defining an analysis is marked as can be completed. If later, the user changes the attribute resource then the export task flips back to ready to run.

Ideas on the 3D View Side of Things

Within CMB, perhaps the 3D windows could have a Contents decoration and/or Context Menu entry that would allow the user to choose which Task is being displayed in the View?

For example, assume we have 2 Tasks: one to assign boundary conditions; another to assign material properties. ModelBuilder (using Tasks) could look like this:

The left 3D view is associated with the assign material task and is displaying volumes while the right side (associated with the assign boundary conditions task) is displaying surfaces.

I want to take this even further so that the tasks can influence how the model gets rendered as shown below:

Here the view for material assignment is color coded, where blue could indicate the volumes where the current material is assigned, red indicating where another material has been assigned, and white where no material is currently assigned.

@johnt @C_Wetterer-Nelson @dcthomp @aron.helser @Ryan_Krattiger - Comments definitely welcomed!

@Bob_Obara I love the idea of unifying large swaths of the UI under the Tasks umbrella. How would this handle Tasks where the user must, say, assign boundary conditions to surfaces and materials to volumes? Would you have two 3D Views, one for selecting faces, and another for selecting volumes, or would the Task manage logic to update the current 3D View based on which of those two procedures the user is taking care of at the moment?

Good Question - See this Section for an example of how this could work.

Thanks, Bob. I think this describes the concepts very well, and provides a good reference for more detailed work. My main reactions here are focused on software development considerations, even though you might have intended to address these in future discussions:

  • A first observation is that there are many similarities between tasks as described here and smtk operations. Tasks need a specification for their input/consuming resources, which I consider analogous to an operation’s specification (attribute). Tasks should also describe their output/producing resources, which I see as analogous to an operation’s result attribute. Operations have an ableToOperate() method to indicate that all required inputs have been specified; tasks should have a Ready state to indicate when all of their inputs are connected. Give these similarities, we should look at the software patterns used in smtk::operation::Operation when designing smtk::project::Task. One of the biggest areas of divergence might be that operations essentially have 2 states: running and not running; whereas task life cycle should be modeled by more involved state logic.
  • I think it is very important that we separate customized-view software from other task and workflow software. Users should be able to change the custom-view assigned to a task at runtime, and to reuse the same custom view settings across multiple tasks. Ideally, the user should be able to apply a custom view without any task or other workflow software running (perhaps with an internal no-op task hidden from the user). We also want to give the user as much flexibility as we can to create and edit custom views at runtime. I think it will be much easier to develop these features if our customized-view code is disjoint to the extent practicable from the other task/workflow software.
  • And finally, I’ll add my usual reminder that we should expand smtk’s data management features to encompass more than smtk resources. In our typical heat transfer workflow, the primary data from the end user’s perspective are the mesh (Genesis file) imported into modelbuilder and the Truchas input file generated by modelbuilder. The smtk resources that modelbuilder creates and work with serve as intermediate data that facilitate the user’s work. As much as we rely on them, smtk resources are not used by the numerical codes that our users run. In brief, our smtk project and task software would be greatly enhanced if we can represent non-smtk data assets.
1 Like

Definitely agree on all 3 points.

@Bob_Obara and I had a discussion about how this might work. There are many alternatives, and not all are mutually exclusive.

Tasks and Views

ParaView has the concept of an active view (note this only concerns pqView subclasses and does not extend to active panels which subclass QDockWidget). Switching the active view in ParaView may trigger a change in the active task or may not.

Our feeling is that it is more useful to have an active task assigned to each pqView and switching views will switch the active task (thus updating which tab in the task view is active).

Potential issues

  • An inactive view may have as its task something that becomes invalidated while in another view. Example: we have 2 ParaView views: the active view has “material assignment” as its task (showing volumes colored by material assigned) and the inactive view has “export simulation” as its task (set to show volumes colored by solvers that produce solutions on that volume). A user removes a material assignment.
    1. What should the inactive view display (i.e., there is now a volume for which no solver is assigned… do we have something to illustrate invalid state?)?
    2. When the user clicks on the inactive view to make it active, the task panel cannot switch to the “export simulation” tab. What should happen? (NB: Do not suggest a pop-up warning or usability experts everywhere will rise from their graves and seek you out.) One possibility is that you are not allowed to make that view active, but that is frustrating for users. Another possibility is to always allow tasks to become active, even if they have insufficient data.
  • It’s unclear how the pqSMTKAttributePanel would interact with a task panel. From the mockups above, perhaps we should eliminate pqSMTKAttributePanel and replace it with a new pqSMTKTaskPanel whose tabs could include attribute views specific to the task at hand. Then, when there is no workflow (and thus no task definitions), some generic tasks/task-definitions would be created for each resource. An alternative is to hide the attribute panel when using workflows and hide task panel when there is no workflow.

Task Switching

While tasks may have a purpose (e.g., assigning materials), what matters to their implementation is not their purpose but how to modify the user interface when a task becomes active or inactive. Thus it is changes in task state that we must consider. Tasks need a clear set of responsibilities when becoming active/inactive and a set of API methods to accomplish those responsibilities:

  • 3-D representations for related resources may be modified. Example: a “material assignment” task hides side sets and shows volumes, coloring them by material. (Potential issue: When the task becomes inactive, would component visibilities or coloring be changed to some (default?) state?)
  • Context menus are changed. (Potential issues: SMTK currently doesn’t provide context menus using ParaView’s extension, but perhaps it should so it could manage hiding/showing items as tasks change. Another option is for authors of context menu extensions to be “task-aware” and hide/show themselves depending on the active task.)
  • The resource panel (pqSMTKResourcePanel) may have its PhraseModel swapped or the PhraseModel’s badges changed. (Potential issue: When a task becomes inactive, what happens?)
  • The application selection may be replaced with the task’s selection (Potential issue: if tasks have selections, then tasks must monitor the application selection while the task is active and record/copy changes.)
  • The application selection filter may be change (i.e., the rules that suggest what SMTK components to select when users interact with a visible component). Example: the “material assignment” task sets the selection filter to choose volumes when surfaces are clicked while the “boundary condition” task sets the selection filter to choose side-sets (and/or node-sets) when surfaces are clicked.
  • The set of available operations is changed. Furthermore, there may be UI elements/interactions that automate the choice of parameter values for operations based on the task.
  • Keyboard shortcuts, toolbars, toolbar buttons, menu bars, and menu items (besides context menus) may be hidden/shown when a task transitions to its active or inactive state. (Potential issues: keyboard shortcuts are difficult as there could be collisions with per-widget shortcuts as well as per-task or per-view shortcuts. Having a panel to display – and perhaps edit – shortcuts might be advisable.)

Yes and no. I would not advocate for making tasks operations. In particular:

  • Operations lock resources which would be problematic if tasks needed to run operations.
  • Operations may run asynchronously and in parallel. Since tasks are user-interface components, users are expected to do one thing at a time.

You point out an interesting question concerning a 3D associated with a task that is no longer in a not ready to run state. Possible solutions could be:

  • Freeze the view if possible to indicate that the associated task is not ready
  • Introduce a red outline indicating the 3D window’s associated task is not ready to run
  • If we are displaying the associated task name in the view - color it red

In any case - the 3D view’s context menu would not contain the actions related to the associated Task.

@chart3388 @amuhsin @Aaron - FYI

Rather than making a task or task-view class that’s responsible for all of the things above, a modular design might instead have

  • smtk::task::Task – a class that provides only basic/necessary information about the task: methods to (a) describe/provide help for the task, (b) determine whether it can be active or not, and (c) determine whether it can be considered complete or not)
  • smtk::task::Manager – a class that registers available task classes in a project/workflow, holds the set of extant instances of tasks, holds the currently-active task, and invokes observers when
    • a task instance is created/destroyed
    • a task is newly-available for activation
    • a task becomes active
    • a task becomes inactive (or perhaps is about to become inactive)?
    • a task becomes unavailable for activation
    • a task becomes available for completion
  • smtk::task::qt::View – a small modification of qtGroupView that associates a task with each tab and switches the active task as the user selects tabs. We also discussed the ability for sub-tasks to appear as either (1) child tab-bars of their parent or (2) separate panels in their own right. This would provide a hint to the user that some tasks control larger portions of the workflow (an example was a project with simulations performed in stages as a design is iterated; the stages would appear in the top-level panel while the subtasks for each stage, plus their group views, would appear in a separate panel).
  • smtk::task::qt::Toolbar – a QToolbar that displays the active task, with a drop-down for switching to other available tasks and a checkbox for marking the current task completed. This could be in a separate plugin.
  • smtk::task::qt::Pipeline – a QDockWidget that displays all tasks in a ParaView-pipeline-like view. This could be in a separate plugin. @C_Wetterer-Nelson suggested a task-graph controller view (perhaps a ZUI?) would be useful.

Putting the Qt extensions in separate plugins would let us choose to expose different combinations in different applications.

With user interface components for task display and switching, task-extensions – aware of things such as 3-D representations, the resource panel, and selection filters – can observe changes to active task(s) and update them accordingly. This prevents the task class from becoming an amorphous blob. It would also allow applications that did not make use of some extensions to have a smaller footprint (i.e., the observer that configures representations in views for a particular task can be in a separate plugin).

Information such as operations that are available/preferred by a task could be held in separate objects that an smtk::common::Managers object would provide access to.

[edit: I’m not allowed to post new replies until someone else replies… adding the below as a progress update]

Update

There is now a merge request with a draft of a base Task class (and some auxiliary classes). The idea is that we would subclass Task to add some common classes like:

  • TaskNeedsResources – a task that monitors a resource manager for resources of a given type.
  • TaskNeedsProjectResources – a task that monitors a project manager for resources of a given type marked with given Roles.
  • TaskNeedsComponents – a task that is incomplete until the specified number of components of a specified type are available in the resource with the given role.
  • TaskNeedsAttributes – a task that needs valid attributes in an attribute resource (perhaps specified by a Role plus an attribute::Definition whose instances should be validated). @johnt @Bob_Obara Do you have thoughts on how to specify what you want while keeping it simple? Or should this be split into multiple classes that do slightly different things?
  • TaskNeedsAttributedComponents – a task that is incomplete until all components specified with a given (resource, role, component-query-filter)-tuple are associated to an attribute of a specified type (i.e., a given attribute::Definition). Optionally, this task might verify there are no un-associated attribute instances of the specified type (e.g., all boundaries have a boundary condition and no boundary conditions are without a boundary).
  • TaskNeedsJob – a task that is incomplete until a job (or jobs) reach a specified status (e.g., this task may be used to wait until a job is queued or until it completes). Another use is for tasks that are only available while a job is running (e.g., in-situ visualization may only occur during a simulation; canceling a mesh generation or simulation run may only happen while jobs exist).
  1. Thoughts on any other common tasks to implement?
  2. Tasks should probably be configured at construction with an nlohmann::json object or something similar to smtk::view::Configuration::Component (but cleaner). Does this seem agreeable to everyone? (This would add to the list of mandatory constructors every Task subclass must provide.) Here’s an example of what task configuration might look like for a couple of simple tasks:
{
  "load-files":
    {
      "type": "smtk::task::TaskNeedsResources",
      "title": "Load a simulation attribute and a model file.",
      "resources" :
        [
          { "role": "model geometry", "type": "smtk::model::Model" },
          {
            "role": "simulation attribute",
            "type": "smtk::attribute::Attribute",
            "validator" : {
              "type": "python",
              "script": "def validate(resource):\n  return any([x.type() == 'Foo' for x in resource.definitions()])"
            }
          }
        ]
    },
  "assign-boundary-conditions":
    {
      "type": "smtk::task::TaskNeedsAttributes",
      "title": "Assign boundary-conditions and materials.",
      "requirements":
        [
          {
            "attribute-source": { "role": "simulation attribute" },
            "attribute-type": { "definition": "boundary-condition" },
            "associations":
              {
                "type": "all-matching-components",
                "source": { "role": "model geometry" },
                "components" : { "filter": "face" }
              }
          },
          {
            "attribute-source": { "role": "simulation attribute" },
            "attribute-type": { "definition": "material" },
            "associations":
              {
                "type": "all-matching-components",
                "source": { "role": "model geometry" },
                "components" : { "filter": "volume" }
              }
          }
        ]
    }
}

Update 2

The merge request has been updated with changes to the base Task class and a subclass: TaskNeedsResources. The test has been expanded to include examples of how to use both.

Update 3

The merge request has been updated with these changes:

  • Added a (templated) smtk::common::Instances class that inherits smtk::common::Factory and extends it by owning references to all the objects it creates.
  • Replaced the smtk::task::TaskFactory with smtk::common::Instances<Task, ...> so that user-interface components can monitor tasks. Now, the task manager can be observed to see when tasks are added and, as they are added, individual tasks can be monitored for changes in state. Note that this is different than what was proposed above (having all events generated by the task::Manager).

@Bob_Obara asked for some more details on how the modular design might be implemented. As an example, there are many use cases where the 3-d representation should use custom coloring:

  • Associating boundary condition attributes to geometric surfaces:
    • Boundary condition surfaces are non-overlapping and should be colored uniquely with a special color for unassigned surfaces.
    • Boundary condition surfaces are overlapping and only one should be shown at a time, with a special color for unused regions that are available for assignment.
    • Boundary condition surfaces are overlapping and colors for overlapping regions should be a blend of colors assigned to all involved boundary conditions.
    • Boundary condition surfaces are non-overlapping but vary spatially and should be rendered with a scalar lookup table.
  • Associating materials to geometric volumes:
    • Surfaces should be colored differently on each side, showing the color of the material assigned to that side of the surface.
    • Material properties vary in space and the surface – plus any clip surfaces – show material property values with a scalar lookup table.

Most applications will only need 2 or 3 of these modes. Rather than have a task library that includes all of these cases, we propose separating them into different libraries. Applications or plugins would only link to what they need. The smtk::task::Task class would not have knowledge of any representation coloring modes. Instead, Registrar classes for each library would add an observer on the application’s smtk::task::Manager instance(s). For example, consider a library that implements a PerBoundaryConditionColorFunctor and instantiates a QToolbar that should be used to select a boundary-condition attribute-definition whose instances should be used for coloring.

void Registrar::registerTo(
  smtk::task::Manager::Ptr taskManager)
{
  // ...
  key = taskManager->taskObservers().insert(
    [&](smtk::task::Event event, smtk::task::Task& task) {
      if (task.isA<smtk::task::BoundaryConditionEdit>()) {
        if (event == smtk::task::Event::Activate) {
          PerBoundaryConditionColorFunctor colorFunctor(task);
          // Pass a color functor to each representation
          // in the active view.
          auto view = pqActiveObjects::instance()->activeView();
          for (auto& representation : view->representations()) {
            representation->setColorFunction(colorFunctor);
          }
          // Show a toolbar that modifies the color functor:
          qApp.activeWindow().findChild<
            smtkBoundaryConditionToolbar>()->show();
        } else if (event == smtk::task::Event::Deactivate) {
          // Hide a toolbar that modifies the color functor:
          qApp.activeWindow().findChild<
            smtkBoundaryConditionToolbar>()->hide();
        }
      }
    }
  );
}

void Registrar::unregisterFrom(smtk::task::Manager::Ptr taskManager)
{
  key.release();
}

@johnt The list above is similar to, but perhaps not as exhaustive as, the list of color-by modes in your slides from this afternoon. Perhaps you could include that list on discourse as a wiki page so we can all edit it and reconcile with that’s above?

I just looked over the First task MR, mostly the doc files, but I’ll post my comments here.

  1. My main interest in this feature is the expectation that it will enable customizing 3-D renderviews for a given attribute view. Is that planned for the next MR? is there a rough roadmap in mind?
  2. Along the same line, has any work been done yet on a strawman api for cutomizing the renderview for a given attribute view? Are there 2 parts for (i) assigning attribute views to a task, and (ii) customizing the renderviews for a task?
  3. I didn’t see any I/O or UI code, so I’ll presume they are also on the todo list.
  4. I see in one of the doc files that the “active” task is determined by user focus. At some point, we’ll want/need an API to get/set the active task.

Yes, but that will be done after the UI for making tasks active. Can you fill out the list of all the rendering styles you want? It would be nice to have the list here on discourse… I don’t know where the slides are that you listed them.

It is a thought experiment, but my idea is that you will ask the representation for an object that exposes API specific to the current coloring mode (so it doesn’t all have to live in the representation).

Yes, once the current MR is merged, @Bob_Obara has committed to writing the smtk::task::qt::View class to display tabs and make tasks active. After that’s done, we can add rendering modes because we’ll have notification for task switching.

Based on feedback from @Bob_Obara & @johnt, there will probably be some changes in later merge requests. The descriptions below are starting points for negotiation:

Task assets

The Task class will hold an alias (to TypeContainer or a TypeMap or even just a std::tuple) of “assets.” The assets might be Resources, Components, simulation result files, or any other data in a form produced by some user manipulation. These might not be created from scratch by the task but will be processed by the user to complete the task.


Example:
A TaskNeedsResources instance monitors a resource manager until both an attribute resource (with role “simulation info”) and one or more model resources (role “geometric model”) are present. The assets could be presented by TaskNeedsResources as an alias to a TypeContainer or even a std::tuple:

class TaskNeedsResources
{
public:
  using Assets = smtk::common::TypeContainer;
  const Assets& assets() const { return m_assets; }
protected:
  Assets m_assets;
};

// or even a very specific, less-configurable task:

class TaskNeedsAtributeAndModels
{
public:
  using Assets =
    std::tuple<
      smtk::attribute::Resource::Ptr,
      std::vector<smtk::model::Resource::Ptr>>;
  const Assets& assets() const { return m_assets; }
protected:
  Assets m_assets;
};

Whenever a task is completable, the assets() method may be called to obtain the “work product” of the user. Note that the assets method is not virtual; it is known at compile time for the sake of type safety.
\square


Dependencies as first-class objects

Instead of each Task owning dependencies stored as a std::set<Task::Ptr>, we’ll probably have Task own std::set<Dependency>, where Dependency will hold a Task::Ptr as well as references to how the dependent task should consume the assets presented by its upstream task. The Dependency:

  • owns a shared pointer to the TaskNeedsResources dependency (in a common base class);
  • describes which of the assets of the dependency are used and how to obtain them. This could be accomplished in a subclass of the base templated on the upstream and downstream tasks so that it could “swizzle” the Assets type-alias of the upstream task at compile time into something the downstream task could consume with type-safety.

Example:

A TaskNeedsResources instance titled Load a geometric model might present an Assets alias that includes

  • a model::Resource with role “solid model” and
  • an attribute::Resource with role “simulation info”.

These assets might be consumed by a dependent TaskNeedsAttributedComponents instance titled Assign boundary conditions. The TaskNeedsAttributedComponents instance will hold a Dependency<TaskNeedsResources,TaskNeedsAttributedComponents> object specialized to extract the resources from the upstream task, apply filters to the resource to obtain components to be attributed and attributes to be associated, and present the two sets of components to the downstream task via a simple API:

  • componentsToBeAttributed() – returns a set of components which need attributes
  • availableAttributes() – returns a set of attributes to associate with the components above.

This way the logic to connect task-data is not held by either task but a class with knowledge of both. An smtk::common::Factory would produce Dependency objects as instructed by the downstream task (whose Task::Configuration could include parameters needed to configure the dependency as well). This gives the system compile-time type-safety with run-time configuration.
\square


Task generators

Besides the classes in the MR, we need a grouping mechanism for tasks (used to indicate whether tasks should be exposed for serial or parallel completion).

While it is possible that this group of serial or parallel tasks could itself be a task, I propose that this grouping mechanism be called smtk::task::Generator and that it create task instances but not be a task itself because:

  • A task::Generator could edit the dependencies of its child tasks (some workflows allow expert users to complete tasks in parallel while novice users would be guided more strictly through a serial set of tasks).
  • The current task::Task class does not have any notion of children or parents, only one-way dependency arcs. Things that need to analyze the dependency graph should not have this cluttered by multiple edge types (parent-child in addition to dependency).
  • These task groupings are mainly for presentation; we did not cover use cases where they were functional unless they enforced some ordering (which can (arguably should) be done using dependencies).

Example:

It is possible to run tasks that

  • create material attributes
  • associate material attributes to model volumes
  • create+assign boundary condition attributes to model surfaces

in either series or parallel. A TaskGenerator class that has the tasks above as children would be responsible for

  • Creating an instance of each of the 3 tasks above
  • Adding dependencies to connect the assets needed by the tasks to each other or upstream dependencies.
  • Adding dependencies that – when configured as a serial generator – link the tasks into a sequence where each is dependent on its predecessor or – when configured as a parallel generator – link each task into a single, final task.
  • Providing introspection for user-interface components that wish to treat the child tasks as a block.

\square


I started Candidate RenderView modes for AttributeViews to list some visualization features. Let me know if that isn’t what you are looking for.

1 Like
Privacy Notice