mirror of
https://github.com/enso-org/enso.git
synced 2024-12-19 18:11:42 +03:00
Visualization Data Type (https://github.com/enso-org/ide/pull/413)
* Extend scaffolding for Native and HTML based visualizations.
* Use nicer dataset as an example.
* Define better S wrapper data interface.
* Refactor structs into submodules.
* Add examples of JS-based visualization.
Original commit: 48665e0498
This commit is contained in:
parent
24961ca674
commit
e454c125d6
2
gui/src/rust/Cargo.lock
generated
2
gui/src/rust/Cargo.lock
generated
@ -963,8 +963,10 @@ dependencies = [
|
||||
"enso-frp 0.1.0",
|
||||
"enso-prelude 0.1.0",
|
||||
"ensogl 0.1.0",
|
||||
"js-sys 0.3.35 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"logger 0.1.0",
|
||||
"nalgebra 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.106 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"shapely 0.1.0",
|
||||
"span-tree 0.1.0",
|
||||
|
@ -5,17 +5,19 @@ authors = ["Enso Team <contact@luna-lang.org>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
ast = { version = "0.1.0" , path = "../../ide/ast/impl" }
|
||||
span-tree = { version = "0.1.0" , path = "../../ide/span-tree" }
|
||||
shapely = { version = "0.1.0" , path = "../shapely/impl" }
|
||||
ensogl = { version = "0.1.0" , path = "../../ensogl" }
|
||||
enso-prelude = { version = "0.1.0" , path = "../prelude" }
|
||||
logger = { version = "0.1.0" , path = "../logger" }
|
||||
enso-frp = { version = "0.1.0" , path = "../frp" }
|
||||
ast = { version = "0.1.0" , path = "../../ide/ast/impl" }
|
||||
span-tree = { version = "0.1.0" , path = "../../ide/span-tree" }
|
||||
shapely = { version = "0.1.0" , path = "../shapely/impl" }
|
||||
ensogl = { version = "0.1.0" , path = "../../ensogl" }
|
||||
enso-prelude = { version = "0.1.0" , path = "../prelude" }
|
||||
logger = { version = "0.1.0" , path = "../logger" }
|
||||
enso-frp = { version = "0.1.0" , path = "../frp" }
|
||||
|
||||
wasm-bindgen = { version = "=0.2.58" , features = ["nightly"] }
|
||||
nalgebra = { version = "0.19.0" }
|
||||
serde_json = { version = "1.0" }
|
||||
wasm-bindgen = { version = "=0.2.58" , features = ["nightly","serde-serialize"] }
|
||||
nalgebra = { version = "0.19.0" , features = ["serde-serialize"] }
|
||||
serde_json = { version = "1.0" }
|
||||
serde = { version = "1.0" , features = ["derive"] }
|
||||
js-sys = { version = "0.3.28" }
|
||||
|
||||
[dependencies.web-sys]
|
||||
version = "0.3.4"
|
||||
|
@ -474,7 +474,7 @@ impl Node {
|
||||
let visualization_container = visualization::Container::new();
|
||||
visualization_container.mod_position(|t| {
|
||||
t.x = 60.0;
|
||||
t.y = -60.0;
|
||||
t.y = -120.0;
|
||||
});
|
||||
|
||||
display_object.add_child(&visualization_container);
|
||||
|
@ -1,276 +1,29 @@
|
||||
//! This module defines the visualization widgets and related functionality.
|
||||
//!
|
||||
//! At the core of this functionality is the `Visualisation` that takes in data and renders an
|
||||
//! output visualisation which is displayed in a `Container`. The `Container` provides generic UI
|
||||
//! elements that facilitate generic interactions, for example, visualisation selection. The
|
||||
//! `Container` also provides the FRP API that allows internal interaction with the
|
||||
//! `Visualisation`. Data for a visualisation has to be provided wrapped in the `Data` struct.
|
||||
//! The overall architecture of visualizations consists of three parts:
|
||||
//!
|
||||
//! 1. The `DataRenderer` trait provides the functionality to render the actual visualization view
|
||||
//! that implements `display::Object`. It is provided with data and itself provides frp streams
|
||||
//! of its output if there is some, for example, if it acts as a widget.
|
||||
//!
|
||||
//! 2. The `Visualization` is a struct that wraps the `DataRenderer` and implements the generic
|
||||
//! tasks that are the same for all visualisations. That is, interfacing with the other UI
|
||||
//! elements, the visualization registry, as well as propagating frp messages.
|
||||
//!
|
||||
//! 3. The `Container` wraps the `Visualisation` and provides the UI elements that facilitate
|
||||
//! user interactions. For example, selecting a visualisation or connecting it to nodes in the
|
||||
//! graph editor scene.
|
||||
//!
|
||||
//! In addition this module also contains a `Data` struct that provides a dynamically typed way to
|
||||
//! handle data for visualisations. This allows the `Visualisation` struct to be without type
|
||||
//! parameters and simplifies the FRP communication and complexity of the node system.
|
||||
|
||||
use crate::prelude::*;
|
||||
pub mod class;
|
||||
pub mod container;
|
||||
pub mod renderer;
|
||||
pub mod data;
|
||||
|
||||
use crate::frp;
|
||||
|
||||
use ensogl::display::DomSymbol;
|
||||
use ensogl::display;
|
||||
use ensogl::system::web;
|
||||
use serde_json;
|
||||
use web::StyleSetter;
|
||||
use ensogl::display::object::traits::*;
|
||||
|
||||
|
||||
|
||||
// ============================================
|
||||
// === Wrapper for Visualisation Input Data ===
|
||||
// ============================================
|
||||
|
||||
/// Wrapper for data that can be consumed by a visualisation.
|
||||
// TODO replace with better typed data wrapper.
|
||||
#[derive(Clone,CloneRef,Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Data {
|
||||
JSON { content : Rc<serde_json::Value> },
|
||||
Binary,
|
||||
}
|
||||
|
||||
impl Data {
|
||||
/// Render the data as JSON.
|
||||
pub fn as_json(&self) -> String {
|
||||
match &self {
|
||||
Data::JSON { content } => content.to_string(),
|
||||
_ => "{}".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============================================
|
||||
// === Internal Visualisation Representation ===
|
||||
// =============================================
|
||||
|
||||
/// Inner representation of a visualisation.
|
||||
#[derive(Clone,CloneRef,Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct Visualization {
|
||||
content : DomSymbol
|
||||
}
|
||||
|
||||
impl display::Object for Visualization {
|
||||
fn display_object(&self) -> &display::object::Instance {
|
||||
&self.content.display_object()
|
||||
}
|
||||
}
|
||||
|
||||
impl Visualization {
|
||||
/// Update the visualisation with the given data.
|
||||
// TODO remove dummy functionality and use an actual visualisation
|
||||
pub fn set_data(&self, data:Data){
|
||||
self.content.dom().set_inner_html(
|
||||
&format!(r#"
|
||||
<svg>
|
||||
<circle style="fill: #69b3a2" stroke="black" cx=50 cy=50 r={}></circle>
|
||||
</svg>
|
||||
"#, data.as_json()));
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DomSymbol> for Visualization {
|
||||
fn from(symbol:DomSymbol) -> Self {
|
||||
Visualization { content : symbol }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// =========================
|
||||
// === Visualization FRP ===
|
||||
// =========================
|
||||
|
||||
/// Visualization events.
|
||||
#[derive(Clone,CloneRef,Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct ContainerFrp {
|
||||
pub network : frp::Network,
|
||||
pub set_visibility : frp::Source<bool>,
|
||||
pub toggle_visibility : frp::Source,
|
||||
pub set_visualization : frp::Source<Option<Visualization>>,
|
||||
pub set_data : frp::Source<Option<Data>>,
|
||||
}
|
||||
|
||||
impl Default for ContainerFrp {
|
||||
fn default() -> Self {
|
||||
frp::new_network! { visualization_events
|
||||
def set_visibility = source();
|
||||
def toggle_visibility = source();
|
||||
def set_visualization = source();
|
||||
def set_data = source();
|
||||
};
|
||||
let network = visualization_events;
|
||||
Self {network,set_visibility,set_visualization,toggle_visibility,set_data }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ================================
|
||||
// === Visualizations Container ===
|
||||
// ================================
|
||||
|
||||
/// Container that wraps a `Visualization` for rendering and interaction in the GUI.
|
||||
///
|
||||
/// The API to interact with the visualisation is exposed through the `ContainerFrp`.
|
||||
#[derive(Clone,CloneRef,Debug,Shrinkwrap)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct Container {
|
||||
// The internals are split into two structs: `ContainerData` and `ContainerFrp`. The
|
||||
// `ContainerData` contains the actual data and logic for the `Container`. The `ContainerFrp`
|
||||
// contains the FRP api and network. This split is required to avoid creating cycles in the FRP
|
||||
// network: the FRP network holds `Rc`s to the `ContainerData` and thus must not live in the
|
||||
// same struct.
|
||||
|
||||
#[shrinkwrap(main_field)]
|
||||
data : Rc<ContainerData>,
|
||||
pub frp : Rc<ContainerFrp>,
|
||||
}
|
||||
|
||||
/// Internal data of a `Container`.
|
||||
#[derive(Debug,Clone)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct ContainerData {
|
||||
logger : Logger,
|
||||
display_object : display::object::Instance,
|
||||
size : Cell<Vector2<f32>>,
|
||||
visualization : RefCell<Option<Visualization>>,
|
||||
}
|
||||
|
||||
impl ContainerData {
|
||||
/// Set whether the visualisation should be visible or not.
|
||||
pub fn set_visibility(&self, visibility:bool) {
|
||||
if let Some(vis) = self.visualization.borrow().as_ref() {
|
||||
if visibility { self.add_child(&vis) } else { vis.unset_parent() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates whether the visualisation is visible.
|
||||
pub fn is_visible(&self) -> bool {
|
||||
self.visualization.borrow().as_ref().map(|t| t.has_parent()).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Toggle visibility.
|
||||
pub fn toggle_visibility(&self) {
|
||||
self.set_visibility(!self.is_visible())
|
||||
}
|
||||
|
||||
/// Update the data in the inner visualisation.
|
||||
pub fn set_data(&self, data:Data) {
|
||||
self.visualization.borrow().for_each_ref(|vis| vis.set_data(data));
|
||||
}
|
||||
|
||||
/// Set the visualization shown in this container..
|
||||
pub fn set_visualisation(&self, visualization:Visualization) {
|
||||
let size = self.size.get();
|
||||
visualization.content.set_size(size);
|
||||
self.display_object.add_child(&visualization);
|
||||
self.visualization.replace(Some(visualization));
|
||||
self.set_visibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
impl display::Object for ContainerData {
|
||||
fn display_object(&self) -> &display::object::Instance {
|
||||
&self.display_object
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Container {
|
||||
/// Constructor.
|
||||
pub fn new() -> Self {
|
||||
let logger = Logger::new("visualization_container");
|
||||
let visualization = default();
|
||||
let size = Cell::new(Vector2::new(100.0, 100.0));
|
||||
let display_object = display::object::Instance::new(&logger);
|
||||
let data = ContainerData {logger,visualization,size,display_object};
|
||||
let data = Rc::new(data);
|
||||
let frp = default();
|
||||
Self {data, frp} . init_frp()
|
||||
}
|
||||
|
||||
fn init_frp(self) -> Self {
|
||||
let frp = &self.frp;
|
||||
let network = &self.frp.network;
|
||||
|
||||
frp::extend! { network
|
||||
|
||||
let container_data = &self.data;
|
||||
|
||||
def _set_visibility = frp.set_visibility.map(f!((container_data)(is_visible) {
|
||||
container_data.set_visibility(*is_visible);
|
||||
}));
|
||||
|
||||
def _toggle_visibility = frp.toggle_visibility.map(f!((container_data)(_) {
|
||||
container_data.toggle_visibility()
|
||||
}));
|
||||
|
||||
def _set_visualization = frp.set_visualization.map(f!((container_data)(visualisation) {
|
||||
if let Some(visualisation) = visualisation.as_ref() {
|
||||
container_data.set_visualisation(visualisation.clone_ref());
|
||||
}
|
||||
}));
|
||||
|
||||
def _set_data = frp.set_data.map(f!((container_data)(data) {
|
||||
if let Some(data) = data.as_ref() {
|
||||
container_data.set_data(data.clone_ref());
|
||||
}
|
||||
}));
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Container {
|
||||
fn default() -> Self {
|
||||
Container::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl display::Object for Container {
|
||||
fn display_object(&self) -> &display::object::Instance {
|
||||
&self.data.display_object
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =================
|
||||
// === Mock Data ===
|
||||
// =================
|
||||
|
||||
/// Dummy content for testing.
|
||||
// FIXME[mm] remove this when actual content is available.
|
||||
pub(crate) fn default_content() -> DomSymbol {
|
||||
let div = web::create_div();
|
||||
div.set_style_or_panic("width","100px");
|
||||
div.set_style_or_panic("height","100px");
|
||||
|
||||
let content = web::create_element("div");
|
||||
content.set_inner_html(
|
||||
r#"<svg>
|
||||
<circle style="fill: #69b3a2" stroke="black" cx=50 cy=50 r=20></circle>
|
||||
</svg>
|
||||
"#);
|
||||
content.set_attribute("width","100%").unwrap();
|
||||
content.set_attribute("height","100%").unwrap();
|
||||
|
||||
div.append_child(&content).unwrap();
|
||||
|
||||
let r = 102_u8;
|
||||
let g = 153_u8;
|
||||
let b = 194_u8;
|
||||
let color = iformat!("rgb({r},{g},{b})");
|
||||
div.set_style_or_panic("background-color",color);
|
||||
|
||||
let symbol = DomSymbol::new(&div);
|
||||
symbol.dom().set_attribute("id","vis").unwrap();
|
||||
symbol.dom().style().set_property("overflow","hidden").unwrap();
|
||||
symbol
|
||||
}
|
||||
pub use class::*;
|
||||
pub use data::*;
|
||||
pub use container::*;
|
||||
pub use renderer::*;
|
||||
|
@ -0,0 +1,156 @@
|
||||
//! This module defines the `Visualization` struct and related functionality.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::frp;
|
||||
use crate::visualization::*;
|
||||
|
||||
use ensogl::display;
|
||||
|
||||
|
||||
|
||||
// ====================
|
||||
// === Helper Types ===
|
||||
// ====================
|
||||
|
||||
/// Type alias for a string containing enso code.
|
||||
#[derive(Clone,CloneRef,Debug)]
|
||||
pub struct EnsoCode {
|
||||
content: Rc<String>
|
||||
}
|
||||
|
||||
/// Type alias for a string representing an enso type.
|
||||
#[derive(Clone,CloneRef,Debug)]
|
||||
pub struct EnsoType {
|
||||
content: Rc<String>
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =========================
|
||||
// === Visualization FRP ===
|
||||
// =========================
|
||||
|
||||
/// Events that are used by the visualization.
|
||||
#[derive(Clone,CloneRef,Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct Frp {
|
||||
/// Can be sent to set the data of the visualization.
|
||||
pub set_data : frp::Source<Option<Data>>,
|
||||
/// Will be emitted if the visualization has new data (e.g., through UI interaction).
|
||||
/// Data is provides encoded as EnsoCode.
|
||||
pub on_change : frp::Stream<Option<EnsoCode>>,
|
||||
/// Will be emitted if the visualization changes it's preprocessor code.
|
||||
pub on_preprocess_change : frp::Stream<Option<EnsoCode>>,
|
||||
/// Will be emitted if the visualization has been provided with invalid data.
|
||||
pub on_invalid_data : frp::Stream<()>,
|
||||
|
||||
// Internal sources that feed the public streams.
|
||||
change : frp::Source<Option<EnsoCode>>,
|
||||
preprocess_change : frp::Source<Option<EnsoCode>>,
|
||||
invalid_data : frp::Source<()>,
|
||||
|
||||
}
|
||||
|
||||
impl Frp {
|
||||
fn new(network: &frp::Network) -> Self {
|
||||
frp::extend! { network
|
||||
def change = source();
|
||||
def preprocess_change = source();
|
||||
def invalid_data = source();
|
||||
def set_data = source();
|
||||
|
||||
let on_change = change.clone_ref().into();
|
||||
let on_preprocess_change = preprocess_change.clone_ref().into();
|
||||
let on_invalid_data = invalid_data.clone_ref().into();
|
||||
};
|
||||
Self { on_change,on_preprocess_change,set_data,on_invalid_data,change
|
||||
,preprocess_change,invalid_data}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ===========================
|
||||
// === Visualization Model ===
|
||||
// ===========================
|
||||
|
||||
/// Internal data of Visualization.
|
||||
#[derive(Clone,CloneRef,Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct State {
|
||||
pub renderer : Rc<dyn DataRenderer>,
|
||||
pub preprocessor : Rc<RefCell<Option<EnsoCode>>>,
|
||||
}
|
||||
|
||||
impl display::Object for State {
|
||||
fn display_object(&self) -> &display::object::Instance {
|
||||
&self.renderer.display_object()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =====================
|
||||
// === Visualization ===
|
||||
// =====================
|
||||
|
||||
/// A visualization that can be rendered and interacted with. Provides an frp API.
|
||||
#[derive(Clone,CloneRef,Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct Visualization {
|
||||
pub network : frp::Network,
|
||||
pub frp : Frp,
|
||||
state : State
|
||||
}
|
||||
|
||||
impl display::Object for Visualization {
|
||||
fn display_object(&self) -> &display::object::Instance {
|
||||
&self.state.display_object()
|
||||
}
|
||||
}
|
||||
|
||||
impl Visualization {
|
||||
/// Create a new `Visualization` with the given `DataRenderer`.
|
||||
pub fn new<T: DataRenderer + 'static>(renderer:T) -> Self {
|
||||
let preprocessor = default();
|
||||
let network = default();
|
||||
let frp = Frp::new(&network);
|
||||
let renderer = Rc::new(renderer);
|
||||
let internal = State {preprocessor,renderer};
|
||||
Visualization{frp, state: internal,network}.init()
|
||||
}
|
||||
|
||||
fn init(self) -> Self {
|
||||
let network = &self.network;
|
||||
let visualization = &self.state;
|
||||
let frp = &self.frp;
|
||||
frp::extend! { network
|
||||
def _set_data = self.frp.set_data.map(f!((frp,visualization)(data) {
|
||||
if let Some(data) = data {
|
||||
if visualization.renderer.receive_data(data.clone_ref()).is_err() {
|
||||
frp.invalid_data.emit(())
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
let renderer_frp = self.state.renderer.frp();
|
||||
let renderer_network = &renderer_frp.network;
|
||||
frp::new_bridge_network! { [network,renderer_network]
|
||||
def _on_changed = renderer_frp.on_change.map(f!((frp)(data) {
|
||||
frp.change.emit(data)
|
||||
}));
|
||||
def _on_preprocess_change = renderer_frp.on_preprocess_change.map(f!((frp)(data) {
|
||||
frp.preprocess_change.emit(data.as_ref().map(|code|code.clone_ref()))
|
||||
}));
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the viewport size of the visualization.
|
||||
pub fn set_size(&self, size:Vector2<f32>) {
|
||||
self.state.renderer.set_size(size)
|
||||
}
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
//! This module defines the `Container` struct and related functionality.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::frp;
|
||||
use crate::visualization::*;
|
||||
|
||||
use ensogl::display::traits::*;
|
||||
use ensogl::display;
|
||||
|
||||
|
||||
|
||||
// ===========
|
||||
// === FRP ===
|
||||
// ===========
|
||||
|
||||
/// Event system of the `Container`.
|
||||
#[derive(Clone,CloneRef,Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct ContainerFrp {
|
||||
pub network : frp::Network,
|
||||
pub set_visibility : frp::Source<bool>,
|
||||
pub toggle_visibility : frp::Source,
|
||||
pub set_visualization : frp::Source<Option<Visualization>>,
|
||||
pub set_data : frp::Source<Option<Data>>,
|
||||
}
|
||||
|
||||
impl Default for ContainerFrp {
|
||||
fn default() -> Self {
|
||||
frp::new_network! { visualization_events
|
||||
def set_visibility = source();
|
||||
def toggle_visibility = source();
|
||||
def set_visualization = source();
|
||||
def set_data = source();
|
||||
};
|
||||
let network = visualization_events;
|
||||
Self {network,set_visibility,set_visualization,toggle_visibility,set_data }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ================================
|
||||
// === Visualizations Container ===
|
||||
// ================================
|
||||
|
||||
/// Container that wraps a `Visualization` for rendering and interaction in the GUI.
|
||||
///
|
||||
/// The API to interact with the visualisation is exposed through the `ContainerFrp`.
|
||||
#[derive(Clone,CloneRef,Debug,Shrinkwrap)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct Container {
|
||||
// The internals are split into two structs: `ContainerData` and `ContainerFrp`. The
|
||||
// `ContainerData` contains the actual data and logic for the `Container`. The `ContainerFrp`
|
||||
// contains the FRP api and network. This split is required to avoid creating cycles in the FRP
|
||||
// network: the FRP network holds `Rc`s to the `ContainerData` and thus must not live in the
|
||||
// same struct.
|
||||
|
||||
#[shrinkwrap(main_field)]
|
||||
data : Rc<ContainerData>,
|
||||
pub frp : ContainerFrp,
|
||||
}
|
||||
|
||||
/// Internal data of a `Container`.
|
||||
#[derive(Debug,Clone)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct ContainerData {
|
||||
logger : Logger,
|
||||
display_object : display::object::Instance,
|
||||
size : Cell<Vector2<f32>>,
|
||||
visualization : RefCell<Option<Visualization>>,
|
||||
}
|
||||
|
||||
impl ContainerData {
|
||||
/// Set whether the visualisation should be visible or not.
|
||||
pub fn set_visibility(&self, is_visible:bool) {
|
||||
if let Some(vis) = self.visualization.borrow().as_ref() {
|
||||
if is_visible {
|
||||
vis.display_object().set_parent(&self.display_object);
|
||||
} else {
|
||||
vis.display_object().unset_parent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicates whether the visualisation is visible.
|
||||
fn is_visible(&self) -> bool {
|
||||
if let Some(vis) = self.visualization.borrow().as_ref() {
|
||||
vis.has_parent()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle visibility.
|
||||
fn toggle_visibility(&self) {
|
||||
self.set_visibility(!self.is_visible())
|
||||
}
|
||||
|
||||
/// Update the content properties with the values from the `ContainerData`.
|
||||
///
|
||||
/// Needs to called when a visualisation has been set.
|
||||
fn init_visualisation_properties(&self) {
|
||||
let size = self.size.get();
|
||||
if let Some(vis) = self.visualization.borrow().as_ref() {
|
||||
vis.set_size(size);
|
||||
};
|
||||
self.set_visibility(true);
|
||||
}
|
||||
|
||||
/// Set the visualization shown in this container..
|
||||
fn set_visualisation(&self, visualization:Visualization) {
|
||||
visualization.display_object().set_parent(&self.display_object);
|
||||
self.visualization.replace(Some(visualization));
|
||||
self.init_visualisation_properties();
|
||||
}
|
||||
}
|
||||
|
||||
impl Container {
|
||||
/// Constructor.
|
||||
pub fn new() -> Self {
|
||||
let logger = Logger::new("visualization");
|
||||
let visualization = default();
|
||||
let size = Cell::new(Vector2::new(100.0, 100.0));
|
||||
let display_object = display::object::Instance::new(&logger);
|
||||
let data = ContainerData {logger,visualization,size,display_object};
|
||||
let data = Rc::new(data);
|
||||
let frp = default();
|
||||
Self {data,frp} . init_frp()
|
||||
}
|
||||
|
||||
fn init_frp(self) -> Self {
|
||||
let frp = &self.frp;
|
||||
let network = &self.frp.network;
|
||||
|
||||
frp::extend! { network
|
||||
|
||||
let container_data = &self.data;
|
||||
|
||||
def _f_hide = frp.set_visibility.map(f!((container_data)(is_visible) {
|
||||
container_data.set_visibility(*is_visible);
|
||||
}));
|
||||
|
||||
def _f_toggle = frp.toggle_visibility.map(f!((container_data)(_) {
|
||||
container_data.toggle_visibility()
|
||||
}));
|
||||
|
||||
def _f_set_vis = frp.set_visualization.map(f!((container_data)(visualisation) {
|
||||
if let Some(visualisation) = visualisation.as_ref() {
|
||||
container_data.set_visualisation(visualisation.clone());
|
||||
}
|
||||
}));
|
||||
|
||||
def _f_set_data = frp.set_data.map(f!((container_data)(data) {
|
||||
container_data.visualization.borrow()
|
||||
.for_each_ref(|vis| vis.frp.set_data.emit(data));
|
||||
}));
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Container {
|
||||
fn default() -> Self {
|
||||
Container::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl display::Object for Container {
|
||||
fn display_object(&self) -> &display::object::Instance {
|
||||
&self.data.display_object
|
||||
}
|
||||
}
|
@ -0,0 +1,108 @@
|
||||
//! This module defines the `Data` struct and related functionality.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::component::visualization::EnsoType;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
|
||||
|
||||
// ======================================
|
||||
// === Wrapper for Visualisation Data ===
|
||||
// =======================================
|
||||
|
||||
/// Type indicator
|
||||
pub type DataType = EnsoType;
|
||||
|
||||
/// Wrapper for data that can be consumed by a visualisation.
|
||||
/// TODO[mm] consider static versus dynamic typing for visualizations and data!
|
||||
#[derive(Clone,CloneRef,Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum Data {
|
||||
JSON { content : Rc<serde_json::Value> },
|
||||
// TODO replace with actual binary data stream.
|
||||
Binary { content : Rc<dyn Any> },
|
||||
}
|
||||
|
||||
impl Data {
|
||||
/// Returns the data as as JSON. If the data cannot be returned as JSON, it will return a
|
||||
/// `DataError` instead.
|
||||
pub fn as_json(&self) -> Result<Rc<serde_json::Value>, DataError> {
|
||||
match &self {
|
||||
Data::JSON { content } => Ok(Rc::clone(content)),
|
||||
_ => { Err(DataError::InvalidDataType{}) },
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the wrapped data in Rust format. If the data cannot be returned as rust datatype, a
|
||||
/// `DataError` is returned instead.
|
||||
pub fn as_binary<T>(&self) -> Result<Rc<T>, DataError>
|
||||
where for<'de> T:Deserialize<'de> + 'static {
|
||||
match &self {
|
||||
Data::JSON { content } => {
|
||||
// We try to deserialize here. Just in case it works.
|
||||
// This is useful for simple data types where we don't want to care to much about
|
||||
// representation, e.g., a list of numbers.
|
||||
let value : serde_json::Value = content.as_ref().clone();
|
||||
if let Ok(result) = serde_json::from_value(value) {
|
||||
Ok(Rc::new(result))
|
||||
} else {
|
||||
Err(DataError::InvalidDataType)
|
||||
}
|
||||
},
|
||||
Data::Binary { content } => { Rc::clone(content).downcast()
|
||||
.or(Err(DataError::InvalidDataType))},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==============
|
||||
// === Errors ===
|
||||
// ==============
|
||||
|
||||
/// Indicates a problem with the provided data. That is, the data has the wrong format, or maybe
|
||||
/// violates some other assumption of the visualization.
|
||||
// TODO[mm] add more information to errors once typing is defined.
|
||||
#[derive(Copy,Clone,Debug)]
|
||||
pub enum DataError {
|
||||
/// Indicates that that the provided data type does not match the expected data type.
|
||||
InvalidDataType,
|
||||
/// The data caused an error in the computation of the visualisation.
|
||||
InternalComputationError,
|
||||
}
|
||||
|
||||
|
||||
|
||||
// =============================
|
||||
// === Sample Data Generator ===
|
||||
// =============================
|
||||
// TODO this will go away once we have real data
|
||||
|
||||
#[derive(Clone,CloneRef,Debug,Default)]
|
||||
pub(crate) struct MockDataGenerator3D {
|
||||
counter: Rc<Cell<f32>>
|
||||
}
|
||||
|
||||
impl MockDataGenerator3D {
|
||||
|
||||
pub fn generate_data(&self) -> Vec<Vector3<f32>> {
|
||||
|
||||
let current_value = self.counter.get();
|
||||
self.counter.set(current_value + 0.1);
|
||||
|
||||
let delta1 = current_value.sin() * 10.0;
|
||||
let delta2 = current_value.cos() * 10.0;
|
||||
|
||||
vec![
|
||||
Vector3::new(25.0, 75.0, 25.0 + delta1),
|
||||
Vector3::new(25.0, 25.0, 25.0 + delta2),
|
||||
Vector3::new(75.0 - 12.5, 75.0 + delta1, 5.0 ),
|
||||
Vector3::new(75.0 + 12.5, 75.0 + delta2, 15.0 ),
|
||||
Vector3::new(75.0 - 12.5 + delta1, 25.0 + delta2, 5.0 ),
|
||||
Vector3::new(75.0 + 12.5 + delta2, 25.0 + delta1, 15.0 ),
|
||||
]
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
//! This module defines the `DataRenderer` trait, related functionality and examples of how to use
|
||||
//! the `DataRenderer`.
|
||||
|
||||
mod class;
|
||||
mod js;
|
||||
pub mod example;
|
||||
|
||||
pub use class::*;
|
||||
pub use js::*;
|
@ -0,0 +1,79 @@
|
||||
//! This module defines the `DataRenderer` trait and related functionality.
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::visualization::*;
|
||||
|
||||
use crate::frp;
|
||||
|
||||
use ensogl::display;
|
||||
|
||||
|
||||
|
||||
// ===========
|
||||
// === FRP ===
|
||||
// ===========
|
||||
|
||||
/// FRP api of a `DataRenderer`.
|
||||
#[derive(Clone,Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct DataRendererFrp {
|
||||
pub network : frp::Network,
|
||||
/// This is emitted if the state of the renderer has been changed by UI interaction.
|
||||
/// It contains the output data of this visualisation if there is some.
|
||||
pub on_change : frp::Stream<Option<EnsoCode>>,
|
||||
/// Will be emitted if the visualization changes it's preprocessor. Transmits the new
|
||||
/// preprocessor code.
|
||||
pub on_preprocess_change : frp::Stream<Option<EnsoCode>>,
|
||||
// Internal sources that feed the public streams.
|
||||
change : frp::Source<Option<EnsoCode>>,
|
||||
preprocess_change : frp::Source<Option<EnsoCode>>,
|
||||
}
|
||||
|
||||
impl Default for DataRendererFrp {
|
||||
fn default() -> Self {
|
||||
frp::new_network! { renderer_events
|
||||
def change = source();
|
||||
def preprocess_change = source();
|
||||
|
||||
let on_change = change.clone_ref().into();
|
||||
let on_preprocess_change = preprocess_change.clone_ref().into();
|
||||
};
|
||||
let network = renderer_events;
|
||||
Self {network,on_change,on_preprocess_change,change,preprocess_change}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ====================
|
||||
// === DataRenderer ===
|
||||
// ====================
|
||||
|
||||
/// At the core of the visualization system sits a `DataRenderer`. The DataRenderer is in charge of
|
||||
/// producing a `display::Object` that will be shown in the scene. It will create FRP events to
|
||||
/// indicate updates to its output data (e.g., through user interaction).
|
||||
///
|
||||
/// A DataRenderer can indicate what kind of data it can use to create a visualisation through the
|
||||
/// `valid_input_types` method. This serves as a hint, it will also reject invalid input in the
|
||||
/// `set_data` method with a `DataError`. The owner of the `DataRenderer` is in charge of producing
|
||||
/// UI feedback to indicate a problem with the data.
|
||||
pub trait DataRenderer: display::Object + Debug {
|
||||
/// Indicate which `DataType`s can be rendered by this visualization.
|
||||
fn valid_input_types(&self) -> Vec<DataType> {
|
||||
// TODO this will need to be implemented by each Renderer once we get to the registry.
|
||||
unimplemented!()
|
||||
}
|
||||
/// Receive the data that should be rendered. If the data is valid, it will return the data as
|
||||
/// processed by this `DataRenderer`, if the data is of an invalid data type, it violates other
|
||||
/// assumptions of this `DataRenderer`, a `DataError` is returned.
|
||||
fn receive_data(&self, data:Data) -> Result<(), DataError>;
|
||||
/// Set the size of viewport of the visualization. The visualisation must not render outside of
|
||||
/// this viewport.
|
||||
fn set_size(&self, size:Vector2<f32>);
|
||||
|
||||
/// Return a ref to the internal FRP network. This replaces a potential callback mechanism.
|
||||
///
|
||||
/// Note: the presence of this functions imposes the requirement that a `DataRendererFrp` is
|
||||
/// owned by whoever implements this trait.
|
||||
fn frp(&self) -> &DataRendererFrp;
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
//! The `example` modules contains some examples of visualisations.
|
||||
//! TODO further describe the examples, how they work and how to extend them.
|
||||
|
||||
pub mod native;
|
||||
pub mod js;
|
@ -0,0 +1,139 @@
|
||||
//! Example of the visualisation JS wrapper API usage
|
||||
// TODO remove once we have proper visualizations or replace with a nice d3 example.
|
||||
// These implementations are neither efficient nor pretty, but get the idea across.
|
||||
|
||||
use crate::component::visualization::JsRenderer;
|
||||
|
||||
/// Returns a simple bubble chart implemented in vanilla JS. uses single functions to implement the
|
||||
/// visualization.
|
||||
pub fn function_sample_js_bubble_chart() -> JsRenderer {
|
||||
let fn_set_data = r#"{
|
||||
const xmlns = "http://www.w3.org/2000/svg";
|
||||
const root = arguments[0];
|
||||
while (root.firstChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
const svgElem = document.createElementNS(xmlns, "svg");
|
||||
svgElem.setAttributeNS(null, "id" , "vis-svg");
|
||||
svgElem.setAttributeNS(null, "viewBox", "0 0 " + 100 + " " + 100);
|
||||
svgElem.setAttributeNS(null, "width" , 100);
|
||||
svgElem.setAttributeNS(null, "height" , 100);
|
||||
root.appendChild(svgElem);
|
||||
|
||||
const data = arguments[1];
|
||||
data.forEach(data => {
|
||||
const bubble = document.createElementNS(xmlns,"circle");
|
||||
bubble.setAttributeNS(null,"stroke", "black");
|
||||
bubble.setAttributeNS(null,"fill" , "red");
|
||||
bubble.setAttributeNS(null,"r" , data[2]);
|
||||
bubble.setAttributeNS(null,"cx" , data[0]);
|
||||
bubble.setAttributeNS(null,"cy" , data[1]);
|
||||
svgElem.appendChild(bubble);
|
||||
});
|
||||
}
|
||||
"#;
|
||||
|
||||
let fn_set_size = r#"{
|
||||
const root = arguments[0];
|
||||
const width = arguments[1][0];
|
||||
const height = arguments[1][1];
|
||||
const svgElem = root.firstChild;
|
||||
svgElem.setAttributeNS(null, "viewBox", "0 0 " + width + " " + height);
|
||||
svgElem.setAttributeNS(null, "width" , width);
|
||||
svgElem.setAttributeNS(null, "height" , height);
|
||||
}"#;
|
||||
JsRenderer::from_functions(fn_set_data, fn_set_size)
|
||||
}
|
||||
|
||||
/// Returns a simple bubble chart implemented in vanilla JS. Uses an object to implement the
|
||||
/// visualization logic.
|
||||
pub fn object_sample_js_bubble_chart() -> JsRenderer {
|
||||
let fn_prototype = r#"
|
||||
(() => {
|
||||
class BubbleVisualisation {
|
||||
onDataReceived(root, data) {
|
||||
const xmlns = "http://www.w3.org/2000/svg";
|
||||
while (root.firstChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
const svgElem = document.createElementNS(xmlns, "svg");
|
||||
svgElem.setAttributeNS(null, "id" , "vis-svg");
|
||||
svgElem.setAttributeNS(null, "viewBox", "0 0 " + 100 + " " + 100);
|
||||
svgElem.setAttributeNS(null, "width" , 100);
|
||||
svgElem.setAttributeNS(null, "height" , 100);
|
||||
root.appendChild(svgElem);
|
||||
|
||||
data.forEach(data => {
|
||||
const bubble = document.createElementNS(xmlns,"circle");
|
||||
bubble.setAttributeNS(null,"stroke", "black");
|
||||
bubble.setAttributeNS(null,"fill" , "red");
|
||||
bubble.setAttributeNS(null,"r" , data[2]);
|
||||
bubble.setAttributeNS(null,"cx" , data[0]);
|
||||
bubble.setAttributeNS(null,"cy" , data[1]);
|
||||
svgElem.appendChild(bubble);
|
||||
});
|
||||
}
|
||||
|
||||
setSize(root, size) {
|
||||
const width = size[0];
|
||||
const height = size[1];
|
||||
const svgElem = root.firstChild;
|
||||
svgElem.setAttributeNS(null, "viewBox", "0 0 " + width + " " + height);
|
||||
svgElem.setAttributeNS(null, "width" , width);
|
||||
svgElem.setAttributeNS(null, "height" , height);
|
||||
}
|
||||
}
|
||||
|
||||
return new BubbleVisualisation();
|
||||
})()
|
||||
"#;
|
||||
JsRenderer::from_object(fn_prototype).unwrap()
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Returns a simple bubble chart implemented in vanilla JS. uses single functions to implement the
|
||||
/// visualization.
|
||||
pub fn constructor_sample_js_bubble_chart() -> JsRenderer {
|
||||
let fn_constructor = r#"
|
||||
class BubbleVisualisation {
|
||||
onDataReceived(root, data) {
|
||||
const xmlns = "http://www.w3.org/2000/svg";
|
||||
while (root.firstChild) {
|
||||
root.removeChild(root.lastChild);
|
||||
}
|
||||
|
||||
const svgElem = document.createElementNS(xmlns, "svg");
|
||||
svgElem.setAttributeNS(null, "id" , "vis-svg");
|
||||
svgElem.setAttributeNS(null, "viewBox", "0 0 " + 100 + " " + 100);
|
||||
svgElem.setAttributeNS(null, "width" , 100);
|
||||
svgElem.setAttributeNS(null, "height" , 100);
|
||||
root.appendChild(svgElem);
|
||||
|
||||
data.forEach(data => {
|
||||
const bubble = document.createElementNS(xmlns,"circle");
|
||||
bubble.setAttributeNS(null,"stroke", "black");
|
||||
bubble.setAttributeNS(null,"fill" , "red");
|
||||
bubble.setAttributeNS(null,"r" , data[2]);
|
||||
bubble.setAttributeNS(null,"cx" , data[0]);
|
||||
bubble.setAttributeNS(null,"cy" , data[1]);
|
||||
svgElem.appendChild(bubble);
|
||||
});
|
||||
}
|
||||
|
||||
setSize(root, size) {
|
||||
const width = size[0];
|
||||
const height = size[1];
|
||||
const svgElem = root.firstChild;
|
||||
svgElem.setAttributeNS(null, "viewBox", "0 0 " + width + " " + height);
|
||||
svgElem.setAttributeNS(null, "width" , width);
|
||||
svgElem.setAttributeNS(null, "height" , height);
|
||||
}
|
||||
}
|
||||
|
||||
return new BubbleVisualisation();
|
||||
"#;
|
||||
JsRenderer::from_constructor(fn_constructor).unwrap()
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
//! Examples of defining visualisation in Rust using web_sys or ensogl.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::component::visualization::*;
|
||||
|
||||
use ensogl::data::color::Rgba;
|
||||
use ensogl::display::layout::alignment;
|
||||
use ensogl::display::scene::Scene;
|
||||
use ensogl::display;
|
||||
use ensogl::gui::component;
|
||||
|
||||
|
||||
|
||||
// ==========================
|
||||
// === Native BubbleChart ===
|
||||
// ==========================
|
||||
|
||||
/// Bubble shape definition.
|
||||
pub mod shape {
|
||||
use super::*;
|
||||
use ensogl::display::shape::*;
|
||||
use ensogl::display::scene::Scene;
|
||||
use ensogl::display::Sprite;
|
||||
use ensogl::display::Buffer;
|
||||
use ensogl::display::Attribute;
|
||||
|
||||
ensogl::define_shape_system! {
|
||||
(position:Vector2<f32>,radius:f32) {
|
||||
let node = Circle(radius);
|
||||
let node = node.fill(Rgba::new(0.17,0.46,0.15,1.0));
|
||||
let node = node.translate(("input_position.x","input_position.y"));
|
||||
node.into()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample implementation of a Bubble Chart using the ensogl shape system.
|
||||
#[derive(Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct BubbleChart {
|
||||
pub display_object : display::object::Instance,
|
||||
pub scene : Scene,
|
||||
frp : DataRendererFrp,
|
||||
views : RefCell<Vec<component::ShapeView<shape::Shape>>>,
|
||||
logger : Logger,
|
||||
size : Cell<Vector2<f32>>,
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
impl BubbleChart {
|
||||
pub fn new(scene:&Scene) -> Self {
|
||||
let logger = Logger::new("bubble");
|
||||
let display_object = display::object::Instance::new(&logger);
|
||||
let views = RefCell::new(vec![]);
|
||||
let frp = default();
|
||||
let size = Cell::new(Vector2::zero());
|
||||
let scene = scene.clone_ref();
|
||||
|
||||
BubbleChart { display_object,views,logger,frp,size,scene }
|
||||
}
|
||||
}
|
||||
|
||||
impl DataRenderer for BubbleChart {
|
||||
|
||||
fn receive_data(&self, data:Data) -> Result<(),DataError> {
|
||||
let data_inner: Rc<Vec<Vector3<f32>>> = data.as_binary()?;
|
||||
|
||||
// Avoid re-creating views, if we have already created some before.
|
||||
let mut views = self.views.borrow_mut();
|
||||
views.resize_with(data_inner.len(),|| component::ShapeView::new(&self.logger,&self.scene));
|
||||
|
||||
// TODO[mm] this is somewhat inefficient, as the canvas for each bubble is too large.
|
||||
// But this ensures that we can get a cropped view area and avoids an issue with the data
|
||||
// and position not matching up.
|
||||
views.iter().zip(data_inner.iter()).for_each(|(view,item)| {
|
||||
|
||||
let shape_system = self.scene.shapes.shape_system(PhantomData::<shape::Shape>);
|
||||
shape_system.shape_system.set_alignment(
|
||||
alignment::HorizontalAlignment::Left, alignment::VerticalAlignment::Bottom);
|
||||
|
||||
view.display_object.set_parent(&self.display_object);
|
||||
view.shape.sprite.size().set(self.size.get());
|
||||
view.shape.radius.set(item.z);
|
||||
view.shape.position.set(Vector2::new(item.x,item.y));
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_size(&self, size:Vector2<f32>) {
|
||||
self.size.set(size);
|
||||
}
|
||||
|
||||
fn frp(&self) -> &DataRendererFrp {
|
||||
&self.frp
|
||||
}
|
||||
}
|
||||
|
||||
impl display::Object for BubbleChart {
|
||||
fn display_object(&self) -> &display::object::Instance {
|
||||
&self.display_object.display_object()
|
||||
}
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
//! This module contains functionality that allows the usage of JavaScript to define visualizations.
|
||||
//!
|
||||
//! The `JsRenderer` defines a generic way to wrap JS function calls and allow interaction with
|
||||
//! JS code and the visualisation system.
|
||||
//!
|
||||
//! There are at the moment three way to generate a `JsRenderer`:
|
||||
//! 1. `JsRenderer::from_functions` where the bodies of the required functions are provided as
|
||||
//! source code.
|
||||
//! 2. `JsRenderer::from_object` where the a piece of JS code is provided that must evaluate to an
|
||||
//! object that has the required methods that will be called at runtime.
|
||||
//! 3. `JsRenderer::from_constructor`where the body of a constructor function needs to be
|
||||
//! provided. The returned object needs to fulfill the same specification as in (2).
|
||||
//!
|
||||
//! Right now the only functions required on the wrapped object are
|
||||
//! * `onDataReceived(root, data)`, which receives the html element that the visualisation should be
|
||||
//! appended on, as well as the data that should be rendered.
|
||||
//! * `setSize(root, size)`, which receives the node that the visualisation should be appended on,
|
||||
//! as well as the intended size.
|
||||
//!
|
||||
//! TODO: refine spec and add functions as needed, e.g., init, callback hooks or type indicators.
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
use crate::component::visualization::Data;
|
||||
use crate::component::visualization::DataError;
|
||||
use crate::component::visualization::DataRenderer;
|
||||
use crate::component::visualization::DataRendererFrp;
|
||||
|
||||
use ensogl::display::DomScene;
|
||||
use ensogl::display::DomSymbol;
|
||||
use ensogl::display;
|
||||
use ensogl::system::web::JsValue;
|
||||
use ensogl::system::web;
|
||||
use js_sys;
|
||||
|
||||
|
||||
|
||||
// ==============
|
||||
// === Errors ===
|
||||
// ==============
|
||||
|
||||
/// Errors that can occur when transforming JS source to a visualization.
|
||||
#[derive(Clone,Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum JsVisualisationError {
|
||||
NotAnObject { inner:JsValue },
|
||||
NotAFunction { inner:JsValue },
|
||||
/// An unknown error occurred on the JS side. Inspect the content for more information. .
|
||||
Unknown { inner:JsValue }
|
||||
}
|
||||
|
||||
impl From<JsValue> for JsVisualisationError {
|
||||
fn from(value:JsValue) -> Self {
|
||||
// TODO add differentiation if we encounter specific errors and return new variants.
|
||||
JsVisualisationError::Unknown {inner:value}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ==================
|
||||
// === JsRenderer ===
|
||||
// ==================
|
||||
|
||||
/// `JsVisualizationGeneric` allows the use of arbitrary javascript to create visualisations. It
|
||||
/// takes function definitions as strings and proved those functions with data.
|
||||
#[derive(Clone,Debug)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct JsRenderer {
|
||||
pub root_node : DomSymbol,
|
||||
pub logger : Logger,
|
||||
on_data_received : js_sys::Function,
|
||||
set_size : js_sys::Function,
|
||||
frp : DataRendererFrp,
|
||||
}
|
||||
|
||||
impl JsRenderer {
|
||||
/// Constructor from single functions.
|
||||
///
|
||||
/// `fn_set_data` and `fn_set_size` need to be strings that contain valid JavaScript code. This
|
||||
/// code will be executed as the function body of the respective functions.
|
||||
///
|
||||
/// `fn_set_data` will be called with two arguments: the first argument (`root) )will be the
|
||||
/// root node that the visualisation should use to build its output, the second argument
|
||||
/// (`data`)will be the data that it should visualise.
|
||||
///
|
||||
/// `fn_set_size` will be called with a tuple of floating point values indicating the desired
|
||||
/// width and height. This can be used by the visualisation to ensure proper scaling.
|
||||
///
|
||||
/// For a full example see
|
||||
/// `crate::component::visualization::renderer::example::function_sample_js_bubble_chart`
|
||||
pub fn from_functions(fn_set_data:&str, fn_set_size:&str) -> Self {
|
||||
let set_data = js_sys::Function::new_no_args(fn_set_data);
|
||||
let set_size = js_sys::Function::new_no_args(fn_set_size);
|
||||
|
||||
let logger = Logger::new("JsRendererGeneric");
|
||||
let frp = default();
|
||||
let div = web::create_div();
|
||||
let root_node = DomSymbol::new(&div);
|
||||
root_node.dom().set_attribute("id","vis").unwrap();
|
||||
|
||||
JsRenderer { on_data_received: set_data,set_size,root_node,frp,logger }
|
||||
}
|
||||
|
||||
/// Internal helper that tries to convert a JS object into a `JsRenderer`.
|
||||
fn from_object_js(object:js_sys::Object) -> Result<JsRenderer,JsVisualisationError> {
|
||||
let set_data = js_sys::Reflect::get(&object,&"onDataReceived".into())?;
|
||||
let set_size = js_sys::Reflect::get(&object,&"setSize".into())?;
|
||||
if !set_data.is_function() {
|
||||
return Err(JsVisualisationError::NotAFunction { inner:set_data })
|
||||
}
|
||||
if !set_size.is_function() {
|
||||
return Err(JsVisualisationError::NotAFunction { inner:set_size })
|
||||
}
|
||||
let set_data:js_sys::Function = set_data.into();
|
||||
let set_size:js_sys::Function = set_size.into();
|
||||
|
||||
let logger = Logger::new("JsRenderer");
|
||||
let frp = default();
|
||||
let div = web::create_div();
|
||||
let root_node = DomSymbol::new(&div);
|
||||
root_node.dom().set_attribute("id","vis")?;
|
||||
|
||||
Ok(JsRenderer { on_data_received: set_data,set_size,root_node,frp,logger })
|
||||
}
|
||||
|
||||
/// Constructor from a source that evaluates to an object with specific methods.
|
||||
///
|
||||
/// Example:
|
||||
/// --------
|
||||
///
|
||||
/// ```no_run
|
||||
/// use graph_editor::component::visualization::JsRenderer;
|
||||
///
|
||||
/// let renderer = JsRenderer::from_object("function() {
|
||||
/// class Visualization {
|
||||
/// onDataReceived(root, data) {};
|
||||
/// setSize(root, size) {};
|
||||
/// }
|
||||
/// return new Visualisation();
|
||||
/// }()").unwrap();
|
||||
///
|
||||
/// ```
|
||||
///
|
||||
/// For a full example see
|
||||
/// `crate::component::visualization::renderer::example::object_sample_js_bubble_chart`
|
||||
pub fn from_object(source: &str) -> Result<JsRenderer,JsVisualisationError> {
|
||||
let object = js_sys::eval(source)?;
|
||||
if !object.is_object() {
|
||||
return Err(JsVisualisationError::NotAnObject { inner:object } )
|
||||
}
|
||||
Self::from_object_js(object.into())
|
||||
}
|
||||
|
||||
/// Constructor from function body that returns a object with specific functions.
|
||||
///
|
||||
/// Example:
|
||||
/// --------
|
||||
///
|
||||
/// ```no_run
|
||||
/// use graph_editor::component::visualization::JsRenderer;
|
||||
///
|
||||
/// let renderer = JsRenderer::from_constructor("
|
||||
/// class Visualization {
|
||||
/// onDataReceived(root, data) {};
|
||||
/// setSize(root, size) {};
|
||||
/// }
|
||||
/// return new Visualisation();
|
||||
/// ").unwrap();
|
||||
///
|
||||
/// ```
|
||||
/// For a full example see
|
||||
/// `crate::component::visualization::renderer::example::constructor_sample_js_bubble_chart`
|
||||
pub fn from_constructor(source:&str) -> Result<JsRenderer,JsVisualisationError> {
|
||||
let context = JsValue::NULL;
|
||||
let constructor = js_sys::Function::new_no_args(source);
|
||||
let object = constructor.call0(&context)?;
|
||||
if !object.is_object() {
|
||||
return Err(JsVisualisationError::NotAnObject { inner:object } )
|
||||
}
|
||||
Self::from_object_js(object.into())
|
||||
}
|
||||
|
||||
/// Hooks the root node into the given scene.
|
||||
///
|
||||
/// MUST be called to make this visualisation visible.
|
||||
// TODO[mm] find a better mechanism to ensure this. Probably through the registry later on.
|
||||
pub fn set_dom_layer(&self, scene:&DomScene) {
|
||||
scene.manage(&self.root_node);
|
||||
}
|
||||
}
|
||||
|
||||
impl DataRenderer for JsRenderer {
|
||||
|
||||
fn receive_data(&self, data:Data) -> Result<(),DataError> {
|
||||
let context = JsValue::NULL;
|
||||
let data_json = data.as_json()?;
|
||||
let data_js = match JsValue::from_serde(&data_json) {
|
||||
Ok(value) => value,
|
||||
Err(_) => return Err(DataError::InvalidDataType),
|
||||
};
|
||||
if let Err(error) = self.on_data_received.call2(&context, &self.root_node.dom(), &data_js) {
|
||||
self.logger.warning(
|
||||
|| format!("Failed to set data in {:?} with error: {:?}",self,error));
|
||||
return Err(DataError::InternalComputationError)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_size(&self, size:Vector2<f32>) {
|
||||
let context = JsValue::NULL;
|
||||
let data_json = JsValue::from_serde(&size).unwrap();
|
||||
if let Err(error) = self.set_size.call2(&context, &self.root_node.dom(), &data_json) {
|
||||
self.logger.warning(
|
||||
|| format!("Failed to set size in {:?} with error: {:?}", self, error));
|
||||
}
|
||||
}
|
||||
|
||||
fn frp(&self) -> &DataRendererFrp {
|
||||
&self.frp
|
||||
}
|
||||
}
|
||||
|
||||
impl display::Object for JsRenderer {
|
||||
fn display_object(&self) -> &display::object::Instance {
|
||||
&self.root_node.display_object()
|
||||
}
|
||||
}
|
@ -56,9 +56,9 @@ use ensogl::display::Scene;
|
||||
use crate::component::node::port::Expression;
|
||||
use crate::component::visualization::Visualization;
|
||||
use crate::component::visualization;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::component::visualization::example::js::constructor_sample_js_bubble_chart;
|
||||
use crate::component::visualization::MockDataGenerator3D;
|
||||
use crate::component::visualization::example::native;
|
||||
|
||||
|
||||
// =====================
|
||||
@ -241,20 +241,24 @@ ensogl::def_command_api! { Commands
|
||||
toggle_visualization_visibility,
|
||||
/// Set the data for the selected nodes. // TODO only has dummy functionality at the moment.
|
||||
debug_set_data_for_selected_node,
|
||||
/// Cycle the visualization for the selected nodes. TODO only has dummy functionality at the moment.
|
||||
debug_cycle_visualisation_for_selected_node,
|
||||
}
|
||||
|
||||
impl Commands {
|
||||
pub fn new(network:&frp::Network) -> Self {
|
||||
frp::extend! { network
|
||||
def add_node = source();
|
||||
def add_node_at_cursor = source();
|
||||
def remove_selected_nodes = source();
|
||||
def remove_all_nodes = source();
|
||||
def toggle_visualization_visibility = source();
|
||||
def debug_set_data_for_selected_node = source();
|
||||
def add_node = source();
|
||||
def add_node_at_cursor = source();
|
||||
def remove_selected_nodes = source();
|
||||
def remove_all_nodes = source();
|
||||
def toggle_visualization_visibility = source();
|
||||
def debug_set_data_for_selected_node = source();
|
||||
def debug_cycle_visualisation_for_selected_node = source();
|
||||
}
|
||||
Self {add_node,add_node_at_cursor,remove_selected_nodes,remove_all_nodes
|
||||
,toggle_visualization_visibility,debug_set_data_for_selected_node}
|
||||
,toggle_visualization_visibility,debug_set_data_for_selected_node
|
||||
,debug_cycle_visualisation_for_selected_node}
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,6 +285,8 @@ pub struct FrpInputs {
|
||||
pub set_node_position : frp::Source<(NodeId,Position)>,
|
||||
pub set_visualization_data : frp::Source<NodeId>,
|
||||
pub translate_selected_nodes : frp::Source<Position>,
|
||||
pub cycle_visualization : frp::Source<NodeId>,
|
||||
pub set_visualization : frp::Source<(NodeId,Option<Visualization>)>,
|
||||
}
|
||||
|
||||
impl FrpInputs {
|
||||
@ -299,12 +305,14 @@ impl FrpInputs {
|
||||
def set_node_position = source();
|
||||
def set_visualization_data = source();
|
||||
def translate_selected_nodes = source();
|
||||
def cycle_visualization = source();
|
||||
def set_visualization = source();
|
||||
}
|
||||
let commands = Commands::new(&network);
|
||||
Self {commands,remove_edge,press_node_port,set_visualization_data
|
||||
,connect_detached_edges_to_node,connect_edge_source,connect_edge_target
|
||||
,set_node_position,select_node,translate_selected_nodes,set_node_expression
|
||||
,connect_nodes,deselect_all_nodes}
|
||||
,connect_nodes,deselect_all_nodes,cycle_visualization,set_visualization}
|
||||
}
|
||||
}
|
||||
|
||||
@ -721,14 +729,11 @@ impl GraphEditorModelWithNetwork {
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
let dummy_content = visualization::default_content();
|
||||
let chart = constructor_sample_js_bubble_chart();
|
||||
let dom_layer = model.scene.dom.layers.front.clone_ref();
|
||||
dom_layer.manage(&dummy_content);
|
||||
chart.set_dom_layer(&dom_layer);
|
||||
|
||||
let vis:Visualization = dummy_content.into();
|
||||
let vis = Visualization::new(chart);
|
||||
node.view.frp.set_visualization.emit(Some(vis));
|
||||
|
||||
|
||||
@ -1017,6 +1022,7 @@ impl application::shortcut::DefaultShortcutProvider for GraphEditor {
|
||||
, Self::self_shortcut(&[Key::Backspace] , "remove_selected_nodes")
|
||||
, Self::self_shortcut(&[Key::Character(" ".into())] , "toggle_visualization_visibility")
|
||||
, Self::self_shortcut(&[Key::Character("d".into())] , "debug_set_data_for_selected_node")
|
||||
, Self::self_shortcut(&[Key::Character("f".into())] , "debug_cycle_visualisation_for_selected_node")
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -1200,31 +1206,58 @@ impl application::View for GraphEditor {
|
||||
})
|
||||
}));
|
||||
|
||||
// === Vis Cycling ===
|
||||
def _cycle_vis= inputs.debug_cycle_visualisation_for_selected_node.map(f!((inputs,nodes)(_) {
|
||||
nodes.selected.for_each(|node| inputs.cycle_visualization.emit(node));
|
||||
}));
|
||||
|
||||
// === Vis Set ===
|
||||
def _update_vis_data = inputs.set_visualization.map(f!((nodes)((node_id,vis)) {
|
||||
if let Some(node) = nodes.get_cloned_ref(node_id) {
|
||||
node.view.visualization_container.frp.set_visualization.emit(vis)
|
||||
}
|
||||
}));
|
||||
|
||||
// === Vis Update Data ===
|
||||
|
||||
// TODO remove this once real data is available.
|
||||
let dummy_counter = Rc::new(Cell::new(1.0_f32));
|
||||
def _update_vis_data = inputs.debug_set_data_for_selected_node.map(f!((nodes)(_) {
|
||||
let dc = dummy_counter.get();
|
||||
dummy_counter.set(dc + 0.1);
|
||||
let content = Rc::new(json!(format!("{}", 20.0 + 10.0 * dummy_counter.get().sin())));
|
||||
let dummy_data = Some(visualization::Data::JSON { content });
|
||||
let dummy_switch = Rc::new(Cell::new(false));
|
||||
let sample_data_generator = MockDataGenerator3D::default();
|
||||
def _set_dumy_data = inputs.debug_set_data_for_selected_node.map(f!((nodes)(_) {
|
||||
nodes.selected.for_each(|node_id| {
|
||||
if let Some(node) = nodes.get_cloned_ref(node_id) {
|
||||
node.view.visualization_container.frp.set_data.emit(&dummy_data);
|
||||
let data = Rc::new(sample_data_generator.generate_data());
|
||||
let content = Rc::new(serde_json::to_value(data).unwrap());
|
||||
let data = visualization::Data::JSON{ content };
|
||||
if let Some(node) = nodes.get_cloned(node_id) {
|
||||
node.view.visualization_container.frp.set_data.emit(Some(data));
|
||||
}
|
||||
})
|
||||
}));
|
||||
|
||||
def _set_dumy_data = inputs.cycle_visualization.map(f!((scene,nodes)(node_id) {
|
||||
// TODO remove dummy cycling once we have the visualization registry.
|
||||
let dc = dummy_switch.get();
|
||||
dummy_switch.set(!dc);
|
||||
let vis = if dc {
|
||||
Visualization::new(native::BubbleChart::new(&scene))
|
||||
} else {
|
||||
let chart = constructor_sample_js_bubble_chart();
|
||||
let dom_layer = scene.dom.layers.front.clone_ref();
|
||||
chart.set_dom_layer(&dom_layer);
|
||||
Visualization::new(chart)
|
||||
};
|
||||
if let Some(node) = nodes.get_cloned_ref(node_id) {
|
||||
node.view.visualization_container.frp.set_visualization.emit(Some(vis));
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
// === Toggle Visualization Visibility ===
|
||||
|
||||
def _toggle_selected = inputs.toggle_visualization_visibility.map(f!((nodes)(_) {
|
||||
nodes.selected.for_each(|node_id| {
|
||||
if let Some(node) = nodes.get_cloned_ref(node_id) {
|
||||
node.view.visualization_container.toggle_visibility();
|
||||
node.view.visualization_container.frp.toggle_visibility.emit(());
|
||||
}
|
||||
});
|
||||
}));
|
||||
|
Loading…
Reference in New Issue
Block a user