Appearance
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:
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>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:
js
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:
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><!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:
js
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:
js
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:
js
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:
js
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
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
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>