Welcome to the fourth installment of our series on visualizing data with Progress KendoReact. Today we’ll be plugging our data into KendoReact components and building out multiple visualizations for the same set of data. At the end of this article, we’ll display different subsets of our data as both bar charts and a table of the data.
Previous installments:
- Part 1: How To Set Up A React NodeJS App With KendoReact For Data Visualization
- Part 2: How To Manage Multiple Data Visualizations In A React App Using KendoReact And React Hooks
- Part 3: How To Work With Complex JSON Data For Visualization With JavaScript
ChartView
TableView
Let’s jump right in!
Where we left off
At the end of our last installment, our App.js component code looked as follows:
const App = () => {
const [ isChartView, setChartView ] = useState(true);
const [ selected, setSelected ] = useState(
parseInt(localStorage.getItem(SELECTED_KEY), 10) || 0
);
const onTabSelect = ({ selected } = {}) => {
setSelected(selected);
localStorage.setItem(SELECTED_KEY, selected);
};
return (
<>
<h1>
<span role="img" aria-label="">
🍕
</span>
Pizza-o-matic
<span role="img" aria-label="">
🍕
</span>
</h1>
<TabStrip
selected={selected}
onSelect={onTabSelect}
tabPosition="left"
animation={false}
>
<TabStripTab title={"Tab 1"}>
<div className="ratings-header">
<h2>Company Ratings</h2>
<span className="buttons-span">
<img
src="chart-bar.svg"
alt="chart view"
className={isChartView ? "active" : "inactive"}
onClick={() => setChartView(true)}
/>
<img
src="table.svg"
alt="cell view"
className={isChartView ? "inactive" : "active"}
onClick={() => setChartView(false)}
/>
</span>
</div>
<div>{`Tab 1 ${isChartView ? "Chart" : "Table"} View`}</div>
</TabStripTab>
<TabStripTab title={"Tab 2"}>
<div className="ratings-header">
<h2>Company Ratings</h2>
<span className="buttons-span">
<img
src="chart-bar.svg"
alt="chart view"
className={isChartView ? "active" : "inactive"}
onClick={() => setChartView(true)}
/>
<img
src="table.svg"
alt="cell view"
className={isChartView ? "inactive" : "active"}
onClick={() => setChartView(false)}
/>
</span>
</div>
<div>{`Tab 2 ${isChartView ? "Chart" : "Table"} View`}</div>
</TabStripTab>
</TabStrip>
</>
);
};
Let’s cut that waaay down by abstracting some of the repetitive parts so that they’re actually looping through our data.
Our goal is still to display a TabStrip with several TabStripTabs within it. However, we want those tabs to actually represent known quantities within our data. For our specific use case, we want each tab to represent a region of restaurant ratings.
Let’s add an All Regions element to our data and then map over the imported rating data and make a TabStripTab for each region. We’ll first replace our previous ratings import statement with:
const [ratings] = useState([{ name: 'All Regions' }, ...ratingsJSON]);
This is just to “trick” the map function we’re about to run so that we have all 5 tabs present (1 representing all the regions and the other 4 representing each individual region).
Then we can map over all of the ratings and select their name field to determine how they will show up in the TabStrip. We can also send down the props that each Panel will need to determine how to visualize its data:
<TabStrip
selected={selected}
onSelect={onTabSelect}
tabPosition="left"
animation={false}
>
{ratings.map((localRatings) => {
return (
<TabStripTab
title={localRatings.name}
key={localRatings._id || localRatings.name}
>
<Panel
ratings={ratings}
selectedRatingsId={localRatings._id}
isChartView={isChartView}
setChartView={setChartView}
/>
</TabStripTab>
);
})}
</TabStrip>
Now our App.js file should be good to go. It’s accomplishing all of the goals we laid out for it, and we can leave it alone. The next step will be to work on the Panel component and determine which view we want to display therein.
Panel
The Panel component is responsible for choosing which type of visualization it will display and any rendering or data manipulation that might be common between the two visualizations. For our purposes, we’ll be choosing between a TableView component and a ChartView component.
Let’s set up the basics of our Panel component in Panel.js like so:
import React from 'react';
import 'hammerjs';
import { toRatingsSplitter, toCompanyRatings } from '../tools/dataSplitter';
// import { CompanyRatings } from './CompanyRatings';
// import { TableView } from './TableView';
// import { ChartView } from './ChartView';
export const Panel = (props) => {
const { ratings: [, ...ratings], selectedRatingsId, isChartView, setChartView } = props;
return (
<>
<div className="ratings-header">
{/* Overall Company Ratings go here */}
<span className="buttons-span">
<img
src="chart-bar.svg"
alt="chart view"
className={isChartView ? "active" : "inactive"}
onClick={() => setChartView(true)}
/>
<img
src="table.svg"
alt="cell view"
className={isChartView ? "inactive" : "active"}
onClick={() => setChartView(false)}
/>
<img
src="download.svg"
alt="download"
onClick={() => {}}
/>
</span>
</div>
{/* Display Chart or Table View here */}
</>
);
};
Panel.defaultProps = {
ratings: [],
selectedRatingsId: "All Regions",
isChartView: true,
setChartView: () => {}
};
I’ve commented out the imports for components we’ll work on in this article and highlighted where they will go in the Panel component.
Otherwise, we’re just destructuring all of the props passed down into this component by App.js. We’ve also added some JSX that will allow us to display a header for each Panel and buttons that will swap between different visualization styles.
Now we only have a couple more lines before we can leave Panel alone. We need to run a couple of functions from dataSplitter on the ratings that we’ve passed into this component, and then we can pass the results into both TableView and ChartView in the same way.
I’ll add the following to Panel.js above the return statement:
const localRatings = ratings.find((rating) => rating._id === selectedRatingsId);
const region = localRatings ? localRatings.name : "All Regions";
const ratingsSplitter = toRatingsSplitter(ratings);
const companyRatings = toCompanyRatings(ratings);
const regionOrStoreMeans = ratingsSplitter(region);
const SeriesView = isChartView ? ChartView : TableView;
The purpose of this snippet is to create:
- A SeriesView element that represents the correct view/component for a given situation as well as the data that it should accept
- An array of companyRatings that represent the overall ratings that the company received for each category of rating (staff satisfaction’, customer, etc.)
SeriesView is a conditionally-defined component. If our Panel is meant to show the ChartView, SeriesView references ChartView, otherwise it references TableView. Both of those components will be created to accept the same regionOrStoreMeans data. And now below our header in the returned JSX we can add:
<SeriesView
ratings={ratings}
region={region}
means={regionOrStoreMeans}
/>
Along with our overall company ratings in the header:
<h2>Company Ratings</h2>
<CompanyRatings ratings={companyRatings} />
And uncomment the imports:
import { CompanyRatings } from './CompanyRatings';
import { TableView } from './TableView';
import { ChartView } from './ChartView';
Our final Panel.js file should look something like this:
import React from 'react';
import 'hammerjs';
import { toRatingsSplitter, toCompanyRatings } from '../tools/dataSplitter';
import { CompanyRatings } from './CompanyRatings';
import { TableView } from './TableView';
import { ChartView } from './ChartView';
export const Panel = (props) => {
const { ratings: [, ...ratings], selectedRatingsId, isChartView, setChartView } = props;
const localRatings = ratings.find((rating) => rating._id === selectedRatingsId);
const region = localRatings ? localRatings.name : "All Regions";
const ratingsSplitter = toRatingsSplitter(ratings);
const companyRatings = toCompanyRatings(ratings);
const regionOrStoreMeans = ratingsSplitter(region);
const SeriesView = isChartView ? ChartView : TableView;
return (
<>
<div className="ratings-header">
<h2>Company Ratings</h2>
<CompanyRatings ratings={companyRatings} />
<span className="buttons-span">
<img
src="chart-bar.svg"
alt="chart view"
className={isChartView ? "active" : "inactive"}
onClick={() => setChartView(true)}
/>
<img
src="table.svg"
alt="cell view"
className={isChartView ? "inactive" : "active"}
onClick={() => setChartView(false)}
/>
<img
src="download.svg"
alt="download"
onClick={() => {}}
/>
</span>
</div>
<SeriesView
ratings={ratings}
region={region}
means={regionOrStoreMeans}
/>
</>
);
};
Panel.defaultProps = {
ratings: [],
selectedRatingsId: "All Regions",
isChartView: true,
setChartView: () => {}
};
Here’s how the Panel should look right now. Since we haven’t created TableView and ChartView yet, the main area of the Panel is still blank.
Company Ratings
For our overall company ratings, we want to generate an element for each category of rating and display every store’s average in every region for that category. Here’s the code for a component that can do that.
import React from "react";
import "hammerjs";
const toFormattedCategory = (category) => {
const upperCaseCategory =
category.charAt(0).toUpperCase() + category.slice(1);
return upperCaseCategory.split("_").join(" ");
};
export const CompanyRatings = ({ ratings }) => {
const ratingCategories = Object.keys(ratings);
const ratingsElements = ratingCategories.map((category) => {
const score = ratings[category];
return (
<li className="flex-item" key={`${category}-score`}>
{toFormattedCategory(category)}: {score.toFixed(1)}
</li>
);
});
return <ul className="flex-container">{ratingsElements}</ul>;
};
Notice that this data-displaying component’s driving force is a map over ratingCategories with some JSX producing a new element for each value in the array.
Since we passed companyRatings into this component from Panel above, we don’t need to do anything to retrieve the data for each category other than select the appropriate key from each rating element:
const score = ratings[category];
This example does a good job of modeling a component being “dumb.” We want the CompanyRatings component always to accept data that looks the same. If the component is displaying incorrect data, we know the error in our codebase is likely “upstream.” Dumb components like this make it much easier to debug a large codebase and allow for “Separation of Concerns” when working with state data.
And now we’re finished with this component. The only thing happening here is generating JSX elements that can display based off the passed-in data. This work amounts to a simple map over the four categories that adds JSX for each. This pattern is a common theme when using React and allows our application to dovetail with visualization libraries like KendoReact responsively.
ChartView
Our chart view will use the KendoReact Chart component to display a series of bar charts for each rating category. I’ll include the code below and then talk about some of the main ideas for this visualization.
import React from 'react';
import "hammerjs";
import {
Chart,
ChartTitle,
ChartSeries,
ChartLegend,
ChartSeriesItem,
ChartValueAxis,
ChartValueAxisItem,
ChartCategoryAxis,
ChartCategoryAxisItem,
} from "@progress/kendo-react-charts";
import { toChartCategories } from "../utils/dataSplitter";
export const ChartView = ({ ratings, region, means }) => {
const chartCategories = toChartCategories(ratings);
const seriesItems = means.map(({ name, ratings }) => {
const spaceStripper = (x) => x.split(" ").join("-");
const key = `${spaceStripper(region)}-${spaceStripper(name)}`;
const chartData = Object.values(ratings);
return <ChartSeriesItem type="column" data={chartData} key={key} />;
});
const title = region === 'All Regions' ? 'Average Region Ratings by Category' : `Ratings for ${region} Region Stores`;
const yAxisTitle = region === 'All Regions' ? 'Average Rating for Region' : 'Store Rating';
return (
<Chart>
<ChartTitle text={title} />
<ChartCategoryAxis>
<ChartCategoryAxisItem categories={chartCategories} />
</ChartCategoryAxis>
<ChartValueAxis>
<ChartValueAxisItem title={{ text: yAxisTitle }} min={0} max={5} />
</ChartValueAxis>
<ChartLegend position="bottom" orientation="horizontal" />
<ChartSeries>{seriesItems}</ChartSeries>
</Chart>
);
};
Note how few of these lines are actually visualizing the chart:
<Chart>
<ChartTitle text={title} />
<ChartCategoryAxis>
<ChartCategoryAxisItem categories={chartCategories} />
</ChartCategoryAxis>
<ChartValueAxis>
<ChartValueAxisItem title={{ text: yAxisTitle }} min={0} max={5} />
</ChartValueAxis>
<ChartLegend position="bottom" orientation="horizontal" />
<ChartSeries>{seriesItems}</ChartSeries>
</Chart>
KendoReact presents a really simple API for adding data visualizations to a React project. These components are also quite “dumb” in a way that allows us to deterministically work with them. Once we put our data into the correct shape, KendoReact will visualize it the same way every time.
Most of the work I’ve done in this component is mapping over the props data so that we have a ChartSeriesItem for each set of bar charts that we will display. This allows our data to be dynamic and creates a more flexible application that’s responsive to changes in incoming data. Again, this is just another map over the data our component has received. We’re using a map because we have a 1-to-1 relationship between elements in the means array and series items that we want to display (one chart for each group of means).
Here’s what the current application should look like:
TableView
As above, I’ll include the TableView code and then talk about some of the principles that are being used:
import React from "react";
import "hammerjs";
import { Grid, GridColumn as Column } from "@progress/kendo-react-grid";
import { toChartCategories, toGridData } from "../utils/dataSplitter";
export const TableView = ({ ratings, region, means }) => {
const chartCategories = toChartCategories(ratings);
const locality = region === "All Regions" ? "region" : "restaurant";
const heading = (
<Column field={locality} title={locality} key={`first-col-${locality}`} />
);
const columns = chartCategories.map((category) => {
const key = `table-column-${category}`;
return <Column field={category} title={category} key={key} />;
});
const headedColumns = [heading, columns];
const gridData = toGridData(locality, means);
return <Grid data={gridData}>{headedColumns}</Grid>;
};
And again, we have another simple map over some data built from our props to create an array of JSX elements. Here we’re using the chart categories as our source of truth and making a column for each category.
Here’s what the final application should look like at this point:
Conclusion
And that’s all that we need to create a responsive data-driven application! Some of the main ideas boil down to the layout, data manipulation, and then creating dumb components that project that data to the view.
For our final installment, we’ll look at how to export our table view data so that end users can extend our application as they see fit with a CSV file. KendoReact will provide us with the tools to easily export this data. Once we have that working, we should have a fully-formed app that’s ready to go!
Note: I’ve put a version of the code corresponding to the state at the end of this post on my GitHub account.
Resources
The entire source code for this project is available at my GitHub account.
Full Series:
- Part 1: How To Set Up A React NodeJS App With KendoReact For Data Visualization
- Part 2: How To Manage Multiple Data Visualizations In A React App Using KendoReact And React Hooks
- Part 3: How To Work With Complex JSON Data For Visualization With JavaScript
- Part 4: How to Use KendoReact to Create Bar Charts and Tables from the Same Data
- Part 5: How to Export Tabular Data from a React App with KendoReact ExcelExport – Digital Primates