Table of Contents
Familiar with MST models
Let's create a React chart component
Create our memory
Define the properties of the model
Define the operation
Define the view
Home Web Front-end CSS Tutorial Making a Chart? Try Using Mobx State Tree to Power the Data

Making a Chart? Try Using Mobx State Tree to Power the Data

Apr 15, 2025 am 09:49 AM

Making a Chart? Try Using Mobx State Tree to Power the Data

Who likes charts? Everyone likes it, right? There are many ways to create charts, including many libraries. For example, D3.js, Chart.js, amCharts, Highcharts and Chartist, which are just a few of the many options.

But we don't necessarily need a chart library to create charts. Mobx-state-tree (MST) is an intuitive alternative to Redux to manage state in React. We can build interactive custom charts using simple SVG elements and use MST to manage and manipulate the data of the chart. If you have tried building charts with tools like D3.js in the past, I think you'll find this approach more intuitive. Even if you are an experienced D3.js developer, I still think you will be interested in the power of MST as a visual data architecture.

Here is an example of MST using to drive charts:

This example uses the scale function of D3, but the chart itself is rendered using the SVG element in JSX. I don't know of any chart library with the option to flash hamster points, so this is a great example of why building your own chart is great – and it's not as difficult as you think!

I've been building charts with D3 for over 10 years and while I love the power of it, I always find that my code ends up being clumsy and hard to maintain, especially when dealing with complex visualizations. MST completely changes all this by providing an elegant way to separate data processing from rendering. I hope this article encourages you to give it a try.

Familiar with MST models

First, let's give a quick overview of the appearance of the MST model. This is not an in-depth tutorial on everything about MST. I just want to show the basics because in reality you only need that basics 90% of the time.

Here is a Sandbox with code for a simple to-do list built using MST. A quick look and I'll explain what each section does.

