Cross Interaction

Muze enables you to create interactive visualization using a consistent mental model. In this document we will go through how can you create two interactive visualizations and how Muze cross connects the charts automatically.

Lets start with our cars.json data

main
run-button
run-button
reset-button
// See first 5 rows of dataset
loadData('/static/cars.json').then(res => printTable(res, {rowLimit: 5}));

Our goal is to have two visualizations one with Miles_per_Gallon by country (Origin field) and Miles_per_Gallon by Cylinders.

We will be creating a DataModel from the root data

main
run-button
run-button
reset-button
//@preamble-start
loadData('/static/cars.json')
  .then((res) => {
const DataModel = muze.DataModel;

const schema = [
	{ name: 'Name', type: 'dimension' },
	{ name: 'Miles_per_Gallon', type: 'measure', defAggFn: 'avg' },
	{ name: 'Cylinders', type: 'dimension' },
	{ name: 'Displacement', type: 'measure', defAggFn: 'max' },
	{ name: 'Horsepower', type: 'measure', defAggFn: 'max' },
	{ name: 'Weight_in_lbs', type: 'measure', defAggFn: 'avg' },
	{ name: 'Acceleration', type: 'measure', defAggFn: 'avg' },
	{ name: 'Year', type: 'dimension', subtype: 'temporal', format: '%Y-%m-%d' },
	{ name: 'Origin', type: 'dimension' } /* by default dimension */
];
// @preamble_end
const dataModelInstance = new DataModel(res, schema);
printTable(dataModelInstance.getData().data, {});
})

In order to get the data for Miles_per_Gallon by Origin we will be performing a groupBy operation followed by a project operation which keeps only one measure and one dimension. If you want to know more about the operators, read DataModel Operators

main
run-button
run-button
reset-button
//@preamble-start
loadData('/static/cars.json')
  .then((res) => {
    const DataModel = muze.DataModel;

    const schema = [
        { name: 'Name', type: 'dimension' },
        { name: 'Miles_per_Gallon', type: 'measure', defAggFn: 'avg' },
        { name: 'Cylinders', type: 'dimension' },
        { name: 'Displacement', type: 'measure', defAggFn: 'max' },
        { name: 'Horsepower', type: 'measure', defAggFn: 'max' },
        { name: 'Weight_in_lbs', type: 'measure', defAggFn: 'avg' },
        { name: 'Acceleration', type: 'measure', defAggFn: 'avg' },
        { name: 'Year', type: 'dimension', subtype: 'temporal', format: '%Y-%m-%d' },
        { name: 'Origin', type: 'dimension' } /* by default dimension */
    ];

    const dataModelInstance = new DataModel(res, schema);
    // @preamble_end
    const compose = DataModel.Operators.compose;
    const groupBy = DataModel.Operators.groupBy;
    const project = DataModel.Operators.project;

    originMileageDMCreator = compose(
      groupBy(['Origin'], { Miles_per_Gallon: 'avg' }),
      project(['Origin', 'Miles_per_Gallon'])
    );

    originMileageDM = originMileageDMCreator(dataModelInstance);
    printTable(originMileageDM.getData().data, {});
})

Similarly for the other visualization Miles_per_Gallon by Cylinders we perform the similar action only instead of Origin we will be doing groupBy with Cylinders.

main
run-button
run-button
reset-button
//@preamble-start
loadData('/static/cars.json')
  .then((res) => {
    const DataModel = muze.DataModel;

    const schema = [
        { name: 'Name', type: 'dimension' },
        { name: 'Miles_per_Gallon', type: 'measure', defAggFn: 'avg' },
        { name: 'Cylinders', type: 'dimension' },
        { name: 'Displacement', type: 'measure', defAggFn: 'max' },
        { name: 'Horsepower', type: 'measure', defAggFn: 'max' },
        { name: 'Weight_in_lbs', type: 'measure', defAggFn: 'avg' },
        { name: 'Acceleration', type: 'measure', defAggFn: 'avg' },
        { name: 'Year', type: 'dimension', subtype: 'temporal', format: '%Y-%m-%d' },
        { name: 'Origin', type: 'dimension' } /* by default dimension */
    ];

    const dataModelInstance = new DataModel(res, schema);
    // @preamble_end
    const compose = DataModel.Operators.compose;
    const groupBy = DataModel.Operators.groupBy;
    const project = DataModel.Operators.project;

    originMileageDMCreator = compose(
      groupBy(['Cylinders'], { Miles_per_Gallon: 'avg' }),
      project(['Cylinders', 'Miles_per_Gallon'])
    );

    originMileageDM = originMileageDMCreator(dataModelInstance);
    printTable(originMileageDM.getData().data, {});
})

Now that we have our chart rendered, try interacting (hover, click, drag) with one chart and see how the other chart gets affected.

If you notice, we have not written any code to establish interactivity among charts. But still cross interaction happens.

Wrapping Up

In this section we will briefly build the intuition on how automatic interaction is established.

If you recollect from the above section, when we created DataModel instance for the above visualization, we applied operators (originMileageDMCreator and cylinderMileageDMCreator) on the root DataModel.

Every time we apply an operator on a DataModel instance it creates another instance of DataModel with the effect of the operator. If we keep on applying these operators we end up getting a Directed Acyclic Graph (DAG) like this

Attach an illustration here

Every node of graph is a DataModel instance. The edge between two nodes is the operator used to create the child from the parent.

As you can see in the picture, we can pickup any DataModel instance and visualize it in DOM.

When you perform any action (click, hover, drag) on one visualization, the affected set of data with action details gets propagated through out the DAG. If the entries in the affected set are found in any other visualization.