One to Many Relationships (Frontend)
Now, let's make the front-end for our Temperatures app.
Roadmap
- Display all of a single location's average high temperatures on a chart.
- Interact with our Rails API
- Use fetch for AJAX requests
- Configure CORS
- Make a line graph with
Chart.js
This is the final product we're shooting for:
Temperatures for Location 1
Setup
Make our Frontend app.
In the top-level temperatures
npx create-react-app temperatures_client
cd temperatures_client
- Open with VS Code
code .
-
touch .env
- inside
.env
typePORT=3001
(set default port to always be 3001, since rails default port is 3000 - notice: no spaces, no quotes, port is all caps) - You can also do this with one command like this:echo 'PORT=3001' > .env
- inside
-
in
package.json
setproxy
to behttp://localhost:3000
-
install chart.js
npm install chart.js
mkdir src/components
touch src/components/BarChart.js
- Get your Dev Server started with
npm start
BarChart.js
import Chart from 'chart.js';
function BarChart() {
return (
<>
<h1>Temperatures</h1>
<canvas id="temperatures" width="300" height="100"></canvas>
</>
);
}
export default BarChart;
App.js
import BarChart from './components/BarChart.js';
import './App.css';
function App() {
return (
<div className="App">
<BarChart />
</div>
);
}
export default App;
Fetch
Using Chrome's fetch command we can make AJAX requests with 'vanilla' javaScript instead of importing some framework or library to do so.
Let's get all of our locations from our Rails API.
Make an AJAX request to get locations in BarChart.js
:
// make sure we import the useEffect hook from react
import { useEffect } from 'react';
// Make the AJAX request using helper function below once the component mounts to the DOM
useEffect(() => {
getAppData();
});
// Define a helper for making our AJAX request
function getAppData() {
fetch('/locations')
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.log(err)) ;
}
If you get this...
...then restart rails (control c and rails s
) and restart create react app (control c and npm start
).
We may get our single-origin policy obstruction. (And if we don't, we would definitely get it when we deploy to production.)
CORS
Switch over to temperatures_api
for the next couple steps.
Uncomment rack-cors in Gemfile
:
Run bundle
.
Allow all origins in config/initializers/cors.rb
Restart the Rails server.
Note: When you deploy your projects to production, make sure you are not allowing all origins as we are above. You should only allow traffic from your local server and your production front-end (e.g. GitHub Pages).
Configure Fetch
Now, switch back to temperatures_client
.
Let's change our fetch
to get a single location and also to console.log it. We want a single location so that we can display a chart of climate data just for that location.
Change the URL to get /locations/1
:
In the developer console, you should see something like this:
fetch worked! We received location 1 along with that location's temperatures.
Chart.JS
We want to display a chart that graphs all the average high temperatures for a given location.
Chart.js
is a library that renders charts using HTML5's Canvas capability.
Here's a Canvas tutorial if you want to learn more another day.
Chart.js
can do all the heavy lifting with Canvas. All we have to do is plug in some data.
We already put a canvas
element in our BarChart
component:
<canvas id="temperatures" width="300" height="100"></canvas>
Now let's bring in our data:
useEffect(() => {
getAppData();
});
function getAppData() {
fetch('/locations/1')
.then(response => response.json())
.then(data => console.log(data))
.catch(err => console.log(err));
}
We'll want to prepare our data. Currently our data isn't arranged to go into our bar chart. We could always look up how to make the correctly shaped object in the chart.js docs, but for now let's trust that the code below will do the trick.
Our data object at minimum should have two arrays:
- labels - for the x axis
- datasets - for the y axis
function getAppData() {
fetch('/locations/1')
.then(response => response.json())
.then(data => prepareData(data))
.catch(err => console.log(err));
}
function prepareData(data) {
const chartData = {
labels: [],
datasets: [
{
label: 'Avg high temps',
data: []
}
]
};
data.temperatures.forEach(temperature => {
chartData.labels.push(temperature.month)
chartData.datasets[0].data.push(temperature.average_high_f)
});
return chartData;
}
- Instantiate a new Chart object. The Chart constructor takes the canvas context and an options object as arguments.
useEffect(() => {
getAppData();
});
function getAppData() {
fetch('/locations/1')
.then(response => response.json())
.then(data => prepareData(data))
.then(preparedData => createChart(preparedData))
.catch(err => console.log(err));
}
function prepareData(data) {
const chartData = {
labels: [],
datasets: [
{
label: 'Avg high temps',
data: []
}
]
};
data.temperatures.forEach(temperature => {
chartData.labels.push(temperature.month)
chartData.datasets[0].data.push(temperature.average_high_f)
});
return chartData;
}
function createChart(data) {
const ctx = document.getElementById('temperatures');
new Chart(ctx, {
type: 'line',
data
});
}
If it's working, we should see something like the chart below:
All the code:
import { useEffect } from 'react';
import Chart from 'chart.js';
function BarChart() {
useEffect(() => {
getAppData();
});
function getAppData() {
fetch('/locations/1')
.then(response => response.json())
.then(data => prepareData(data))
.then(preparedData => createChart(preparedData))
.catch(error => console.log(error));
}
function prepareData(data) {
const chartData = {
labels: [],
datasets: [
{
label: 'Avg high temps',
data: []
},
{
label: 'Avg low temps',
data:[]
}
]
};
data.temperatures.forEach(temperature => {
chartData.labels.push(temperature.month)
chartData.datasets[0].data.push(temperature.average_high_f)
});
return chartData;
}
function createChart(data) {
const ctx = document.getElementById('temperatures')
new Chart(ctx, {
type: 'line',
data,
});
}
return (
<>
<h1>Temperatures</h1>
<canvas id="temperatures" width="300" height="100"></canvas>
</>
);
}
export default BarChart;
Second dataset
Add in a second dataset for Avg low temps
.
We'll push in the average_low_f
data.
const chartData = {
labels: [],
datasets: [
{
label: 'Avg high temps',
data: []
},
{
label: 'Avg low temps',
data: []
}
]
};
Push in the low temps:
data.temperatures.forEach(temperature => {
chartData.labels.push(temperature.month);
chartData.datasets[0].data.push(temperature.average_high_f);
chartData.datasets[1].data.push(temperature.average_low_f);
});
Possible Result:
Datasets options
Each 'datasets' object can have more options than just label
and data
. You can choose how to display each dataset. And as you might have guessed, you can have more than one dataset on a chart.
Try hardcoding each one of these into your chartData options separately and seeing the results
Example:
Result:
Change the chart type
Bar chart
Result:
Chart options
Here are some advanced chart options for reference:
const chartData = {
labels: [],
datasets: [
{
label: 'Avg high temps',
data: [],
fill: false,
lineTension: 0,
backgroundColor: "rgba(192, 77,77,.5)",
borderColor: "rgba(192, 77,77,1)",
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: "rgba(192, 77,77,1)",
pointBackgroundColor: "#fff",
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: "rgba(192,77,77,1)",
pointHoverBorderColor: "rgba(220,220,220,1)",
pointHoverBorderWidth: 2,
pointRadius: 1,
pointHitRadius: 10,
spanGaps: false,
},
{
label: 'Avg low temps',
data:[],
fill: false,
lineTension: 0,
backgroundColor: "rgba(75,192,192,0.4)",
borderColor: "rgba(75,192,192,1)",
borderCapStyle: 'butt',
borderDash: [],
borderDashOffset: 0.0,
borderJoinStyle: 'miter',
pointBorderColor: "rgba(75,192,192,1)",
pointBackgroundColor: "#fff",
pointBorderWidth: 1,
pointHoverRadius: 5,
pointHoverBackgroundColor: "rgba(75,192,192,1)",
pointHoverBorderColor: "rgba(220,220,220,1)",
pointHoverBorderWidth: 2,
pointRadius: 1,
pointHitRadius: 10,
spanGaps: false,
}
]
};
Bonus
- Create a form that lets us POST new temperatures to
/locations/1
- Have the chart update after the POST request
Other things our app could do:
- show all of a location's data on a single chart
- have separate charts for each dataset
- display the location in Google Maps using lat and lng
- have an index of selectable locations
- use React Router to tab between charts
- use React Router to tab between locations