DEV Community

Cover image for The Curious Case of the Visualization State Machine
brk
brk

Posted on

The Curious Case of the Visualization State Machine

It was a curious thing indeed, this state machine. I stumbled upon the concept quite by accident, you see, while tinkering with some coding challenge. At first, it seemed like a jumble of gears and levers, a confusing mess of switch-statements and method calls. But the more I studied it, the more I began to see the beauty of its design.

Now, a state machine - what is it, you ask? See it like a well-oiled machine. It breaks down a great, swirling mass of computation into neat, tidy little states, each with its own set of rules and transitions.

You see, this machine is no ordinary automaton. It has states, like when you have different rooms in a grand old house, each with its own unique character and purpose. It also has transitions, just like the pathways of that house that are allowing you to move from one room to the next.

An animated image depicting the different states of a data visualization using state machinesThe state machine in action

Some more findings:

  • One state is defined as the initial state. When a machine starts to execute, it automatically enters this state.
  • Each state can define actions that occur when a machine enters or exits that state. Actions will typically have side effects.
  • Each state can define events that trigger a transition.
  • A transition defines how a machine would react to the event, by exiting one state and entering another state.
  • A transition can define actions that occur when the transition happens.
  • Actions will typically have side effects.

And what some of us might call a finite-state machine (FSM), are the foundation of all computation. They are the building blocks of everything from digital circuits to software programs. By breaking down a problem into a set of defined states and transitions, we can create systems that are more predictable, maintainable, and therefore scalable. It's like taking a great, tangled ball of yarn and carefully, methodically, winding it into a neat, tidy skein.

Now that you've had a look at the big picture, watch a live demo of a (basic) state machine tailored to our data visualization use case and then, we'll dig further.

A never-ending mechanic

Take, the updateVisualization() method. This is the heart of the state machine, the conductor that orchestrates the entire performance. It is a simple enough method, really, just a switch statement that calls the appropriate state-specific method based on the current step:

/**
 * This is the State machine.
 *
 * Updates the visualization based on the current step.
 * @param {number} step - The current step of the visualization.
 */
updateVisualization(step) {
  const visualizationSteps = {
    0: () => this.showInitialState(),
    1: () => this.showIntroState(),
    2: () => this.showZoneState(),
    // ... and so on
  };

  visualizationSteps[step]?.();
}
Enter fullscreen mode Exit fullscreen mode

The line of code below simply means that the first state of the visualization will be handled by showInitialState().

0: () => this.showInitialState()
Enter fullscreen mode Exit fullscreen mode

Within each of those state-specific methods, the real magic happens. Take the showZoneState() method, for instance:


showZoneState() {
  this.setupVisualizationState({
    hideText: ["#intro-text", "#presentation-text"],
    styleType: "zone",
    tooltipType: "zone",
  });
}
Enter fullscreen mode Exit fullscreen mode

The state machine is calling the setupVisualizationState() method, which is responsible for configuring the various aspects of the visualization –like hiding and showing text, updating the styles and tool-tips, and so on. This way, each step is building upon the last, creating therefore a seamless and engaging user experience.

And as I delved deeper into the concept, I couldn't help but marvel at the elegance of it all. The way it handled the complex interplay of user interactions, map layers, and visualization settings – it was like watching a master puppeteer, pulling the strings with effortless grace.

But what really struck me was the way the state machine anticipated the user's every move. It was as if it could peer into the future, anticipating the next button click or menu selection, and adjust its course accordingly. Have a peek at the handleStepChange() method:

/**
 * Handles Next/Previous buttons change.
 *
 * Handles the step change when the "Next" or "Previous" button is clicked.
 * @param {number} direction - The direction of the step change (1 for next, -1 for previous).
 */
handleStepChange(direction) {
  const newStep = this.state.currentStep + direction;
  if (newStep >= 0 && newStep <= 10) {
    this.state.currentStep = newStep;
    this.updateVisualization(newStep);
    this.updateButtonState();
  }
}
Enter fullscreen mode Exit fullscreen mode

Here, the method is carefully monitoring the user's button clicks, adjusting the current step, updating the visualization to the corresponding step, and of course updating the state of the buttons themselves. We wouldn't like to get lost along the way, right?