First, the shape of the object is defined by the typed definition of the model attributes. Simply put, this means that instances of the to-do model must have a title, which must be a string, and by default the "done" property is false.

 <code>.model("Todo", { title: types.string, done: false //这相当于types.boolean,默认为false })</code>
Copy after login

Next, we have the views and manipulation functions. A view function is a method of accessing calculated values ​​based on data in the model without changing the data saved by the model. You can think of them as read-only functions.

 <code>.views(self => ({ outstandingTodoCount() { return self.todos.length - self.todos.filter(t => t.done).length; } }))</code>
Copy after login

On the other hand, operation functions allow us to safely update data. This is always done in the background in a non-mutable way.

 <code>.actions(self => ({ addTodo(title) { self.todos.push({ id: Math.random(), title }); } }));</code>
Copy after login

Finally, we create a new storage instance:

 <code>const todoStore = TodoStore.create({ todos: [ { title: "foo", done: false } ] });</code>
Copy after login

To show what the memory actually does, I added some console logs to show the output of outStandingTodoCount() before and after the toggle function that triggers the first instance of Todo.

 <code>console.log(todoStore.outstandingTodoCount()); // 输出:1 todoStore.todos[0].toggle(); console.log(todoStore.outstandingTodoCount()); // 输出:0</code>
Copy after login

As you can see, MST provides us with a data structure that allows us to easily access and manipulate data. More importantly, its structure is very intuitive and the code is clear at a glance - there is no reducer!

Let's create a React chart component

OK, now that we have understood what MST looks like, let's use it to create a memory that manages the data of the chart. However, we will start with chart JSX, because once we know what data is needed, it is much easier to build memory.

Let's take a look at JSX that renders the chart.

The first thing to note is that we are using styled-components to organize our CSS. If you are not familiar with it, Cliff Hall has a great article showing its use in React applications.

First, we are rendering the drop-down menu that will change the axes of the chart. This is a fairly simple HTML drop-down menu wrapped in a styled component. It should be noted that this is a controlled input whose state is set using the selectedAxes value in our model (we will cover this later).

 <code>model.setSelectedAxes(parseInt(e.target.value, 10)) } defaultValue={model.selectedAxes} ></code>
Copy after login

Next is the chart itself. I've split the axes and points into their own components, which are in separate files. By keeping each file concise, this really helps keep the code maintainable. Furthermore, this means that if we want to use line charts instead of points, we can reuse the axes. This is very effective when dealing with large projects with multiple chart types. It also makes it easy to test components individually, whether it is programmatically or manually tested in the Active Style Guide.

 <code>{model.ready ? (</code>
    <code>{model.ready ? (</code> <axes xlabel="{xAxisLabels[model.selectedAxes]}" xticks="{model.getXAxis()}" ylabel="{yAxisLabels[model.selectedAxes]}" yticks="{model.getYAxis()}"></axes><points points="{model.getPoints()}"></points>
) : (
  <loading></loading>
)}
Copy after login

Try commenting out the axes and points components in the Sandbox above to see how they work independently.

Finally, we wrap the component in an observer function. This means that any changes in the model will trigger re-rendering.

 <code>export default observer(HeartrateChart);</code>
Copy after login

Let's take a look at the Axes component:

As you can see, we have an XAxis and a YAxis. Each has a label and a set of tick marks. We'll cover how to create markers later, but here you should note that each axis consists of a set of ticks generated by traversing a set of objects with labels and x or y values ​​(depending on which axis we're rendering).

Try changing some attribute values ​​of the element to see what happens...or what breaks! For example, change the line element in YAxis to the following:

<code><line x1="{30}" x2="95%" y1="{0}" y2="{y}"></line></code>
Copy after login

The best way to learn how to build visualizations using SVG is to simply experiment and destroy things. ?

OK, that's half of the chart. Now let's look at the Points component.

Each point on the chart consists of two parts: an SVG image and a circular element. The image is an animal icon, and the circle provides an animation of impulsiveness that is visible when hovering the mouse over the icon.

Try commenting out the image element, then commenting out the circular element and see what happens.

This time the model must provide an array of point objects that provide us with four properties: the x and y values ​​for the anchor points on the chart, the label of the points (the name of the animal), and the pulse, which is the duration of the pulse animation for each animal icon. Hopefully this all looks intuitive and logical.

Again, try modifying the property value to see which changes and interrupts are made. You can try setting the y property of the image to 0. Trust me, this is much easier than reading the W3C specification for SVG image elements!

Hope this gives you an idea of ​​how we render charts in React. Now, you just need to create a model with appropriate operations to generate the data we need to loop through in JSX.

Create our memory

Here is the complete code for the memory:

I broke the code into the three parts mentioned above:

  1. Define the properties of the model
  2. Define the operation
  3. Define the view

Define the properties of the model

Everything we define here is accessible from external properties as a model instance, and – if observable wrapper components are used – any changes to these properties will trigger re-rendering.

 <code>.model('ChartModel', { animals: types.array(AnimalModel), paddingAndMargins: types.frozen({ paddingX: 30, paddingRight: 0, marginX: 30, marginY: 30, marginTop: 30, chartHeight: 500 }), ready: false, // 表示types.boolean,默认为false selectedAxes: 0 // 表示types.number,默认为0 })</code>
Copy after login

Each animal has four data points: name (Creature), lifespan (Longevity__Years_), weight (Mass__grams_), and resting heart rate (Resting_Heart_Rate__BPM_).

 <code>const AnimalModel = types.model('AnimalModel', { Creature: types.string, Longevity__Years_: types.number, Mass__grams_: types.number, Resting_Heart_Rate__BPM_: types.number });</code>
Copy after login

Define the operation

We only have two operations. The first (setSelectedAxes) is called when changing the drop-down menu, which updates the selectedAxes property, which in turn determines which data is used to render the axis.

 <code>setSelectedAxes(val) { self.selectedAxes = val; },</code>
Copy after login

The setUpScales operation requires more explanation. This function is called in the useEffect hook function immediately after the chart component is mounted, or after the window is resized. It accepts an object containing the width of the DOM containing the element. This allows us to set a scale function for each axis to fill the full available width. I will explain the scale function soon.

To set the scale function we need to calculate the maximum values ​​for each data type, so we first traverse the animals to calculate these maximum values ​​and minimum values. We can use zeros as the minimum value of any scale we want to start from zero.

 <code>// ... self.animals.forEach( ({ Creature, Longevity__Years_, Mass__grams_, Resting_Heart_Rate__BPM_, ...rest }) => { maxHeartrate = Math.max( maxHeartrate, parseInt(Resting_Heart_Rate__BPM_, 10) ); maxLongevity = Math.max( maxLongevity, parseInt(Longevity__Years_, 10) ); maxWeight = Math.max(maxWeight, parseInt(Mass__grams_, 10)); minWeight = minWeight === 0 ? parseInt(Mass__grams_, 10) : Math.min(minWeight, parseInt(Mass__grams_, 10)); } ); // ...</code>
Copy after login

Now set the scale function! Here we will use the scaleLinear and scaleLog functions of D3.js. When setting these functions we specify the domain, which is the minimum and maximum input that the function can expect, and the range, which is the maximum and minimum output.

For example, when I call self.heartScaleY with the maxHeartate value, the output will equal marginTop. This makes sense because this will be at the top of the chart. For the lifetime attribute, we need to have two scale functions, because this data will be displayed on the x-axis or y-axis, depending on which drop-down option is selected.

 <code>self.heartScaleY = scaleLinear() .domain([maxHeartrate, minHeartrate]) .range([marginTop, chartHeight - marginY - marginTop]); self.longevityScaleX = scaleLinear() .domain([minLongevity, maxLongevity]) .range([paddingX marginY, width - marginX - paddingX - paddingRight]); self.longevityScaleY = scaleLinear() .domain([maxLongevity, minLongevity]) .range([marginTop, chartHeight - marginY - marginTop]); self.weightScaleX = scaleLog() .base(2) .domain([minWeight, maxWeight]) .range([paddingX marginY, width - marginX - paddingX - paddingRight]);</code>
Copy after login

Finally, we set self.ready to true because the chart is ready for rendering.

Define the view

We have two sets of view functions. The first set outputs the data required to render the axis scale (I said we'll get there!) the second set outputs the data required to render the point. We will first look at the scale function.

There are only two tick functions called from the React application: getXAxis and getYAxis. These functions simply return the output of other view functions based on the value of self.selectedAxes.

 <code>getXAxis() { switch (self.selectedAxes) { case 0: return self.longevityXAxis; break; case 1: case 2: return self.weightXAxis; break; } }, getYAxis() { switch (self.selectedAxes) { case 0: case 1: return self.heartYAxis; break; case 2: return self.longevityYAxis; break; } },</code>
Copy after login

If we look at the Axis functions themselves, we can see that they use the ticks method of the scale function. This returns a set of numbers that fit the axis. We then iterate through these values ​​to return the data required by the axis component.

 <code>heartYAxis() { return self.heartScaleY.ticks(10).map(val => ({ label: val, y: self.heartScaleY(val) })); } // ...</code>
Copy after login

Try changing the parameter value of the ticks function to 5 and see how it affects the chart: self.heartScaleY.ticks(5).

Now we have a view function that returns the data required by the Points component.

If we look at longevityHeartratePoints (which returns the point data for the Lifespan vs Heart Rate graph), we can see that we are traversing the animal array and using the appropriate scale function to get the x and y positions of the points. For pulse properties, we use some mathematical methods to convert the number of beats per minute of the heart rate into a numerical value representing the duration of a single heartbeat in milliseconds.

 <code>longevityHeartratePoints() { return self.animals.map( ({ Creature, Longevity__Years_, Resting_Heart_Rate__BPM_ }) => ({ y: self.heartScaleY(Resting_Heart_Rate__BPM_), x: self.longevityScaleX(Longevity__Years_), pulse: Math.round(1000 / (Resting_Heart_Rate__BPM_ / 60)), label: Creature }) ); },</code>
Copy after login

At the end of the store.js file, we need to create a Store model and instantiate it with the raw data of the animal object. It is common practice to attach all models to the parent Store model, and then access these models through the top-level provider as needed.

 <code>const Store = types.model('Store', { chartModel: ChartModel }); const store = Store.create({ chartModel: { animals: data } }); export default store;</code>
Copy after login

That's it! Here is our demonstration:

This is by no means the only way to organize data in JSX to build charts, but I found it very efficient. I've built a custom chart library for large enterprise clients using this structure and stack in a real environment and was shocked by how well MST performed in this purpose. I hope you have the same experience!

The above is the detailed content of Making a Chart? Try Using Mobx State Tree to Power the Data. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

AI Hentai Generator

AI Hentai Generator

Generate AI Hentai for free.

Hot Article

R.E.P.O. Energy Crystals Explained and What They Do (Yellow Crystal)
1 months ago By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. Best Graphic Settings
4 weeks ago By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. How to Fix Audio if You Can't Hear Anyone
1 months ago By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. Chat Commands and How to Use Them
1 months ago By 尊渡假赌尊渡假赌尊渡假赌

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

Working With GraphQL Caching Working With GraphQL Caching Mar 19, 2025 am 09:36 AM

If you’ve recently started working with GraphQL, or reviewed its pros and cons, you’ve no doubt heard things like “GraphQL doesn’t support caching” or

Building an Ethereum app using Redwood.js and Fauna Building an Ethereum app using Redwood.js and Fauna Mar 28, 2025 am 09:18 AM

With the recent climb of Bitcoin’s price over 20k $USD, and to it recently breaking 30k, I thought it’s worth taking a deep dive back into creating Ethereum

Creating Your Own Bragdoc With Eleventy Creating Your Own Bragdoc With Eleventy Mar 18, 2025 am 11:23 AM

No matter what stage you’re at as a developer, the tasks we complete—whether big or small—make a huge impact in our personal and professional growth.

Vue 3 Vue 3 Apr 02, 2025 pm 06:32 PM

It&#039;s out! Congrats to the Vue team for getting it done, I know it was a massive effort and a long time coming. All new docs, as well.

A bit on ci/cd A bit on ci/cd Apr 02, 2025 pm 06:21 PM

I&#039;d say "website" fits better than "mobile app" but I like this framing from Max Lynch:

Can you get valid CSS property values from the browser? Can you get valid CSS property values from the browser? Apr 02, 2025 pm 06:17 PM

I had someone write in with this very legit question. Lea just blogged about how you can get valid CSS properties themselves from the browser. That&#039;s like this.

Stacked Cards with Sticky Positioning and a Dash of Sass Stacked Cards with Sticky Positioning and a Dash of Sass Apr 03, 2025 am 10:30 AM

The other day, I spotted this particularly lovely bit from Corey Ginnivan’s website where a collection of cards stack on top of one another as you scroll.

Let's use (X, X, X, X) for talking about specificity Let's use (X, X, X, X) for talking about specificity Mar 24, 2025 am 10:37 AM

I was just chatting with Eric Meyer the other day and I remembered an Eric Meyer story from my formative years. I wrote a blog post about CSS specificity, and

See all articles