Last active
August 16, 2022 19:22
-
-
Save kyle-wendling/c511cc027d21ac461fadb7048bc135e7 to your computer and use it in GitHub Desktop.
D3 Real Time Chart
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//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