Improved financial sample (#6998)

* Improved financial sample
* Switch from adapter to moment
* Use data instead of cached timestamps
This commit is contained in:
Ben McCann 2020-01-26 12:35:53 -08:00 committed by Evert Timberg
parent 1ad5f369af
commit 47c7a42aae
3 changed files with 254 additions and 206 deletions

View File

@ -0,0 +1,251 @@
<!doctype html>
<html>
<head>
<title>Line Chart</title>
<script src="https://cdn.jsdelivr.net/npm/moment@2.24.0/moment.min.js"></script>
<script src="../../dist/Chart.min.js"></script>
<script src="../utils.js"></script>
<style>
canvas {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
</style>
</head>
<body>
<div style="width:1000px">
<p>This example demonstrates a time series scale with custom logic for generating minor and major ticks. Major ticks are bolded</p>
<p>For more specific functionality for financial charts, please see <a href="https://github.com/chartjs/chartjs-chart-financial">chartjs-chart-financial</a></p>
<canvas id="chart1"></canvas>
</div>
<br>
<br>
Chart Type:
<select id="type">
<option value="line">Line</option>
<option value="bar">Bar</option>
</select>
<select id="unit">
<option value="second">Second</option>
<option value="minute">Minute</option>
<option value="hour">Hour</option>
<option value="day" selected>Day</option>
<option value="month">Month</option>
<option value="year">Year</option>
</select>
<button id="update">update</button>
<script>
function isFirstUnitOfPeriod(date, unit, period) {
let first = date.clone().startOf(period);
while (first.isoWeekday() > 5) {
first.add(1, 'days');
}
if (unit === 'second' || unit === 'minute' || unit === 'hour') {
first = first.hours(9).minutes(30);
}
return date.isSame(first);
}
// Generate data between the stock market hours of 9:30am - 5pm.
// This method is slow and unoptimized, but in real life we'd be fetching it from the server.
function generateData() {
const unit = document.getElementById('unit').value;
function unitLessThanDay() {
return unit === 'second' || unit === 'minute' || unit === 'hour';
}
function beforeNineThirty(date) {
return date.hour() < 9 || (date.hour() === 9 && date.minute() < 30);
}
// Returns true if outside 9:30am-4pm on a weekday
function outsideMarketHours(date) {
if (date.isoWeekday() > 5) {
return true;
}
if (unitLessThanDay() && (beforeNineThirty(date) || date.hour() > 16)) {
return true;
}
return false;
}
function randomNumber(min, max) {
return Math.random() * (max - min) + min;
}
function randomBar(date, lastClose) {
const open = randomNumber(lastClose * 0.95, lastClose * 1.05).toFixed(2);
const close = randomNumber(open * 0.95, open * 1.05).toFixed(2);
return {
t: date.valueOf(),
y: close
};
}
let date = moment('Jan 01 1990', 'MMM DD YYYY');
const now = moment();
const data = [];
const lessThanDay = unitLessThanDay();
for (; data.length < 600 && date.isBefore(now); date = date.clone().add(1, unit).startOf(unit)) {
if (outsideMarketHours(date)) {
if (!lessThanDay || !beforeNineThirty(date)) {
date = date.clone().add(date.isoWeekday() >= 5 ? 8 - date.isoWeekday() : 1, 'day');
}
if (lessThanDay) {
date = date.hour(9).minute(30).second(0);
}
}
data.push(randomBar(date, data.length > 0 ? data[data.length - 1].y : 30));
}
return data;
}
const ctx = document.getElementById('chart1').getContext('2d');
ctx.canvas.width = 1000;
ctx.canvas.height = 300;
const color = Chart.helpers.color;
const cfg = {
data: {
datasets: [{
label: 'CHRT - Chart.js Corporation',
backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(),
borderColor: window.chartColors.red,
data: generateData(),
type: 'line',
pointRadius: 0,
fill: false,
lineTension: 0,
borderWidth: 2
}]
},
options: {
animation: {
duration: 0
},
scales: {
x: {
type: 'time',
distribution: 'series',
offset: true,
ticks: {
major: {
enabled: true,
},
fontStyle: function(context) {
return context.tick.major ? 'bold' : undefined;
},
source: 'labels', // We provided no labels. Generate no ticks. We'll make our own
autoSkip: true,
autoSkipPadding: 75,
maxRotation: 0,
sampleSize: 100
},
// Custom logic that chooses ticks from dataset timestamp by choosing first timestamp in time period
afterBuildTicks: function(scale) {
// Determine units according to our own logic
// Make sure there's at least 10 ticks generated. autoSkip will remove any extras
const units = ['second', 'minute', 'hour', 'day', 'month', 'year'];
const duration = moment.duration(moment(scale.max).diff(scale.min));
const unit = document.getElementById('unit').value;
let minorUnit = unit;
for (let i = units.indexOf(minorUnit); i < units.length; i++) {
const periods = duration.as(units[i]);
if (periods < 10) {
break;
}
minorUnit = units[i];
}
let majorUnit;
if (units.indexOf(minorUnit) !== units.length - 1) {
majorUnit = units[units.indexOf(minorUnit) + 1];
}
// Generate ticks according to our own logic
const data = scale.chart.data.datasets[0].data;
const firstDate = moment(data[0].t);
function findIndex(ts) {
// Note that we could make this faster by doing a binary search
// However, Chart.helpers.collection._lookup requires key and it's already pretty fast
let result = -1;
for (let i = 0; i < data.length; i++) {
if (data[i].t >= ts) {
result = i;
break;
}
}
if (result === 0) {
return isFirstUnitOfPeriod(firstDate, unit, minorUnit) ? 0 : 1;
}
return result;
}
// minor ticks
let start = moment(scale.min).startOf(minorUnit);
const end = moment(scale.max);
const values = new Set();
for (let date = start; date.isBefore(end); date.add(1, minorUnit)) {
const index = findIndex(+date);
if (index !== -1) {
values.add(data[index].t);
}
}
const ticks = Array.from(values, value => ({value}));
// major ticks
for (let i = 0; i < ticks.length; i++) {
if (!majorUnit || isFirstUnitOfPeriod(moment(ticks[i].value), unit, majorUnit)) {
ticks[i].major = true;
}
}
scale.ticks = ticks;
}
},
y: {
type: 'linear',
gridLines: {
drawBorder: false
},
scaleLabel: {
display: true,
labelString: 'Closing price ($)'
}
}
},
tooltips: {
intersect: false,
mode: 'index',
callbacks: {
label: function(tooltipItem, myData) {
let label = myData.datasets[tooltipItem.datasetIndex].label || '';
if (label) {
label += ': ';
}
label += parseFloat(tooltipItem.value).toFixed(2);
return label;
}
}
}
}
};
const chart = new Chart(ctx, cfg);
document.getElementById('update').addEventListener('click', function() {
const type = document.getElementById('type').value;
const dataset = chart.config.data.datasets[0];
dataset.type = type;
dataset.data = generateData();
chart.update();
});
</script>
</body>
</html>

View File

@ -124,9 +124,6 @@
}, {
title: 'Line (break on 2 day gap)',
path: 'scales/time/line-max-span.html'
}, {
title: 'Time Series',
path: 'scales/time/financial.html'
}, {
title: 'Combo',
path: 'scales/time/combo.html'
@ -242,6 +239,9 @@
}, {
title: 'Advanced',
items: [{
title: 'Custom minor and major ticks',
path: 'advanced/financial.html'
}, {
title: 'Progress bar',
path: 'advanced/progress-bar.html'
}, {

View File

@ -1,203 +0,0 @@
<!doctype html>
<html>
<head>
<title>Line Chart</title>
<script src="https://cdn.jsdelivr.net/npm/moment@2.24.0/moment.min.js"></script>
<script src="../../../dist/Chart.min.js"></script>
<script src="../../utils.js"></script>
<style>
canvas {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
</style>
</head>
<body>
<div style="width:1000px">
<p>This example demonstrates a time series scale by drawing a financial line chart using just the core library. For more specific functionality for financial charts, please see <a href="https://github.com/chartjs/chartjs-chart-financial">chartjs-chart-financial</a></p>
<canvas id="chart1"></canvas>
</div>
<br>
<br>
Chart Type:
<select id="type">
<option value="line">Line</option>
<option value="bar">Bar</option>
</select>
<select id="unit">
<option value="second">Second</option>
<option value="minute">Minute</option>
<option value="hour">Hour</option>
<option value="day" selected>Day</option>
<option value="month">Month</option>
<option value="year">Year</option>
</select>
<button id="update">update</button>
<script>
function generateData() {
var unit = document.getElementById('unit').value;
function unitLessThanDay() {
return unit === 'second' || unit === 'minute' || unit === 'hour';
}
function beforeNineThirty(date) {
return date.hour() < 9 || (date.hour() === 9 && date.minute() < 30);
}
// Returns true if outside 9:30am-4pm on a weekday
function outsideMarketHours(date) {
if (date.isoWeekday() > 5) {
return true;
}
if (unitLessThanDay() && (beforeNineThirty(date) || date.hour() > 16)) {
return true;
}
return false;
}
function randomNumber(min, max) {
return Math.random() * (max - min) + min;
}
function randomBar(date, lastClose) {
var open = randomNumber(lastClose * 0.95, lastClose * 1.05).toFixed(2);
var close = randomNumber(open * 0.95, open * 1.05).toFixed(2);
return {
t: date.valueOf(),
y: close
};
}
var date = moment('Jan 01 1990', 'MMM DD YYYY');
var now = moment();
var data = [];
var lessThanDay = unitLessThanDay();
for (; data.length < 600 && date.isBefore(now); date = date.clone().add(1, unit).startOf(unit)) {
if (outsideMarketHours(date)) {
if (!lessThanDay || !beforeNineThirty(date)) {
date = date.clone().add(date.isoWeekday() >= 5 ? 8 - date.isoWeekday() : 1, 'day');
}
if (lessThanDay) {
date = date.hour(9).minute(30).second(0);
}
}
data.push(randomBar(date, data.length > 0 ? data[data.length - 1].y : 30));
}
return data;
}
var ctx = document.getElementById('chart1').getContext('2d');
ctx.canvas.width = 1000;
ctx.canvas.height = 300;
var color = Chart.helpers.color;
var cfg = {
data: {
datasets: [{
label: 'CHRT - Chart.js Corporation',
backgroundColor: color(window.chartColors.red).alpha(0.5).rgbString(),
borderColor: window.chartColors.red,
data: generateData(),
type: 'line',
pointRadius: 0,
fill: false,
lineTension: 0,
borderWidth: 2
}]
},
options: {
animation: {
duration: 0
},
scales: {
x: {
type: 'time',
distribution: 'series',
offset: true,
ticks: {
major: {
enabled: true,
},
fontStyle: function(context) {
return context.tick.major ? 'bold' : undefined;
},
source: 'data',
autoSkip: true,
autoSkipPadding: 75,
maxRotation: 0,
sampleSize: 100
},
afterBuildTicks: function(scale) {
var majorUnit = scale._majorUnit;
var ticks = scale.ticks;
var firstTick = ticks[0];
var i, ilen, val, tick, currMajor, lastMajor;
val = moment(ticks[0].value);
if ((majorUnit === 'minute' && val.second() === 0)
|| (majorUnit === 'hour' && val.minute() === 0)
|| (majorUnit === 'day' && val.hour() === 9)
|| (majorUnit === 'month' && val.date() <= 3 && val.isoWeekday() === 1)
|| (majorUnit === 'year' && val.month() === 0)) {
firstTick.major = true;
} else {
firstTick.major = false;
}
lastMajor = val.get(majorUnit);
for (i = 1, ilen = ticks.length; i < ilen; i++) {
tick = ticks[i];
val = moment(tick.value);
currMajor = val.get(majorUnit);
tick.major = currMajor !== lastMajor;
lastMajor = currMajor;
}
}
},
y: {
type: 'linear',
gridLines: {
drawBorder: false
},
scaleLabel: {
display: true,
labelString: 'Closing price ($)'
}
}
},
tooltips: {
intersect: false,
mode: 'index',
callbacks: {
label: function(tooltipItem, myData) {
var label = myData.datasets[tooltipItem.datasetIndex].label || '';
if (label) {
label += ': ';
}
label += parseFloat(tooltipItem.value).toFixed(2);
return label;
}
}
}
}
};
var chart = new Chart(ctx, cfg);
document.getElementById('update').addEventListener('click', function() {
var type = document.getElementById('type').value;
var dataset = chart.config.data.datasets[0];
dataset.type = type;
dataset.data = generateData();
chart.update();
});
</script>
</body>
</html>