Reputation: 69
I'm trying to make a webpage display a chart using nodejs, express, mysql, and ejs, but I clearly don't understand something about how ejs / javascript etc. works. I need a script to run that sets up a chart (from chart.js module) yet it is not outputting any sort of chart whatsoever.
What I tried:
script below will not run:
<canvas id="myChart" width="50%" height="100px"></canvas>
<script scr="map-access-data.js" type="text/javascript"></script>
<script id ="map-popularity" type="text/javascript">
var Chart = require('chart');
var ctx = document.getElementById("myChart").getContext("2d");
var myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: getMapAccessNames(),
datasets:[{
label: "Map Selection Popularity",
data: getMapAccessData(),
backgroundColor: getMapColors(),
borderColor: getMapColors(),
borderWidth: 1
}]
},
options: {
scales: {
yAxes: [{
ticks: {
beginAtZero:true
}
}]
}
}
});
</script>
map-access-data.js file referenced in first script of that file:
var mysql = require('mysql');
var connection = mysql.createConnection({
host : 'localhost',
user : 'admin',
password : 'pass',
database : 'db'
});
connection.connect();
function getNumMaps(){
connection.query("SELECT NAME, COUNT(*) FROM map_access GROUP BY NAME ORDER BY NAME", function(err, rows){
return rows.length();
});
}
function getMapAccessData(){
var arr = [];
connection.query("SELECT NAME, COUNT(*) FROM map_access GROUP BY NAME ORDER BY NAME", function(err, rows){
for(i in rows){
arr.push(i.count);
}
});
}
function getMapAccessNames(){
var arr = [];
connection.query("SELECT NAME, COUNT(*) FROM map_access GROUP BY NAME ORDER BY NAME", function(err, rows){
for(i in rows){
arr.push(i.name);
}
});
return arr;
}
function getMapColors(){
var arr = [];
for(var i = 0; i < getNumMaps(); i++){
arr.push('rgba(255,99,132,1)');
}
return arr;
actual file that this code is rendered in:
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<% include header.ejs %>
<h1><%= title %></h1>
<p>See how people are using our app <br/></p>
<% include map-access-chart %>
</body>
</html>
Upvotes: 2
Views: 9760
Reputation: 560
There are a bunch of misconceptions here: The HTML file will be served to your client, which will see <script>
tags, request them from web server, and execute the code. You cannot expect a web browser to run a SQL query on your server), so obviously this is not the way you want to execute this JS code.
So much is wrong, it will be a long answer. Here is a your two main misconceptions:
Then your many smaller errors:
for (i in rows)
so i is the item index (and I guess you got that because you named it i), then you use i.count
SELECT COUNT(*) FROM
then just use .count
property, I'm not sure it will work without AS count
At this point I can only guess your SQL and Chart usage are no better, sorry :( I will try to point you in the right direction though.
So, first of all, you need to execute this JS code from your Node.js server. The usual steps would be:
Sample data structure for the next steps:
/
+- app.js
+- lib/
+- map-access-data.js
+- views/
+- index.ejs
+- map-access-chart.ejs
+- stylesheets/
+- styles.css
Required server dependencies: npm install express ejs mysql
, the rest is for client (like chart
)
// app.js
const express = require('express')
const app = express()
// Serve public static assets, like stylesheets and client-side JS
app.use(app.static('public'))
// Configure EJS template engine
app.set('view engine', 'ejs')
// Your route
app.get('/', (req, res) => {
// Your route handler
// req is client's request
// res is server's response
})
// Start web server on http://localhost:8000
app.listen(8000)
OK, here you're server-side, you can use MySQL and similar systems. But first we need to address another issue.
Asynchronous is a very important part of Node, really, but we can't address everything here. You will have the keywords, I let you do your research to tame that part. I'll use async
/await
so you're not too disturbed when reading the code, and util.promisify
to transform the methods. The things you have to understand:
connection.query
will query a remote server, in Node it will be done asynchronously, which means you won't get any result immediately, but your code won't be stopped either (or it would be blocking, which sucks)then
method; when using promises you must return those objects, which are a representation of your future data and the only way to access itasync
which allows you to wait
for promised data, but your async function will still be async which means it returns a wrapper, not your actual result, unless you wait
for it tooHere is your errors:
getNumMaps
, your return
is in the callback. This callback is called way after the function has returned its own result, so it will just return undefinedgetMapAccessData
you didn't even bother to return anything, still undefinedgetMapAccessNames
, finally you return something! but as connection.query is async, you will push data to your array way after funtion has already returned arr
, so it always returns []
And I'll add you execute three times the same request, sounds wasteful ;) So, you know you want to finally include all this in your Chart instance, let's not split that in 4 functions which all execute the same query, we'll instead build a single result with adapted format.
// lib/map-access-data.js
const mysql = require('mysql')
const connection = mysql.createConnection(/* your config here */)
// get promises instead of using callbacks
const util = require('util')
const queryCallback = connection.query.bind(connection) // or we'll have issues with "this"
const queryPromise = util.promisify(queryCallback) // now this returns a promise we can "await"
// our asynchronous method, use "async" keyword so Node knows we can await for promises in there
async function getChartData () {
const rows = await queryPromise("SELECT name, COUNT(*) AS count FROM map_access GROUP BY name ORDER BY name")
// Array#map allows to create a new array by transforming each value
const counts = rows.map(row => row.count)
const names = rows.map(row => row.name)
const colors = rows.map(row => 'rgba(255,99,132,1)')
// Return an object with chart's useful data
return {
labels: names,
data: counts,
colors: colors,
}
}
OK, now you have a function, available server side only, that gives you what you need.
Now you need to be able to call it from app.js
, which means you need to:
// lib/map-access-data.js
…
// Export your function as default
module.exports = getChartData
// app.js
const getChartData = require('./lib/map-access-data)
This is called CommonJS modules
Now in your route handler you can simply call your async function, and await for its result:
// app.js
…
app.get('/', async (req, res) => {
// Your route handler
const data = await getChartData()
})
Now you have your data made available, you're still server-side, you now have to generate valid HTML for your client, which currently looks like:
<!DOCTYPE html>
<html>
… a bunch of HTML …
<p>See how people are using our app <br/></p>
<canvas id="myChart" width="50%" height="100px"></canvas>
<!-- NO! it's not a client JS, it's server JS, client CANNOT download it -->
<script scr="map-access-data.js" type="text/javascript"></script>
<script id ="map-popularity" type="text/javascript">
var Chart = require('chart'); // NO! you can't *require* from client
var ctx = document.getElementById("myChart").getContext("2d");
var myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: getMapAccessNames(), // NO! You can't call server methods from client
datasets:[{
…
Obviously we need to fix a few things:
map-access-data.js
which makes no sensechart.js
the browser's way, like from a CDNHere I think instead of injecting the real data directly into HTML you could use an Ajax request, but I don't know Chart so I will let you do this part. An Express app serving JSON data is absolutely trivial, just res.send(data)
, then do some Ajax on client side. Let's see the version where you inject data directly into HTML to break all the walls:
JSON.stringify
)<script>
, runs it, everyone is happy<!-- views/map-access-chart.ejs -->
<canvas id="myChart" width="50%" height="100px"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/1.0.2/Chart.min.js"></script>
<script id ="map-popularity" type="text/javascript">
var ctx = document.getElementById("myChart").getContext("2d");
var myChart = new Chart(ctx, {
type: 'bar',
data: {
labels: <%- JSON.stringify(data.labels) %>,
datasets:[{
label: "Map Selection Popularity",
data: <%- JSON.stringify(data.data) %>,
backgroundColor: <%- JSON.stringify(data.colors) %>,
borderColor: <%- JSON.stringify(data.colors) %>,
borderWidth: 1
…
// app.js
…
// Your route handler
const data = await getChartData()
// Render 'index.ejs' with variables 'title' and 'data' available
res.render('index', {
title: 'Page title',
data: data,
})
Now when you run node app.js
from your terminal, and go to http://localhost:8000 you should see the result. I guess there will be many remaining errors, but that will be a better start :)
Upvotes: 13