Reactivity over minimongo collection (newbie inside)


#1

I am building a miniapp with meteor to plot a pair of data series in a nvd3 chart. Those data series are arrays of 201 (x, y) points, which are calculated from only some user input values, which I am storing into session variables, something like this:

HTML:
<input type="number" name="my-value" min="0" step="0.01">

JS:

Template.body.onCreated(function(){
	// Initialization
	Session.set("my_value_1", 0);
        Session.set("my_value_2", 0);
	
	Dataseries = new Mongo.Collection(null);
	Dataseries.insert({_id: "x_values", data: range(0, 20, 201)})
	Dataseries.insert({_id: "y1", data: Array.from({length: 201}, () => 0)})
	Dataseries.insert({_id: "y2", data: Array.from({length: 201}, () => 0)})
});

In Template.body.events I use the change event on my parameters form, and use Session.set to store new Session variable values when Session.equals is false. That is ok.

The problem is the Minimongo dataseries. Once all the Session variables are set, I update the series like this (range is a function I wrote to generate a range, just like in Python):
Dataseries.update("x_values", {$set: {data: range(my_value1*0.85, my_value2*1.15, 201)}})

And my chart is built from that data like this:

Template.chart.onRendered(function() {
	var myData = [
		{
		  values: [{x: Dataseries.find("x_values"), y: Dataseries.find("y1")}],
		  key: 'Chart1',
		  color: '#ff7f0e'
		},
		{
		  values: [{x: Dataseries.find("x_values"), y: Dataseries.find("y2")}],
		  key: 'Chart2',
		  color: '#2ca02c'
		}
	];
 // ... more code to run the chart
});

The chart starts empty, with values between -1 and 1 in both X and Y axis, and of course it is not being updated automatically.

I supposed that, since I am defining the data as the result from a Minimongo query, a change in the collection would imply updating the chart itself, but it does not seem so. I tried looking for information on the publish-suscribe model, but it does not seem to apply here, since I am not suscribing to a collection created in the server: it is all happening at the client.

How can I achieve reactivity on my MiniMongo data?

Since I am defining Dataseries variable into Template.body.onCreated, is the variable also available into Template.chart.onRendered?

Thanks


#2

Not necessarily (it depends on if you’re defining your body and chart Templates in the same source file). Instead of defining/creating your Collection in a Template’s onCreated callback though, it’s better practice to define this Collection outside of a Template, so it’s available to the rest of your application. For example:

Option 1: If using classic meteor™ (credit @robfallows :slight_smile: )

/lib/somecollection.js

Dataseries = new Mongo.Collection(null);

Option 2: If using Meteor ES2015 modules:

/imports/api/data/somecollection.js

import { Mongo } from 'meteor/mongo';
const Dataseries = new Mongo.Collection(null);
export default Dataseries;

#3

If I do that, just like that, I get the error “Dataseries is not defined”.

In /client/main.js I am importing:

import '../imports/api/data/dataseries.js';

Which looks like this:

import { Mongo } from 'meteor/mongo';
const Dataseries = new Mongo.Collection(null);
export default Dataseries;

#4

Are you then importing Dataseries where you’re attempting to use it? So in your one of your Template files for example, are you:

import Dataseries from '/imports/api/data/somecollection';

#5

There are a number of things here which you may want to consider.

  1. As you have shown it, Dataseries may be defined globally, so could be accessible within your onRendered. You may like to consider binding this to the template instance - onCreated and onRendered share this (pun intended).
this.Dataseries = new Mongo.Collection(null);
this.Dataseries.insert({_id: "x_values", data: range(0, 20, 201)})
this.Dataseries.insert({_id: "y1", data: Array.from({length: 201}, () => 0)})
this.Dataseries.insert({_id: "y2", data: Array.from({length: 201}, () => 0)})

and

values: [{x: this.Dataseries.find("x_values"), y: this.Dataseries.find("y1")}],
//
values: [{x: this.Dataseries.find("x_values"), y: this.Dataseries.find("y2")}],
  1. You will only “see” changes reactively if you access reactive data within a reactive context. Cursors (as Dataseries.find()) are reactive data, but onRendered is not a reactive context. You could use a template autorun to establish a reactive context here.

  2. The lines like:

values: [{x: Dataseries.find("x_values"), y: Dataseries.find("y1")}],

Are returning cursors - you need to return values (de-reference the cursors), perhaps by using .find().fetch() - although you will need to restructure your code, because fetch() will return whole objects not just the values you’re after.


#6

