Build a Live Poll Result Chart With JavaScript
In this guide, you will build a live-updating chart based on votes to a multiple-choice poll. New entries will be added using a Directus REST API, and results will be accessed using a WebSockets connection. The chart will be created and updated using Chart.js.
Before You Start
You will need a Directus project. If you don’t already have one, the easiest way to get started is with our managed Directus Cloud service.
Create a new collection called votes
, with a dropdown selection field called choice
. Create two choices - one with the value of dogs
and one with cats
.
In the Public
role, give Create access to the votes
collection.
Create a new Role called Results
, and make sure the Results
role has Read access to the votes
collection. Create a new user with this role, generate an access token, and make note of it.
Create The Vote Page
Create a vote.html
file and open it in your code editor. Add the following:
<!DOCTYPE html>
<html>
<body>
<div id="options">
<button id="cat">Cats</button>
<button id="dog">Dogs</button>
</div>
<p></p>
<script>
const directusUrl = 'https://your-directus-url';
document.querySelector('#cat').addEventListener('click', vote('cats'));
document.querySelector('#dog').addEventListener('click', vote('dogs'));
async function vote(choice) {
await fetch(`${directusUrl}/items/votes`, {
method: 'POST',
body: JSON.stringify({ choice }),
headers: {
'Content-Type': 'application/json',
},
});
}
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<div id="options">
<button id="cat">Cats</button>
<button id="dog">Dogs</button>
</div>
<p></p>
<script>
const directusUrl = 'https://your-directus-url';
document.querySelector('#cat').addEventListener('click', vote('cats'));
document.querySelector('#dog').addEventListener('click', vote('dogs'));
async function vote(choice) {
await fetch(`${directusUrl}/items/votes`, {
method: 'POST',
body: JSON.stringify({ choice }),
headers: {
'Content-Type': 'application/json',
},
});
}
</script>
</body>
</html>
This uses the Directus REST API to create a new item in the votes
collection when the a button is pressed. Make sure to replace your-directus-url
with your project’s URL.
Load vote.html
in your browser and click a button. Check your Directus project and you should see a new item in the votes
collection.
At the bottom of your vote
function, add some user feedback that the vote was cast:
document.body.innerHTML = 'Vote cast';
document.body.innerHTML = 'Vote cast';
Refresh your browser and try casting a vote. The page should be replaced with a success message once the vote has taken place.
Create The Results Page
Also create a results.html
file and open it in your editor. Add the following:
<!DOCTYPE html>
<html>
<body>
<div style="display: flex; justify-content: center; height: 80vh">
<canvas id="chart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script type="module">
import {
createDirectus,
realtime,
authentication,
staticToken,
} from 'https://www.unpkg.com/@directus/sdk/dist/index.js';
const url = 'https://your-directus-url/';
const access_token = 'your-access-token';
const client = createDirectus(url)
.with(staticToken(access_token))
.with(realtime());
client.connect();
client.onWebSocket('open', function () {
console.log({ event: 'onopen' });
});
client.onWebSocket('message', function (data) {
if (data.type == 'auth' && data.status == 'ok') {
}
if (data.type == 'subscription' && data.event == 'init') {
}
if (data.type == 'subscription' && data.event == 'create') {
}
});
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<div style="display: flex; justify-content: center; height: 80vh">
<canvas id="chart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script type="module">
import {
createDirectus,
realtime,
authentication,
staticToken,
} from 'https://www.unpkg.com/@directus/sdk/dist/index.js';
const url = 'https://your-directus-url/';
const access_token = 'your-access-token';
const client = createDirectus(url)
.with(staticToken(access_token))
.with(realtime());
client.connect();
client.onWebSocket('open', function () {
console.log({ event: 'onopen' });
});
client.onWebSocket('message', function (data) {
if (data.type == 'auth' && data.status == 'ok') {
}
if (data.type == 'subscription' && data.event == 'init') {
}
if (data.type == 'subscription' && data.event == 'create') {
}
});
</script>
</body>
</html>
This boilerplate creates a placeholder element for our chart, and includes Chart.js from a CDN. It also sets up our WebSocket methods - for more information check out our Getting Started With WebSockets guide.
Make sure to replace your-directus-url
with your project’s URL, and your-access-token
with your token.
Set Up Chart
At the bottom of your <script>
, initialize a pie chart which will have two segments:
const ctx = document.getElementById('chart');
const chart = new Chart(ctx, {
type: 'pie',
data: {
labels: [],
datasets: [
{
label: '# of Votes',
data: [],
backgroundColor: ['#4f46e5', '#f472b6'],
},
],
},
});
const ctx = document.getElementById('chart');
const chart = new Chart(ctx, {
type: 'pie',
data: {
labels: [],
datasets: [
{
label: '# of Votes',
data: [],
backgroundColor: ['#4f46e5', '#f472b6'],
},
],
},
});
Load results.html
in your browser. Don’t worry that nothing is displayed yet - the chart has no data so it can’t render.
Add Existing Votes On Load
Once authenticated, immediately subscribe to the votes
collection:
if (data.type == 'auth' && data.status == 'ok') {
client.sendMessage({
type: 'subscribe',
collection: 'votes',
query: {
aggregate: { count: 'choice' },
groupBy: ['choice'],
},
})
}
if (data.type == 'auth' && data.status == 'ok') {
client.sendMessage({
type: 'subscribe',
collection: 'votes',
query: {
aggregate: { count: 'choice' },
groupBy: ['choice'],
},
})
}
The query
groups all items by their choice
value, and are then counted by the aggregation. The result is a payload that shows how many of each choice exist in the collection.
A message is sent over the connection when a connection is initialized with data from the existing collection. Use this data to edit the chart’s dataset and update it:
if (data.type == 'subscription' && data.event == 'init') {
for (const item of data.data) {
chart.data.labels.push(item.choice);
chart.data.datasets[0].data.push(item.count.choice);
}
chart.update();
}
if (data.type == 'subscription' && data.event == 'init') {
for (const item of data.data) {
chart.data.labels.push(item.choice);
chart.data.datasets[0].data.push(item.count.choice);
}
chart.update();
}
Refresh the page, and you should see the chart update with the initial values.
Update Chart With New Votes
When a new vote is cast, update the chart’s dataset and update it:
if (data.type == 'subscription' && data.event == 'create') {
const vote = data.data[0];
const itemToUpdate = chart.data.labels.indexOf(vote.choice);
if (itemToUpdate !== -1) {
chart.data.datasets[0].data[itemToUpdate]++;
} else {
chart.data.labels.push(vote.choice);
chart.data.datasets[0].data.push(1);
}
chart.update();
}
if (data.type == 'subscription' && data.event == 'create') {
const vote = data.data[0];
const itemToUpdate = chart.data.labels.indexOf(vote.choice);
if (itemToUpdate !== -1) {
chart.data.datasets[0].data[itemToUpdate]++;
} else {
chart.data.labels.push(vote.choice);
chart.data.datasets[0].data.push(1);
}
chart.update();
}
This code finds which index position the choice is in, increases it by 1, and updates the chart.
Open both vote.html
and results.html
in your browser. Make a vote and see the chart update.
Next Steps
There are many ways to improve the project built in this guide:
- Accept more data - such as voter name or contact details.
- Dynamically generate the vote form based on the field options.
- Disallow multiple votes by storing completion states.
- Create other chart types.
Full Code Sample
vote.html
<!DOCTYPE html>
<html>
<body>
<div id="options">
<button id="cat">Cats</button>
<button id="dog">Dogs</button>
</div>
<p></p>
<script>
const directusUrl = 'https://your-directus-url';
document.querySelector('#cat').addEventListener('click', vote('cats'));
document.querySelector('#dog').addEventListener('click', vote('dogs'));
async function vote(choice) {
await fetch(`${directusUrl}/items/votes`, {
method: 'POST',
body: JSON.stringify({ choice }),
headers: {
'Content-Type': 'application/json',
},
});
}
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<div id="options">
<button id="cat">Cats</button>
<button id="dog">Dogs</button>
</div>
<p></p>
<script>
const directusUrl = 'https://your-directus-url';
document.querySelector('#cat').addEventListener('click', vote('cats'));
document.querySelector('#dog').addEventListener('click', vote('dogs'));
async function vote(choice) {
await fetch(`${directusUrl}/items/votes`, {
method: 'POST',
body: JSON.stringify({ choice }),
headers: {
'Content-Type': 'application/json',
},
});
}
</script>
</body>
</html>
results.html
<!DOCTYPE html>
<html>
<body>
<div style="display: flex; justify-content: center; height: 80vh">
<canvas id="chart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script type="module">
import {
createDirectus,
realtime,
authentication,
staticToken,
} from 'https://www.unpkg.com/@directus/sdk/dist/index.js';
const url = 'https://your-directus-url/';
const access_token = 'your-access-token';
const client = createDirectus(url)
.with(staticToken(access_token))
.with(realtime());
client.connect();
client.onWebSocket('open', function () {
console.log({ event: 'onopen' });
});
client.onWebSocket('message', function (data) {
if (data.type == 'auth' && data.status == 'ok') {
client.sendMessage({
type: 'subscribe',
collection: 'votes',
query: {
aggregate: { count: 'choice' },
groupBy: ['choice'],
},
});
}
if (data.type == 'subscription' && data.event == 'init') {
for (const item of data.data) {
chart.data.labels.push(item.choice);
chart.data.datasets[0].data.push(item.count.choice);
}
chart.update();
}
if (data.type == 'subscription' && data.event == 'create') {
const vote = data.data[0];
const itemToUpdate = chart.data.labels.indexOf(vote.choice);
if (itemToUpdate !== -1) {
chart.data.datasets[0].data[itemToUpdate]++;
} else {
chart.data.labels.push(vote.choice);
chart.data.datasets[0].data.push(1);
}
chart.update();
}
});
const ctx = document.getElementById('chart');
const chart = new Chart(ctx, {
type: 'pie',
data: {
labels: [],
datasets: [
{
label: '# of Votes',
data: [],
backgroundColor: ['#4f46e5', '#f472b6'],
},
],
},
});
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<body>
<div style="display: flex; justify-content: center; height: 80vh">
<canvas id="chart"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script type="module">
import {
createDirectus,
realtime,
authentication,
staticToken,
} from 'https://www.unpkg.com/@directus/sdk/dist/index.js';
const url = 'https://your-directus-url/';
const access_token = 'your-access-token';
const client = createDirectus(url)
.with(staticToken(access_token))
.with(realtime());
client.connect();
client.onWebSocket('open', function () {
console.log({ event: 'onopen' });
});
client.onWebSocket('message', function (data) {
if (data.type == 'auth' && data.status == 'ok') {
client.sendMessage({
type: 'subscribe',
collection: 'votes',
query: {
aggregate: { count: 'choice' },
groupBy: ['choice'],
},
});
}
if (data.type == 'subscription' && data.event == 'init') {
for (const item of data.data) {
chart.data.labels.push(item.choice);
chart.data.datasets[0].data.push(item.count.choice);
}
chart.update();
}
if (data.type == 'subscription' && data.event == 'create') {
const vote = data.data[0];
const itemToUpdate = chart.data.labels.indexOf(vote.choice);
if (itemToUpdate !== -1) {
chart.data.datasets[0].data[itemToUpdate]++;
} else {
chart.data.labels.push(vote.choice);
chart.data.datasets[0].data.push(1);
}
chart.update();
}
});
const ctx = document.getElementById('chart');
const chart = new Chart(ctx, {
type: 'pie',
data: {
labels: [],
datasets: [
{
label: '# of Votes',
data: [],
backgroundColor: ['#4f46e5', '#f472b6'],
},
],
},
});
</script>
</body>
</html>