These are some of the classes in my JavaScript application:
myApp.mode.model Handles the state
myApp.mode.controller Instantiates and updates components based on the model
myApp.data.dataManager Does operations on the dataSource
myApp.data.dataSource A big singleton with structured data
myApp.chart.grid A grid component
myApp.chart.scatter A scatter gram renderer
myApp.chart.tooltip A tooltip renderer
The interaction between these components are sketched below: (Sorry for the bad illus. skills ... ;) )
What I'm looking for is a clean way to pass the necessary arguments (dependency management) to the sub-components of the Visualization controller:
Let's say the user changes an indicator in the Visualization display. The model asks the data manager to load the necessary data.
When the data is loaded, the Visualization controller learns about the model change and should update its respective components: Grid, Scatter, Tooltips etc.
The Grid needs to know things such as xMin, xMax, width, height ...
The "Scatter renderer" also needs xMin, xMax, width, height. In addition it needs access to the big data singleton and it needs to find out what parts of the data to draw.
Three questions:
How do I pass the dataSource to the Scatter renderer? Do I declare it or pass it?
Many components are interested in the available data to draw. The data manager could answer this query. Should the "dataAvailability" be passed to the Scatter renderer or should it instead have the whole data manager as a dependency?
Looking at the schematic drawing, how would you lay out the objects so that a new state (new indicators, year, selection, width, height) would easily propagate down through all the sub-objects?
Thanks :)
You might want to look at AngularJS as it has DI capabilities (to support easier testing).
also check out https://github.com/briancavalier/wire for dependency injection
What you're talking about is more a question of MVC architecture. You don't have tens of instances of objects in different scopes to require a DI.
Looking at the diagram you drew, I have a strong feeling that in place of model there should be a controller. It's controller's duty to handle user's interactions. Your controller might look like this:
var Controller = {
init: function {
this.dataManager = ...;
this.grid = ...; // grid is some sort of widget
this.scatter = ...; // scatter is some sort of widget
// Setup widgets
this.scatter.x = 100;
this.scatter.y = 100;
},
bind: function {
// Bind to your indicator controls
$('#indicator').change(_.bind(this.update, this)); // mind the scope
},
update: function () {
var indicatorValue = $('#indicator').val();
var data = this.dataManager.getData(indicatorValue);
this.grid.setData(data);
this.scatter.setData(data);
this.scatter.setRegion(300, 300);
}
}
Controller.init();
Controller.bind();
That's it. Pass ready data to Grid and Scatter, don't pass data source and data query parameters to them.
Related
Disclaimer: I am really not well experienced with neither svelte, nor D3, nor general JavaScript patterns. But I really like it, so I really want to learn it and already invested quite some time. Still, this feels like a super basic question that annoys me a lot. I hope it is not too confusing and someone might has an idea.
It is basically about how to setup a simple graph (let it be a bar chart) in an efficient, reproducible and "best-practice" way. I guess my main concern is on how to pass around the data and use it for different tasks. E.g. I think it might be a good idea to separate out the construction of the scales (using d3) in a separate component. However, this component needs access to the data (and probably also access to the, in the best case resposive width, of the chart-container).
However, also the bars, which are in another component, need access to the data in order to know how do draw the rectangles.
A general misunderstanding (i guess that is the right word) I have with JavaScript is that I do not understand how to fetch data asynchronously (using e.g. the browsers fetchor D3's csvmethod). I simply can not fetch the data and then pass it as prop to another component. Because what I would be passing would be a promise...
So I have this very basic REPL that kind of shows a bit this know I have in my head: https://svelte.dev/repl/398f4c21b7a9409a9811fd8e38703a36?version=3.44.1
It looks like this. In the App.html I fetch the data that I want to use for multiple purposes. However I cannot "get it out" of that component.
<script>
import Chart from "./Chart.svelte"
const url = "https://api.github.com/search/repositories?q=stars:>100000";
async function getData(){
let response = await fetch(url)
let data = await response.json()
console.log(data)
}
//async function getDataLocal(){
// let data = await d3.csv(<path_to_data>)
// return await data
// }
let data = await getData()
</script>
<Chart {data}>Do Something with the data. Make the chart, build the scales, ....</Chart>
So the main questions are:
Are there any ressources on how to learn building sustainable graphics with remote data, svelte and a bit of D3. I already watched many many youtube videos and I guess I will rewatch the one from Matthias Stahl;)
Is it a good idea to use stores in such a case to store the data
And a little more specific: As the data is (probably) fixed, however the dimension arent't: What is a good way/place to let the app know to recalculate the scales etc.
There are 3 separate concerns here:
fetching, storing and retrieving data (aka the data source layer)
manipulating/transforming data (aka the business logic layer)
displaying data (aka the presentation layer)
I will leave the last part aside as it solely concerns D3 (if that is your visualization library of choice) and there are plenty of resources available online on this topic, and I will instead focus on what seems to be the heart of your question, i.e. how to fetch data in Svelte, where to store it, how to pass it around to components, and how to manipulate the data.
1. Asynchronous queries in Svelte
Your first inquiry is about how to deal with asynchronous requests. You cannot use await statements at the root level of the <script> section of a Svelte file, meaning the following reactive statement would generate an error:
// will fail
$: data = await getData(url)
However, you can call an asynchronous function that will handle the assignment. Reactivity will still work and your component will re-render when the url is changed and the new data retrieved:
// will work
$: updateData(url)
async function updateData(url) {
data = await getData(url)
}
Here is a working example based on the REPL in your question
2. Using stores
As you could see from the above example, you had to pass the data to your <Header> and <Chart> components for it to be used in either:
<Header {data}>GitHub Lookup</Header>
<Chart {data}/>
But what if you want to use your Chart somewhere else in your application? What if you have another component that wants to make use of the same data?
Obviously you do not want to fetch the same data over & over (unless the request itself has changed). You also want to avoid passing the data around as a prop everywhere in your app. You will want to make the data available only to these components that will use it.
This is where stores come in handy. Stores can be subscribed to by any component. A writable store will allow its contents to be updated, while a readable store will be -as the name implies- read-only.
A store need not be complex. The following is a very basic writable store:
import { writable } from 'svelte/store'
export const githubStore = writable(null) // initialized with a null value
All you have to do then is interact with your store.
Updating the store in your App component:
import { githubStore as data } from './githubStore.js' // import the store we defined above under the name 'data'
.
.
.
async function updateData(url) {
$data = await getData(url) // using the $ shorthand to access the store (assigning a new value will update the store content)
}
Using (i.e. subscribing to) the store in your components:
import { githubStore as data } from './githubStore.js' // import the store we defined above under the name 'data'
.
.
.
// using the $ shorthand to access the store
{#each $data.items as item (item.id)}
<li><a href={item.html_url}>{item.full_name}</a> [{item.stargazers_count}⭐]</li>
{/each}
Read here for details on using the $ reactive syntax with stores
Now that your child components are subscribing to the store where you stored your data, you do not need to pass that data as a prop any more:
<Header>GitHub Lookup</Header>
<Chart />
Here is an updated version of the REPL above, using stores
3. Further considerations
When you want to start manipulating or transforming data that has been put into a store, derived stores come in handy. When the data in your original store is updated, the derived store will automatically update itself based on the changes to the original.
You can also build on the provided readable/writable stores by adding your own functionality and custom methods. These are slightly more advanced topics but would come in handy where data manipulation is concerned.
Finally, D3 will provide its own data manipulation methods, so it will be up to you to decide how much manipulation you handle directly in Svelte, and how much you delegate to D3. I would probably leave everything connected to visualization (scaling, zooming, etc.) on the D3 side, and have the actual pre-visualization manipulation of data (i.e. the business logic) on the Svelte side (or better yet, directly on the back-end if you have access to that!).
I've tried to prepare data from an OData source to show it in a bar graph in my fiori app. For this, I setup the OData model in the manifest.json. A test with a list, simply using
items="{path : 'modelname>/dataset'}
works fine and shows the content.
To prepare data for a diagram (VizFrame), I used the onInit() function in the controller of the view (mvc:XMLView). The data preparation is similar to the one discussed in question.
At first I obtain the ODataModel:
var oODataModel = this.getOwnerComponent().getModel("modelname");
Next I do the binding:
var oBindings = oODataModel.bindList("/dataset");
Unfortunately, the oBindings().getContexts() array is always empty, and also oBindings.getLength() is zero. As a consequence, the VizFrame shows only "No Data".
May it be that the data model is not fully loaded during the onInit() function, or do I misunderstand the way to access data?
Thanks in advance
Update
I temporary solved the problem by using the automatically created bind from the view displaying the data as list. I grep the "dataReceived" event from the binding getView().byId("myList").getBindings("items") and do my calculation there. The model for the diagram (since it is used in a different view) is created in the Component.js, and registered in the Core sap.ui.getCore().setModel("graphModel").
I think this solution is dirty, because the graph data depends on the list data from a different view, which causes problems, e.g. when you use a growing list (because the data in the binding gets updated and a different range is selected from the odata model).
Any suggestions, how I can get the odata model entries without depending on a different list?
The following image outlines the lifecycle of your UI5 application.
Important are the steps which are highlighted with a red circle. Basically, in your onInit you don't have full access to your model via this.getView().getModel().
That's probably why you tried using this.getOwnerComponent().getModel(). This gives you access to the model, but it's not bound to the view yet so you don't get any contexts.
Similarly metadataLoaded() returns a Promise that is fullfilled a little too early: Right after the metadata has been loaded, which might be before any view binding has been done.
What I usually do is
use onBeforeRendering
This is the lifecycle hook that gets called right after onInit. The view and its models exist, but they are not yet shown to the user. Good possibility to do stuff with your model.
use onRouteMatched
This is not really a lifecycle hook but an event handler which can be bound to the router object of your app. Since you define the event handler in your onInit it will be called later (but not too late) and you can then do your desired stuff. This obviously works only if you've set up routing.
You'll have to wait until the models metadata has been loaded. Try this:
onInit: function() {
var oBindings;
var oODataModel = this.getComponent().getModel("modelname");
oODataModel.metadataLoaded().then(function() {
oBindings = oODataModel.bindList("/dataset");
}.bind(this));
},
May it be that the data model is not fully loaded during the onInit()
function, or do I misunderstand the way to access data?
You could test if your model is fully loaded by console log it before you do the list binding
console.log(oODataModel);
var oBindings = oODataModel.bindList("/dataset");
If your model contains no data, then that's the problem.
My basic misunderstanding was to force the use of the bindings. This seems to work only with UI elements, which organize the data handling. I switched to
oODataModel.read("/dataset", {success: function(oEvent) {
// do all my calculations on the oEvent.results array
// write result into graphModel
}
});
This whole calculation is in a function attached to the requestSent event of the graphModel, which is set as model for the VizFrame in the onBeforeRendering part of the view/controller.
I know the MVC concept in ExtJs just very briefly. Could you please help me to fill up the knowledge gaps please? I only know how to create a single view this way...
Ext.define('My.controller.Contacts', {
extend: 'Ext.app.Controller',
stores: ['Contacts'],
views: ['ContactsGrid'],
refs: [{ref: 'grid', selector: '', xtype: 'contacts-grid', autoCreate: true}],
getGrid: function() {
var g = this.getGrid();
return g;
}
});
this.getGrid() seems to give you the same grid view. But what if:
I want to create multiple instances of grid views dynamically, how? and where do I store them by convention?
For each view I have created, I want to give it a config object, like how I do Ext.create(somecontrol, config); but this case in MVC they are in refs? Where do I insert this config object for every view instance I create?
I have a store Contacts, what is the relationship between all these views and the store? One each, or all sharing one store?
Thank you very much.
For all of these the answer is going to be "it depends",
I would create dynamic views in the view definition if it's something that you read at startup, otherwise if you are clicking on a button and adding a view element, you could have everything in the controller, or you could have the controller call a method on the view that actually creates the view. I'd probably go with the latter but it depends on how you want to encapsulate your view logic.
As far as 'storing' these views is concerned, the convention is to give your views an unique id so you can reference it later, similar to DOM lookups. But you can also store references to a component in a variable obviously. It really depends on what you are doing. If a controller is constructing a bunch of dynamic elements, it might make sense to just hold on to references to those elements in the controller.
Views are typically defined in their own files under the MVC approach, essentially this is an Ext.define block with a configuration in it. There are a few sample MVC applications on the Sencha site, I recommend looking them over.
This really depends on what your doing. If you have multiple Contacts views, it might make sense to have a single store be referenced by multiple views, but in general Stores represent a collection of specific data. Like Books, or Contacts, or Employees. So if a view needs to show Books and Employees, it would make sense to references those stores in the view.
I think the crux of what you're asking is where do I encapsulate the logic for dynamic views. I would recommend creating reusable view components that encapsulate your display logic and have the controller create these components and give them the data to populate themselves. This gives you a nice flexible separation of concerns.
If you're just getting started with ExtJS and their implementation of MVC I highly recommend playing around with Sencha Architect. I wouldn't build a real project with it, but it's great for throwing together quick little demo apps, and it generates an MVC structure for you. Take a look at what it gives you and take a look at the demo applications on the Sencha site.
Here's some code to explain as well. The button text config is set to 'ZZZ' in my example. The component being instantiated from view 'view-XXX' (xtype) extends 'Ext.window.Window' in my example. If a component is being instantiated within a view, the Ext.create is implicit when using lazy instantiation via the xtype config. If a component is being instantiated within the controller, I use the standard notation Ext.create. If you want to give an id, use the Ext.id() to dynamically generate an id for the component. I won't repeat #James' answer for #2 and #3.
Try this:
Ext.define('App.controller.ControllerExample', {
extend : 'Ext.app.Controller',
stores: ['XXXs', 'YYYs'],
models: ['XXX', 'YYY'],
refs : [{
ref: 'viewport',
selector: 'view-viewport'
}, {
ref: 'XXX',
selector: 'view-XXX-window'
}, {
ref: 'YYY',
selector: 'view-YYY'
}],
init : function() {
this.control({
'view-XXX-window': {
minimize: function(win, obj) {
this.getXXX().hide();
},
close: function(panel, eOpt) {
this.getXXX().hide();
}
},
'view-XXX-window button[text=ZZZ]': {
click: function() {
var XXX = this.getXXX();
if (XXX === undefined) {
var viewportWidth = this.getViewport().getWidth();
var viewportHeight = this.getViewport().getHeight();
var windowWidth = viewportWidth * 0.9;
var windowHeight = viewportHeight * 0.9;
var x = (viewportWidth / 2) - (windowWidth / 2);
var y = (viewportHeight / 2) - (windowHeight / 2);
XXX = Ext.create('App.view.XXX', {
x: x,
y: y,
width: windowWidth,
height: windowHeight
});
XXX.show();
}
else {
XXX.show();
}
}
},
});
},
});
I have an application that has a middle panel that always changes depending on what part of the application the user is looking at. These might be messages, transactions etc.
Then there are 4 'fixed' panels at the 4 corners of the application around the middle panel that are mostly fixed for the lifetime of the application, but contain dynamically updated data and therefore need to be implemented using backbone.js
How do I structure such an application in backbone.js. It seems to defeat the "Do not repeat" rule to implement the intial rendering for all the side panels within every route in the router as I would end up repeating the same rendering code in every route.
How do I structure my code in this instance so that I don't repeat code in multiple places.
JavaScript is like any other code: if you find yourself writing the same lines of code, extract them in to a function. If you find yourself needing to use the same function, extract it (and related functions and data) in to its own object.
So, your router shouldn't be calling your views and models directly. Instead, it should be delegating to other objects that can manipulate your views and objects.
Additionally, since your going to set up the same basic page layout every time the app starts up, you might not want that code in the router. The layout happens whether or not the router fires, and no matter which route is fired. Sometimes it's easier to put the layout code in another object, as well, and have the layout put in place before the router fires up.
MyApplication = {
layout: function(){
var v1 = new View1();
v1.render();
$("something").html(v1.el);
var v2 = new View2();
v2.render();
$("#another").html(v2.el);
},
doSomething: function(value){
// do someething with the value
// render another view, here
var v3 = new View3();
v3.render();
$("#whatever").html(v3.el);
}
}
MyRouter = Backbone.Router.extend({
routes: {
"some/route/:value": "someRoute"
},
someRoute: function(value){
MyApplication.doSomething(value);
}
});
// start it up
MyApplication.layout();
new MyRouter();
Backbone.history.start();
I've written a handful of articles relating to these things, which you might find useful:
http://lostechies.com/derickbailey/2012/02/06/3-stages-of-a-backbone-applications-startup/
http://lostechies.com/derickbailey/2011/08/30/dont-limit-your-backbone-apps-to-backbone-constructs/
http://lostechies.com/derickbailey/2011/12/27/the-responsibilities-of-the-various-pieces-of-backbone-js/
http://lostechies.com/derickbailey/2012/03/22/managing-layouts-and-nested-views-with-backbone-marionette/
I've run into a headache with Backbone. I have a collection of specified records, which have subrecords, for example: surgeons have scheduled procedures, procedures have equipment, some equipment has consumable needs (gasses, liquids, etc). If I have a Backbone collection surgeons, then each surgeon has a model-- but his procedures and equipment and consumables will all be plain ol' Javascript arrays and objects after being unpacked from JSON.
I suppose I could, in the SurgeonsCollection, use the parse() to make new ProcedureCollections, and in turn make new EquipmentCollections, but after a while this is turning into a hairball. To make it sensible server-side there's a single point of contact that takes one surgeon and his stuff as a POST-- so propagating the 'set' on a ConsumableModel automagically to trigger a 'save' down the hierarchy also makes the whole hierarchical approach fuzzy.
Has anyone else encountered a problem like this? How did you solve it?
This can be helpful in you case: https://github.com/PaulUithol/Backbone-relational
You specify the relations 1:1, 1:n, n:n and it will parse the JSON accordingly. It also create a global store to keep track of all records.
So, one way I solved this problem is by doing the following:
Have all models inherit from a custom BaseModel and put the following function in BaseModel:
convertToModel: function(dataType, modelType) {
if (this.get(dataType)) {
var map = { };
map[dataType] = new modelType(this.get(dataType));
this.set(map);
}
}
Override Backbone.sync and at first let the Model serialize as it normally would:
model.set(response, { silent: true });
Then check to see if the model has an onUpdate function:
if (model.onUpdate) {
model.onUpdate();
}
Then, whenever you have a model that you want to generate submodels and subcollections, implement onUpdate in the model with something like this:
onUpdate: function() {
this.convertToModel('nameOfAttribute1', SomeCustomModel1);
this.convertToModel('nameOfAttribute2', SomeCustomModel2);
}
I would separate out the different surgeons, procedures, equipment, etc. as different resources in your web service. If you only need to update the equipment for a particular procedure, you can update that one procedure.
Also, if you didn't always need all the information, I would also lazy-load data as needed, but send down fully-populated objects where needed to increase performance.