So when the user makes a move, the state machine springs into action, smoothly transitioning from one state to the next, updating the visualization with precision and finesse.

And by watching in motion this humble piece of code, I saw the embodiment of a fundamental truth: the most complex systems can be tamed, if only we have the patience and the vision to understand them.

Enhancing the visualization

Now, this visualization tool we've been tinkering with - since we aim to elevate the viewer's experience and make it a veritable dance of data and pixels, we'll need to furnish it with neat little goodies. Let's review some:

/**
 * Adds circle markers to the map representing the population percentage of each GeoJSON feature.
 */
addPopulationDots() {
  this.state.geoJsonLayer.eachLayer((layer) => {
    const { feature } = layer;
    const radius = Math.max(feature.properties.pop_percentage * 5);
    const dot = L.circleMarker(layer.getBounds().getCenter(), {
      radius,
      color: config.COLORS.DEFAULT,
      weight: 1,
      opacity: config.OPACITY.DEFAULT,
      fillOpacity: config.OPACITY.LOWER,
      fillColor: config.COLORS.DEFAULT,
      zIndexOffset: 1000 - radius,
    }).bindTooltip(
      `${feature.properties.nom_complet}<br>Part de la pop. rég. ${feature.properties.pop_percentage}%`,
      { permanent: false, direction: "center", className: "custom-tooltip" }
    );
    dot.addTo(this.state.map);
  });
}
Enter fullscreen mode Exit fullscreen mode

Take, for instance, the way it handles the population visualization in the snippet provided above. The addPopulationDots() method adds those little dots to the map, each one a representation of the humanity in a visual symphony of our fellow citizens. And wait.. with a flick of the wrist, the whole thing can be cleared away leaving the canvas clean and pristine, ready for the next movement.

But it doesn't stop there, oh no! This state machine, it's got tricks up its sleeve. Look below at the way it manages the styles and tool-tips, seamlessly transitioning from one map mode to the next. Each one is a unique expression of the data, and therefore a different glimpse through a lens which to view the world.

/**
 * Generates a style for the GeoJSON layer .
 *
 * Returns a style object based on the specified feature, type and option.
 * @param {object} feature - The feature to generate the style for.
 * @param {string} type - The type of style to generate. (e.g., "default", "zone", "consumption", "production", "ratioEnr").
 * @param {object} options - Optional options for the style.
 * @returns {object} The generated style object.
 */
generateStyle(feature, type, options = {}) {
  const styleMap = {
    // ... various style generation functions
  };

  return (styleMap[type] || styleMap.default)();
}
Enter fullscreen mode Exit fullscreen mode

Now look at the way it handles the transitions between states. Like a skilled dancer, the state machine glides effortlessly from one step to the next, each move carefully choreographed to create a seamless and captivating performance.

/**
 * Updates the visualization based on the specified category and type.
 * @param {string} category - The category of the visualization (e.g., "consumption", "production", "final").
 * @param {string|Object} type - The type of visualization within the category.
 */
updateVisualizationByType(category, type) {
  const options = {};
  // ... set up options based on category and type
  this.updateGeoJsonStyle(category === "final" ? type : category, options);
  this.updateTooltips(category === "final" ? type : category, options);
}
Enter fullscreen mode Exit fullscreen mode

And when the user wants to explore different aspects of the data within an interactive menu, the state machine is there to guide them, offering up a display of options that integrate with the overall visualization.

/**
 * Shows a radio button menu on the map with the specified types and name.
 * @param {Array} types - An array of objects representing the menu options, each with an id and label.
 * @param {string} name - The name of the menu.
 */
showMenu(types, name) {
  this.hideMenu();
  $(config.SELECTORS.MAP).append(this.createMenu(types, name));
  $(`input[name="${name}-type"]`).on("change", (e) =>
    this.updateVisualizationByType(name, e.target.value)
  );
}
Enter fullscreen mode Exit fullscreen mode

That's all folks, we did it, and I can tell that the visualization in motion you see, is a delicate pas de deux between a human and a machine, each one responding to the other's every move. In the end, what do we have? A visualization that doesn't just display data, but rather, one that makes it sing by telling a story, just like a telltale that captivates and inspires people.

The complete source code is available on this github repository.

(Cover picture: Modern Times, 1936).

Top comments (0)