At Shopify, the Insights team creates visualization experiences that delight and inform. We've done a lot of great work prioritizing accessibility and motion design for web. Our mobile experiences though, were a bit of an afterthought, but not anymore! In this talk, we'll go through how we created our data viz components library; How we encapsulated core logic, animation, types and even UI components for web and mobile; and also why keeping things separate sometimes is better - to create awesome UX.
Sharing is Caring: Reusing Web Data Viz in React Native
AI Generated Video Summary
PolarisViz is a collection of React components that provide consistent visual styles, motion design, and accessibility features. It aims to solve the problem of inconsistencies in visualization decisions made by different teams. The library is flexible for different visual styles and has centralized theme management. PolarisViz was integrated with React Native using a separate library called Polaris Viz core. Challenges included limitations in native apps and the need to share UI components between web and mobile platforms.
1. Introduction to PolarisViz
I'm Cristal, a staff developer at Shopify, and today I'll share our journey on reusing web-built things to speed up React Native development. Before PolarisViz, each team made their own visualization decisions, resulting in inconsistencies. Different styles, implementations, and accessibility issues arose. PolarisViz aims to solve these problems by providing a collection of React components with consistent visual styles, motion design, and accessibility features.
Hi everyone. I'm Cristal, and I'm a staff developer at Shopify, more specifically on the Insights team, where we work creating data visualization experiences. Today I'd like to share with you our journey on reusing the things that we had built originally for web to speed up our development process with React Native.
But before we actually dive into implementation details, we should probably talk about what is PolarisViz, what is this library that we created and we plan to open source soon. And I think that the best way to understand what PolarisViz is is to talk about what data visualization looked like at Shopify before we had it. So before, each team was responsible for making their own decisions in terms of what tool to use or to implement something from scratch.
This led to, of course, a lot of inconsistencies, these two charts that used to live in the admin. And as you can see, one of them has vertical grid lines and the other one doesn't. One of them has thick lines and the other one thin lines. They also have different styles for the dashed lines that represent comparison periods. One of them uses squares on the legend. The other one uses lines on the legend. And lastly, one of them has a visible X-axis and the other one not. These inconsistencies were not only visual though because each had completely different implementation. One had print support and the other one didn't. One was accessible for keyboard navigation and the other one wasn't.
2. Building Polaris Viz and React Native Integration
We created a collection of React components that have consistent visual styles, motion design, and accessibility features. Our charts allow users to highlight data series and provide accessibility for screen readers. We also made the library flexible for different visual styles and centralized theme management. When we started focusing on React Native, we extracted platform-specific code into a third library, Polaris Viz core, to share code between the web and React Native versions. React Native uses different markup tags and event handling compared to React.
My favorite was the super useful area label line chart of sales over time. I mean if I was using a screen reader, I'm not sure that I would be glad with this description because it doesn't actually help me understand how my sales are going.
Even when the charts were just instances of the same component using our in-house components that were built, because the API looked something like this, as you can see we have to pass a lot of different props to make a chart look a specific way and these props had to be repeated for each instance of the chart. So it was very easy for a developer to, for example, say that the horizontal margin was 30 instead of 20 and there you have it, the charts now do not look the same.
So to solve all of those problems we started creating a collection of React components that not only had the same visual styles, but had a special focus on things that are very important for us. Like motion design, for example, because we believe that through animation we can guide the eye of a user and help them better understand the dataset, reducing the cognitive overload. We also have a special focus on accessibility. For example, by allowing our users to highlight a specific data series in a chart while we We help users that have color vision deficiencies make the connection in terms of what that specific bar series means, without having to rely on colors. So we can understand that those bars that are highlighted are the bars representing dinner, for example, even though we cannot differentiate purple from pink. We also have a focus on implementing accessibility for screen readers. This line chart, for example, we use area rows in the SVG markup so that a screen reader can actually access the data points that power the line chart and interact with it as if it was a label. So you can see that I'm here navigating on the row of April 2nd, and we can navigate the different cells through rows and columns like we would do if we were interacting with a plain HTML table.
And, of course, Shopify has many different brands that use different visual styles. As I mentioned before, we plan to open source the library soon, so we wanted the library to be flexible enough that people could implement their own visual identity to the charts, but also, we wanted to keep things consistent for whoever was using our charts. So no props need to be passed to each of these components. We have a centralized place where you can define the theme that you want to use and it gets automatically applied to all instances of a chart in your application. We will talk a little bit more to explore more in depth how that works a little later on.
So, in January 2020, Shopify announced that React Native was the future. And since then we have been focusing on writing our mobile apps with React Native and all of our new features with React Native. For our team, this was a very nice opportunity to get the things that we had learned while we were building Polaris Viz for Web and understand how we could create very good experiences for mobile with React Native. The first thing that we thought was, okay, so we can just create a brand new library, call it Polaris Viz Native, and that's it. That was a lot of work, though. We had been working on Polaris Viz for a few years at that point to get the library where it was, and starting from scratch we couldn't reuse all of those things, right? So what if we could extract the platform-specific code, the platform-agnostic code from Polaris Viz for web into a third library, Polaris Viz core, and then react native, the version of Polaris Viz for web and the version of Polaris Viz for React Native could both have Polaris Viz core as a dependency? What exactly could we share? So let's talk a little bit about the similarities and differences between React and React Native.
In React Native, we create components by using HTML tags. Like divs and p's for paragraphs, for example. On React Native, on the other hand, we have to import the markup from the React Native library. So this means that we have, for example, views instead of divs, and text instead of p. Because we're still in Reacts land, we can use all of the common React functionalities like hooks, for example. You can see that I'm using the useState hook exactly the same way, both in web and React Native. There are some differences in terms of how we attach events to the markup, like you can see here on the HTML button, I'm passing the setCount to OnClick, and the Native button, I'm passing it to OnPress.
3. Native App Limitations and SVG Integration
Native apps won't have access to window methods, so we use window.matchMedia and import the accessibilityinfo module from React Native to check user preferences. React Native doesn't support SVGs, so we use the React Native SVG library with similar API.
Because Native apps won't render in a browser, we won't have access to any of the window methods. So in this example, you can see that we're using window.matchMedia to check if a user prefers reduced motion or not. This is the hook that we have in our library. And we have to import the accessibilityinfo module from React Native to fetch the same information. So both of these hooks return a true or false based on the user preferences. But for web, we check window.matchMedia on Native. We import the accessibilityinfo module from React Native. And lastly, React Native does not support SVGs out of the box. And this was really important for us because all of our charts are written with SVGs. So to have a similar API, we decided to use the React Native SVG library that pretty much works the same way. You have to import the tags from the library and then write your component the same way that you would if you were using the regular SVG tags.
4. Extracting Platform Agnostic Code
We briefly talked about themes and how they are important to keep things consistent. And the theme implementation was actually also very easy to extract. So the way that it works is that the library comes with two themes out of the box. The default theme, which is a dark one, and we also provide a light theme. A theme is basically a big configurational object where you can define not only colors but all sorts of visual styles like if bars should have round edges or not, if the ticks should be visible or not on the axes. So the library exports a component called PolarizistProvider that you can use to wrap your whole application with and overrides the default theme. In this example, I am defining that the background color of my charts should be blue. And later on in my application, I'm implementing a bar chart and even though I'm not passing any props related to style, it has a blue background because I stated that my background should be blue on the default theme in the PolarizistProvider. We can even define multiple themes if we want. For example, here I am defining an Angry Reds and a Happy Green theme in the provider and then later on I can choose the theme that I want to use by the name that I gave them in the provider. In here, Angry Red has a red background, Happy Green has a green background. It looks horrible, but it works. The PolarizistProvider itself is simply a React's context provider. Under the hood, it uses a createTheme function that allows consumers to pass in a list of partial themes and returns a list of complete themes. This is so folks don't need to worry about passing in that huge configuration object. If you don't pass for example what the grids should look like, no problem. We're just going to use the default on the library. Another thing that was very, very easy to just extract was hooks. We have a bunch of hooks that we use to create all charts in a consistent manner.
5. Extracting useYscale, useXscale, and useLabels
We extracted useYscale, useXscale, and useLabels from the original implementation. Labels are SVG text and their display is calculated based on available space and dataset. We also extracted common types and pasted them into Polaris Viz Core. The original Polaris Viz consisted of Chart and UI components, along with the provider for themes, types, hooks, and utilities. We successfully extracted everything apart from UI into Polaris Viz Core.
Things like useYscale that returns to us the ticks and the Y-scale that we can use to draw a chart based on a couple of common props like the drawable heights. If the chart should only have integers on the ticks or not, etc., etc.
The same thing for useXscale and useLabels. Our labels are actually SVG text. They are not HTML elements. We have a very complex calculation to determine if the labels should be displayed horizontally, diagonally, vertically, if they should be truncated or not. All of those things are calculated based on the available screen space and also on the dataset that you have. For that, we use the useLabels.
6. Sharing UI Components Between Web and Mobile
To share UI components between web and mobile, we can extract the Y-axis, X-axis, legends, chart container, and lines. These components can be shared without compromising the mobile experience. We initially tried using React Native Web to render React Native components in a browser. By rewriting the UI building blocks in React Native, we can have both Polaris for web and Polaris Native use these components from a core library.
Before we actually start talking about how we can extract the UI components to share, maybe let's talk a little bit about what we should share. Let's have a look at this line chart, for example. It has this tooltip with a bunch of information that you can access on a hover, and also lines, and also legends, et cetera, et cetera. This component was originally intended for web, though, so you have to imagine that if we just shrink it down and make it fit in a small screen, it's not going to be the best experience. Take the tooltip, for example. If you try to interact with something that has a tooltip on a mobile, your thumb tends to get in the way of the information that you're reading. So we will need to think of a better way to handle that in small screens.
But what we could share, we could share, for example, the Y-axis, the X-axis, we could share legends, the chart container, which is the component that renders the background color and also the grid lines, and possibly all of the little lines. Each of these lines is a component that contains not only the line, but also the subtle gradient comes below the line, and the animation that plays once you load the component, the line grows from bottom to top, all of those things we can probably share without compromising on creating a good experience for mobile.
So how can we share? Because we talked about how the main difference between React and React Native is how we create the markup of components. Views versus divs, for example. So the first approach that we tried was using a library called React Native Web. This library is really amazing because it allows you to just write React Native components and it handles everything for you so that you can render your React Native components in a browser. So theoretically, we could rewrite the UI building blocks that we want to share in React Native and have both Polaris' for web and Polaris' Native use those building blocks. By doing that, we can move the building blocks UI components from the original Polaris Viz into core and have Polaris Viz React and Polaris Viz React Native consume them from core.
7. Challenges with Dependencies and Bundle Size
The problem with this approach is that adding React Native web and React Native as dependencies to Polaris' Native would increase the bundle size significantly. Additionally, React Native does not support SVG out of the box, so including React Native SVG as a dependency of core would be necessary. However, considering the complexity of Shopify's system and the impact on webpage performance, adding dependencies should be minimized.
The problem with this approach though is that not only Polaris' Native would need to have React Native web and React Native as a dependency, but also the web version of Polaris Viz because it would be relying on the components from core. This might not seem much, but if we compare the bundle size of react-dom, you can see that React Native web is almost double the size. And remember that we talked about React Native not supporting SVG out of the box? So to make this work, we would also have to include React Native SVG as a dependency of core. Shopify is a very complex system that has a lot of dependencies already, and because we have customers everywhere in the world, including places that don't have access to fast intranet speeds, every seemingly small dependency that we add can actually cause a big impact into how fast people can see the webpage and manage their shops.
8. Sharing DUI Building Blocks
We need a new strategy to share DUI building blocks while maintaining a small bundle size. The Polaris Viz provider in Core can determine the markup tags based on the platform. For example, in Polaris Viz Native, we import the original provider from Core and svg tags from react-native-svg. We pass the list of svg tags through the provider to create shared building blocks. In Polaris Viz Core Web, we pass regular HTML svg tags wrapped in React components. This allows us to create shared UI components between React Native and web.
Okay, so we need a new strategy to make this work, share DUI building blocks while maintaining the bundle size small. We already have a context provider that both Polaris Viz Web and Native will use. So what if the provider could also determine what the markup tags the components and cores should use depending on if we are in Web or Native? So in Polaris Viz Core, the Polaris Viz provider would accept a list of components, and then Web would pass Web components, Native would pass Native components. So in Core we would have the platform agnostic provider that receives the correct list of markup depending on the platform from the Polaris Viz Web and Polaris Viz Native.
Let's explore this as an example. In Polaris Viz Native we would have a re-export of the original provider, so we would import the original Polaris Viz provider from Core and also import the svg tags from the react-native-svg. So this means that react-native-svg is only a dependency of Polaris Viz Native. We then pass the list of svg tags that we want to use to create those shared building blocks through the provider. We would do a similar thing on the web version of this. We would also re-export the original provider, but instead of passing native svg tags, we would just pass regular HTML svg tags wrapped in React components. So you can see here that I'm using react.createElement to basically use the same behavior of the default api of an svg or a circle tag in React. This means that we can create some shared component in Polaris Viscore, and instead of using svg directly or importing it directly from React's native svg, we get the tags that we want to use to create the UI from the context by calling the UsePolarisVisProvider hook, and then we can create any markup that we want that is going to be shared between React native and web. So by using the UsePolarisVisContext in Polaris Viscore web, I'm going to have regular svg tags, and in Polaris Viscore native, I'm going to have native svg tags.
Now that we talked about what the overall strategy looks like, let's have a closer look into what it looks like to implement this strategy in a real chart. So we have this component called SparkBarChart, and just for context, SparkCharts are useful for presenting a trend. It's meant to be something that you can quickly glance on and have an understanding of your data, but it's not something that is meant for you to explore the details of a data set, like dig into a data set. We usually use it in analytics bars that are presented in pages, where the main focus is not data exploration, but can give useful insights on how your business is doing. In this page, for example, the main task is to work on your orders, the orders that your shop received, so it's useful to know what the trends for order received or product returns, for example, are. This is what the SPARK bar chart file in Polaris Viz native looks like. We first import the shared prop type, the bar components, as well as use X-scale and use Y-scale hooks from Polaris Viz core. We then use the hooks to obtain all of the functions that we need to render the bars, meaning X-scale, Y-scale, the bar width, and the get bar height. We then use the bar component that we imported from core in the chart. Under the hood, the bar component that lives in core is getting its path markup from the context. So, because I'm using bar inside of Polaris Viz native, it's going to get the native version of path instead of the regular one. We then import a native chart container from a relative path that is also located in the Polaris Viz native folder. The reason we need a native chart container is because we access different APIs to calculate the size that the container should be. Our charts in Polaris Viz basically try to occupy the space provided by the parents. So, to do that, we need to measure the parents to understand what the width and height of the chart should be. In native, we get the width and height of the parents by passing a function to the onLayout prop of the view component. That function gets called every time that the component changes size.
9. Building Blocks and Mobile Experience
In web, we use the resize observer to pass the calculated height and width to the children. This approach makes it easy for web developers to contribute to the library. For mobile, we reimagine the data visualization experience by using gestures to explore specific data ranges. We can drop the axis information and use gestures like pinch and scroll to navigate the data. Voice commands can also be used to filter data. Polaris Vis will be open source soon, and you can reach out to me directly or contact the team to test the library before it's released.
Alternatively, in web, we use the resize observer to do the same thing. Both web and native pass the calculated height and width to the children by using react.cloneElement. By using this approach, we can make sure that the elements will have access to the width and height even though we're not passing it explicitly as props. Chart here knows what the width and height of the parents is.
After a lot of exploring, failing and trying again, we got to a solution that actually works for us and makes it very easy for web developers to contribute to the library and speeds up the process.
With these building blocks in place, we can reimagine what a good data visualization experience looks like in mobile. So take a line chart for example. We briefly talked about this but this experience cannot just be, you know, shrink down to fit a small screen. We actually have to think about what it should look like for small screens. Very annoying to try to read a piece of information that is hidden under your thumb.
So what if, for example, we used gestures to explore one specific data range? We could drop the axis information so that we had the whole width of the screen to draw the actual shapes and then we could use gestures to explore a specific period of time, for example. We could have different gestures like a bench for zooming in and out of the data range and we could have, for example, two fingers to scroll horizontally to see the data points that are not currently visible on the screen. All of that while keeping the information of the current data point visible on top of the chart instead of hidden below my thumb. We could even have voice commands like, show me the data between June 8th and June 10th and the chart automatically filters it for you.
I think that's all that I had for today. Hopefully you are as excited as I am for the future of data visualization in mobile. As I mentioned before, Polaris Vis is going to be open source soon, so hopefully you also feel excited to contribute to the library. But if you want to have better access to it now and you want to help us test the library before it's open source, feel free to reach out to me directly, Cristal Campione, on Twitter or write to the team on polaris-vis-feedback at Shopify.com.