Skip to content

Instantly share code, notes, and snippets.

@kyle-wendling
Last active August 16, 2022 19:22
Show Gist options
  • Save kyle-wendling/c511cc027d21ac461fadb7048bc135e7 to your computer and use it in GitHub Desktop.
Save kyle-wendling/c511cc027d21ac461fadb7048bc135e7 to your computer and use it in GitHub Desktop.
D3 Real Time Chart
//uses D3.js v7 https://d3js.org/d3.v7.min.js
//uses https://cdn.rawgit.com/jwagner/simplex-noise.js/master/simplex-noise.js
let time = 0;
const noise = new SimplexNoise();
const seed = 200 + 100 * Math.random();
let chart = {};
chart.make = function (set) {
if (!set) set = {};
let off = 20; //offset or padding scalar
this.refresh = set.refresh;
this.slide_window = set.slide_window || 300;
this.h = set.height || window.innerHeight - (off);
this.w = set.width || window.innerWidth - off;
console.log(this.h, this.w);
this.x_range = d3.scaleLinear().range([0, this.w - off * 2]);
this.y_range = d3.scaleLinear().range([this.h - off * 2, 0]);
this.xAxis = d3.axisBottom(this.x_range).
tickSizeInner(-this.h + off * 2).
tickSizeOuter(0).
tickPadding(off / 2);
this.yAxis = d3.axisLeft(this.y_range).
tickSizeInner(-this.w + off * 2).
tickSizeOuter(0).
tickPadding(off / 2);
this.line = d3.line().
x((d, i) => this.x_range(i + time - chart.slide_window)).
y(d => this.y_range(d));
this.sel = set.sel || "#real-time-chart";
this.svg = d3.select(this.sel).append('svg').
//.attr({width: w, height: h})
attr("width", this.w).
attr("height", this.h).
append('g').
attr('transform', 'translate(30, 20)');
this.x_svg = chart.svg.append('g').
attr('class', 'x axis').
attr('transform', `translate(0, ${this.h - off * 2})`).
call(this.xAxis);
this.y_svg = chart.svg.append('g').
attr('class', 'y axis').
call(this.yAxis);
this.data_series_svg = chart.svg.append('path').
attr('class', 'line data');
for (const s in set.series) { //make a new line for each data series
this[set.series[s].key] = chart.svg.append('path').attr('class', 'line ' + set.series[s].key);
};
this.average_50_svg = chart.svg.append('path').
attr('class', 'line average-50');
this.average_25_svg = chart.svg.append('path').
attr('class', 'line average-25');
this.rects_svg = chart.svg.selectAll('rect').
data(d3.range(chart.slide_window)).
enter().
append('rect').
attr('width', (this.w - off * 2) / chart.slide_window).
attr('x', (d, i) => i * (this.w - off * 2) / chart.slide_window);
this.legend_txt = [];
Object.values(set.series).filter(s => s.type !== 'bar').forEach(s => { //all series that are NOT type bar
this.legend_txt.push([s.name, s.color]);
});
chart.legend_svg = chart.svg.append('g').
attr('transform', `translate(20, 20)`).
selectAll('g').
data(this.legend_txt).
enter().
append('g');
chart.legend_svg.
append('circle').
attr('fill', d => d[1]).
attr('r', 5).
attr('cx', 0).
attr('cy', (d, i) => i * 15);
chart.legend_svg.
append('text').
text(d => d[0]).
attr('transform', (d, i) => `translate(10, ${i * 15 + 4})`);
};
let sim_tick = function (series) {
time++;
let ndt = {};
let ms = series['main'].data;
let avg_50 = series['average_50'].data;
let avg_25 = series['average_25'].data;
let deltas = series['deltas'].data;
ndt.main = Math.max(ms[ms.length - 1] + noise.noise2D(seed, time / 2), 0); //add nice movement, stay above 0
if (time <= 50) {//50 item trailing average
let a = 0;
for (let j = 0; j < ms.length; j++) {
a += ms[j];
}
a /= ms.length;
ndt.average_50 = a;
//return;
} else {
let a = avg_50[avg_50.length - 1] * 50 - ms[ms.length - 50];
a += ndt.main;
a /= 50;
ndt.average_50 = a;
}
if (time <= 25) { //25 item trailing average
let a = 0;
for (let j = 0; j < ms.length; j++) {
a += ms[j];
}
a /= ms.length;
ndt.average_25 = a;
} else {
let a = avg_25[avg_25.length - 1] * 25 - ms[ms.length - 25];
a += ndt.main;
a /= 25;
ndt.average_25 = a;
}
ndt.deltas = (ms[ms.length - 1] - ms[ms.length - 2]);
for (const s in series) {
if (time > chart.slide_window) {
series[s].data.shift(); //if out of space in window, start popping off oldest
}
series[s].data.push(ndt[series[s].key]); //and add newest
};
};
chart.update = function (ds) { //ds = data series
this.x_range.domain([time - chart.slide_window, time]);
//set scale for y axis
const main_s = ds['main']; //redo
let yDom = d3.extent(main_s.data);
yDom[0] = Math.max(yDom[0] - 1, 0);
yDom[1] += 1;
this.y_range.domain(yDom);
this.x_svg.
call(this.xAxis);
this.y_svg.
call(this.yAxis);
//draw each series - line chart
for (s in ds) {
if (ds.type !== 'bar' && ds.type !== 'deltas') {
this[ds[s].key].
datum(ds[s].data).
attr('d', this.line);
}
}
//draw bars
const bar_series = ds['deltas'];
if (bar_series) {
this.rects_svg.
attr('height', (_, i) => Math.abs(bar_series.data[i] * this.h / 10)).
attr('fill', (_, i) => bar_series.data[i] < 0 ? bar_series.color[0] : bar_series.color[1]).
attr('y', (_, i) => this.h - Math.abs(bar_series.data[i] * this.h / 10) - 42);
}
};
chart.simulate = function () {
//fill some initial data
for (let i = 0; i < chart.slide_window + 50; i++) {
sim_tick(series);
}
chart.update(series);
// create a new 'tick' of simulated data and plot on the chart
setInterval(() => { //infinite loop + delay
sim_tick(series);
chart.update(series);
}, chart.refresh);
}
if(window) window.chart = chart;
// let series = { //each data series we want to plot
// main: {
// name: 'Data Series 1',
// key: 'main',
// color: '#f1f',
// data: [seed]
// },
// average_25: {
// name: 'Trailing Average - 25',
// key: 'average_25',
// color: '#ff0',
// data: [0]
// },
// average_50: {
// name: 'Trailing Average - 50',
// key: 'average_50',
// color: '#0ff',
// data: [0]
// },
// // x_axis: {
// // key: 'x_axis',
// // type: 'x_axis',
// // color: '#0ff',
// // data: [0]
// // },
// deltas: {
// name: 'deltas',
// key: 'deltas',
// color: ['red', 'green'],
// type: 'bar',
// data: [seed]
// }
// };
// chart.make({
// slide_window: 300, //data sliding window
// series: series,
// refresh: 100,
// // height: 600,
// // width: 600
// });
// chart.simulate();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment