Creating a Custom Interaction

In this section, we will be creating a completely custom interaction based on all the interaction topics covered so far. Thus, before you proceed, make sure you have learnt about:

Let's put it all together with two examples, one each for Surrogate and Spawnable Side Effects. We will be using the cars.json data for this section. However, you can use any data and any canvas definition with the same interaction model to get the desired output, since the interaction model does not necessarily depend on those criterias.

Registering a physical action

We start off by registering a custom physical action. We had created a ctrlClick physical action while learning about registering physical actions. You can find it here. The code can be found below:

ActionModel
.for(canvas)
.registerPhysicalActions({  /* to register the action */
    ctrlClick: firebolt => (targetEl, behaviours) => {
        targetEl.on('click', function (data) {
            if (event.metaKey) {
                const event = utils.getEvent();
                const mousePos = utils.getClientPoint(this, event);
                const interactionConfig = {
                    data
                };
                const nearestPoint = firebolt.context.getNearestPoint(mousePos.x, mousePos.y, 
                          interactionConfig);
                behaviours.forEach(behaviour => firebolt.dispatchBehaviour(behaviour, {
                    criteria: nearestPoint.id
                }));
            }
        });
    }
});

Registering a Behaviour

Thereafter, we will be registering a new behaviour. This behaviour will be mapped to the physical action we just created. We will be using the singleSelect behaviour we created while learning about registering behaviours. You can find the details about it here. The code for the same is provided below:

 ActionModel
          .for(canvas)
          .registerBehaviouralActions([class SingleSelectBehaviour extends GenericBehaviour {
            static formalName () {
              return 'singleSelect';
            }
            setSelectionSet (addSet, selectionSet) {
      
              if (addSet === null || !addSet.length) {
                selectionSet.reset();
              } else {
                  selectionSet.reset();
                  selectionSet.add(addSet);
              }
            }
      }]);

Mapping the behaviour to the physical action

We will now map the singleSelect behaviour with the ctrlClick physical action we have created. You can read more about mapping behaviours to physical actions here. Find the code for the same below:

 ActionModel
.for(canvas)
.registerPhysicalBehaviouralMap({
     ctrlClick: {
          behaviours: ['singleSelect']
      }
})

Custom Interaction Using A Spawnable Side Effect

Registering a Spawnable Side Effect

We will first register the spawnable side effect, TextSideEffect that we have learned earlier. You can find it here. The code for the same can be found below:

ActionModel
          .for(canvas)
          .registerSideEffects(class TextSideEffect extends SpawnableSideEffect {
            static formalName () {
              return 'selection-text';
            }

            apply (selectionSet) {
              const dataModel = selectionSet.mergedEnter.model;
              const drawingContext = this.drawingContext();

              const textGroups = this.createElement(drawingContext.sideEffectGroup, 'text', [1]);

              /* createElement is a utility method for side effects */
              textGroups.html(`Selected:${dataModel.getData().data.map(e => e.join(', '))}`);

              textGroups.attr('y', '30');
              return this;
            }
          })

Mapping the side effects to the behaviour

Now we will map the new side effect, TextSideEffect with the behaviour singleSelect. You can read about mapping side effects to behaviours here. The code to do the same is found below:

.mapSideEffects({
    singleSelect: ['selection-text']
})

Putting it all together

Once we have mapped the physical action with behaviours and behaviours with side effects, we can see it work every time we Ctrl+Click on the bars, we get a spawnable side effect along with the single select behaviour:

main
run-button
run-button
reset-button

loadData('/static/cars.json').then((res) => {    
  let node = document.getElementById('chart-container');    
  const env = muze();    
  const canvas = env.canvas();    
  const DataModel = muze.DataModel;
  const utils = muze.utils;
  const SpawnableSideEffect = muze.SideEffects.standards.SpawnableSideEffect;
  const GenericBehaviour = muze.Behaviours.standards.GenericBehaviour;
  const ActionModel = muze.ActionModel;  
  loadData('/static/cars-schema.json').then((schema) => {
    const dataModelInstance = new DataModel(res, schema);
          canvas
          	.width(600)
          	.height(400)
			.rows(['Miles_per_Gallon'])
			.columns(['Origin'])
			.data(dataModelInstance)
    		.mount(node)
  
      ActionModel
          .for(canvas)
          .registerPhysicalActions({  /* to register the action */ 
            ctrlClick: firebolt => (targetEl, behaviours) => {
                targetEl.on('click', function (data) {
                    if (event.metaKey) {
                        const event = utils.getEvent();
                        const mousePos = utils.getClientPoint(this, event);
                        const interactionConfig = {
                          data
                        }
                        const nearestPoint = firebolt.context.getNearestPoint(mousePos.x, mousePos.y,interactionConfig);
                      behaviours.forEach(behaviour => firebolt.dispatchBehaviour(behaviour, {
                        criteria: nearestPoint.id, 
                      }));
                    }
                });
            }
        })
    .registerBehaviouralActions([class SingleSelectBehaviour extends GenericBehaviour {
            static formalName () {
              return 'singleSelect';
            }
            setSelectionSet (addSet, selectionSet) {
      
              if (addSet === null || !addSet.length) {
                selectionSet.reset();
              } else {
                  selectionSet.reset();
                  selectionSet.add(addSet);
              }
            }
      }])
          .registerSideEffects(class TextSideEffect extends SpawnableSideEffect {
              static formalName() {
                  return 'selection-text';
              }

              apply(selectionSet) {
                 const dataModel = selectionSet.mergedEnter.model;
                 const drawingInf = this.drawingContext();

                       /* notice i'm appending in the html container */
                 const textGroups = this.createElement(drawingInf.htmlContainer, 'div', [1], 'muze-info')
                       /* createElement is a utility method for side effects */

                       textGroups.html('Selected:' + dataModel.getData().data.map(e=> e.join(', ')))
                 textGroups.style('position', 'absolute')
                              .style('background', 'rgba(255,0,0,0.5)')
                              .style('color', 'black');
                 return this;
             }
            })
        .registerPhysicalBehaviouralMap({
              ctrlClick: {   
                  behaviours: ['singleSelect']
              }
          })
        .mapSideEffects({
          singleSelect: ['selection-text']
      })
    });
})

We can see text appear over the bar when we press Ctrl + Click on a bar. It shows us the exact bar selected on the top of the chart. When we select a different bar, that corresponding bar is now the selected set and hence, the data for the same appears on the top of the canvas.

Custom Interaction Using A Surrogate Side Effect

Registering a Surrogate Side Effect

We will first register the surrogate side effect, StrokeSideEffect that we have learned earlier. You can find it here. The code for the same can be found below:

.registerSideEffects(class StrokeSideEffect extends SurrogateSideEffect{
    static formalName() {
        return 'stroke-effect';
    }

     apply(selectionSet) {
       const { completeSet, mergedExit, mergedEnter } = selectionSet;
       const context = this.firebolt.context;
       const layers = context.layers();
       layers.forEach((layer) => {
          const allElements = layer.getPlotElementsFromSet(completeSet.uids);
          allElements.style('stroke', '').style('stroke-width', 2);
          const enterElements = layer.getPlotElementsFromSet(mergedEnter.uids);
          enterElements.style('stroke', 'red');
          const exitElements = layer.getPlotElementsFromSet(mergedExit.uids);
          exitElements.style('stroke', 'green');
      });
   }
})

Mapping the side effects to the behaviour

Now we will map the new side effect, StrokeSideEffect with the behaviour singleSelect. You can read about mapping side effects to behaviours here. The code to do the same is found below:

.mapSideEffects({
    singleSelect: ['stroke-effect']
})

Putting it all together

Once we have mapped the physical action with behaviours and behaviours with side effects, we can see it work every time we Ctrl+Click on the bars, we get a spawnable side effect along with the single select behaviour:

main
run-button
run-button
reset-button

loadData('/static/cars.json').then((res) => {    
  let node = document.getElementById('chart-container');    
  const env = muze();    
  const canvas = env.canvas();    
  const DataModel = muze.DataModel;
  const utils = muze.utils;
  const SurrogateSideEffect = muze.SideEffects.standards.SurrogateSideEffect;
  const GenericBehaviour = muze.Behaviours.standards.GenericBehaviour;
  const ActionModel = muze.ActionModel;  
  loadData('/static/cars-schema.json').then((schema) => {
    const dataModelInstance = new DataModel(res, schema);
          canvas
          	.width(600)
          	.height(400)
			.rows(['Miles_per_Gallon'])
			.columns(['Origin'])
			.data(dataModelInstance)
    		.mount(node)
  
      ActionModel
          .for(canvas)
          .registerPhysicalActions({  /* to register the action */ 
            ctrlClick: firebolt => (targetEl, behaviours) => {
                targetEl.on('click', function (data) {
                    if (event.metaKey) {
                        const event = utils.getEvent();
                        const mousePos = utils.getClientPoint(this, event);
                        const interactionConfig = {
                          data
                        }
                        const nearestPoint = firebolt.context.getNearestPoint(mousePos.x, mousePos.y,interactionConfig);
                      behaviours.forEach(behaviour => firebolt.dispatchBehaviour(behaviour, {
                        criteria: nearestPoint.id, 
                      }));
                    }
                });
            }
        })
    .registerBehaviouralActions([class SingleSelectBehaviour extends GenericBehaviour {
            static formalName () {
              return 'singleSelect';
            }
            setSelectionSet (addSet, selectionSet) {
      
              if (addSet === null || !addSet.length) {
                selectionSet.reset();
              } else {
                  selectionSet.reset();
                  selectionSet.add(addSet);
              }
            }
      }])
          .registerSideEffects(class StrokeSideEffect extends SurrogateSideEffect {
            static formalName () {
              return 'stroke-effect';
            }

            apply(selectionSet) {
               const { completeSet, mergedExit, mergedEnter } = selectionSet;
               const context = this.firebolt.context;
               const layers = context.layers();
               layers.forEach((layer) => {
                  const allElements = layer.getPlotElementsFromSet(completeSet.uids);
                  allElements.style('stroke', '').style('stroke-width', 2);
                  const enterElements = layer.getPlotElementsFromSet(mergedEnter.uids);
                  enterElements.style('stroke', 'red');
                  const exitElements = layer.getPlotElementsFromSet(mergedExit.uids);
                  exitElements.style('stroke', 'green');
              });
           }
          })
        .registerPhysicalBehaviouralMap({
              ctrlClick: {   
                  behaviours: ['singleSelect']
              }
          })
        .mapSideEffects({
          singleSelect: ['stroke-effect']
      })
    });
})

You can see the strokes on each of the selected and non selected bars whenever you click on the mouse after pressing the Ctrl button.

Wrapping Up

This marks the end of the concepts regarding the interaction in Muze. These interactions play a vital role in making the canvases dynamic and the data driven conceptualization of the same plays a vital role in creating completely custom interactions.