Sharing is Caring: Reusing Web Data Viz in React Native

Bookmark

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.

by



Transcription


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 would 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 Polarizviz, what is this library that we created and we plan to open source soon. And I think that the best way to understand what Polarizviz 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 like 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 a 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. And I guess that my favorite was this 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 are hovering on them, 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 color. 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. So 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 rows 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, and 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. So 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 react 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 set count to onClick and the native button, I'm passing it to onPress. 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 accessibility info 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 accessibility info 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. Okay, so let's talk about how we started extracting platform agnostic code and what exactly is platform agnostic code. So quick wins. The first thing was everything that is just javascript. So I'm talking about things that we literally cut from the original implementation of Polaris Viz and pasted it into Polaris Viz core. So we had a big file with a lot of constants saved that were shared across multiple components. So things like default values for spacing, animation, border ranges, et cetera, et cetera. All of those things are just javascript we can just cut and paste. We also had a bunch of utility functions. So these are things that help us, for example, create linear gradients from an array of solid colors, convert hex to RGB, the custom curve that gets applied to our line charts, functions that help us work with data by filtering out falsy values, et cetera, et cetera. All of those little utility functions, just javascript we can just cut and paste. 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 lights 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 Polaris's provider 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 color should be blue on the default theme in the Polaris's provider. We can even define multiple themes if we want. For example, here I am defining an angry red 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. And here, angry red has a red background, happy green has a green background. It looks horrible, but it works. The Polaris's provider itself, it's simply a react's context provider. Under the hood, it uses a create theme 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 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. Okay. So another thing that was very, very easy to just extract was hooks. We had a bunch of hooks that we used to create all charts in a consistent manner. Things like useYscale that returns to us the ticks and the Yscale 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. So we have a very complex calculation actually to determine if the label 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 data set that you have. And for that, we used the useLabels. Because it was just javascript, we managed to just extract it from the original implementation. And we can also use typescript in react Native, which means that all of our common types could just be extracted and pasted into Polaris Viz core. So to recap, originally, Polaris Viz was composed of the chart components and the UI components that are the building blocks for the charts, the Polaris Viz provider that is responsible for themes, types, hooks, and utilities. Just with the quick wins, and I'm talking about things that we didn't have to change, we just cut and paste, we managed to extract pretty much everything apart from UI from the original implementation of Polaris Viz into Polaris Viz core. So 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. So 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, legends, etc. 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, it's going to be a little bit more difficult to interact with it. So if you're reading 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, the text, 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 that comes below the line, and the animation that plays once you load the component, that 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 Viz for Web and Polaris Viz 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. The problem with this approach, though, is that not only Polaris Viz 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 the 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 internet speeds, every seemingly small dependency that we add can actually cause a big impact into how fast people can see the web page and manage their shops. Okay, so we need a new strategy to make this work, share the UI 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 in core should use, depending on if we are in web or native? So in Polaris Viz core, the Polaris Viz provider would accept the list of components, and then web would pass web components, Native would pass Native components, and 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 will 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 Viz core, and instead of using svg directly or importing it directly from react Native svg, we get the tags that we want to use to create the UI from the context by calling the usePolarisVizProvider 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 usePolarisViz context in Polaris Viz web, I'm going to have regular svg tags, and in Polaris Viz 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 Spark bar chart. And just for context, Spark charts 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. 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 actually make sure that the element is going to have access to the width and height, even though we're not passing it explicitly as props. So chart here knows what 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 use gestures to make the child's eyes look bigger? We could have, for example, we use 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 zoom, a bench for zooming in and out of the data range, or gestures 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. So yeah, I think that 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 Viz 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 Campioni, on Twitter, or write to the team on polaris-viz-feedback at shopify.com. Thank you so much for following along and enjoy the rest of the conference.
26 min
21 Jun, 2022

Check out more articles and videos

We constantly think of articles and videos that might spark Git people interest / skill us up or help building a stellar career

Workshops on related topic