I got my body and chart templates both into /client/main.js. Could improve that, I know.

I was guessing that I required using … to go one level back and then to /imports/blahblahblah. Is that wrong?


#7

Okay, if your Templates are in your /client/main.js file, then your import would look like:

import Dataseries from '../imports/api/data/somecollection';

or

import Dataseries from '/imports/api/data/somecollection';

You can use whichever one you prefer; the outside of Meteor JS ecosystem prefers relative paths. Just make sure you’re specifying Dataseries in your import.


#8

I claimed “meteor classic™”. It looks like you can claim “classic meteor™” :slight_smile:


#9

A template autorun would be…

Template.chart.autorun(function() {
  ...
});

?

Anyway, I don’t see clearly how I control when this code is being executed (should get executed each time the minimongo data changes).


#10

More like this:

Template.chart.onRendered(function() {
  this.autorun(() => {
    var myData = [
      {
        values: [{x: Dataseries.find("x_values"), y: Dataseries.find("y1")}],
        key: 'Chart1',
        color: '#ff7f0e'
      },
      {
        values: [{x: Dataseries.find("x_values"), y: Dataseries.find("y2")}],
        key: 'Chart2',
        color: '#2ca02c'
      }
    ];
  // ... more code to run the chart
  });
});

Except see my note about those finds needing work!

That’s exactly what will happen.


#11

Still missing something. Sorry for being as stupid as a stone:

Template.chart.onRendered(function() {
	var myData = []
	this.autorun(function () {
		this.myData = [
			{
			  values: [{x: Dataseries.find("x_values"), y: Dataseries.find("y1_values")}],
			  key: 'Chart1',
			  color: '#ff7f0e'
			},
			{
			  values: [{x: Dataseries.find("x_values"), y: Dataseries.find("y2_values")}],
			  key: 'Chart2',
			  color: '#2ca02c'
			}
		];
		
		if(this.chart != undefined){
			d3.select('#riskGraph svg')
			  .datum(this.myData)
			  .call(this.chart);
			this.chart.update();
		}
	});
	
	nv.addGraph(function() {
	  var chart = nv.models.lineChart()
					.margin({left: 100}) 			//Adjust chart margins to give the x-axis some breathing room.
					.useInteractiveGuideline(true)  //We want nice looking tooltips and a guideline!
					.showLegend(true)       		//Show the legend, allowing users to turn on/off line series.
					.showYAxis(true)        		//Show the y-axis
					.showXAxis(true);    		    //Show the x-axis

	  chart.xAxis     //Chart x-axis settings
		  .axisLabel('Underlying')
		  .tickFormat(d3.format(',r'));

	  chart.yAxis     //Chart y-axis settings
		  .axisLabel('P/L')
		  .tickFormat(d3.format('.02f'));

	  //Select the <svg> element you want to render the chart in and populate with data
	  d3.select('#riskGraph svg')
		  .datum(this.myData)
		  .call(chart);

	  //Update the chart when window resizes.
	  nv.utils.windowResize(function() { chart.update() });
	  return chart;
	});
});

It doesn’t even get plotted right now. “Uncaught TypeError: Cannot read property ‘map’ of undefined”. Appart of having that pending fix with the cursors, of course. I’ll be working on that

EDIT: Ok, it seems to be related to the cursors: http://stackoverflow.com/questions/17157554/nvd3-pie-chart-says-uncaught-typeerror-cannot-call-method-map-of-undefined

I’ll fix those right now. Anyway, do that code make good sense for you?


#12

Hmm - I think you’ve got some scoping issues with myData and chart. The code below fixes the myData ones (and I think will resolve the cursor issues too).

Template.chart.onRendered(function () {
  var myData = [];
  var chart;
  this.autorun(function () {
    myData = [
      {
        values: [{ x: Dataseries.findOne("x_values").data, y: Dataseries.findOne("y1_values").data }],
        key: 'Chart1',
        color: '#ff7f0e'
      },
      {
        values: [{ x: Dataseries.findOne("x_values").data, y: Dataseries.findOne("y2_values").data }],
        key: 'Chart2',
        color: '#2ca02c'
      }
    ];

    if (chart != undefined) {
      d3.select('#riskGraph svg')
        .datum(myData)
        .call(chart);
      chart.update();
    }
  });

As far as chart is concerned, you have this:

if(chart != undefined){
  d3.select('#riskGraph svg')
    .datum(myData)
    .call(chart);
  chart.update();
}

But I don’t see anywhere that a defined chart is ever returned into the template’s scope for that code to ever execute.