This commit is contained in:
xenofem 2022-04-07 05:36:56 -04:00
parent aa508a43cb
commit 8aaf95c374
5 changed files with 20479 additions and 0 deletions

13269
static/chart.js Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,606 @@
/**
* chartjs-chart-error-bars
* https://github.com/sgratzl/chartjs-chart-error-bars
*
* Copyright (c) 2021 Samuel Gratzl <samu@sgratzl.com>
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('chart.js'), require('chart.js/helpers')) :
typeof define === 'function' && define.amd ? define(['exports', 'chart.js', 'chart.js/helpers'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.ChartErrorBars = {}, global.Chart, global.Chart.helpers));
})(this, (function (exports, chart_js, helpers) { 'use strict';
const allModelKeys = ['xMin', 'xMax', 'yMin', 'yMax'];
function modelKeys(horizontal) {
return (horizontal ? allModelKeys.slice(0, 2) : allModelKeys.slice(2));
}
function calculateScale(properties, data, index, scale, reset) {
const keys = [`${scale.axis}Min`, `${scale.axis}Max`];
const base = scale.getBasePixel();
for (const key of keys) {
const v = data[key];
if (Array.isArray(v)) {
properties[key] = v.map((d) => (reset ? base : scale.getPixelForValue(d, index)));
}
else if (typeof v === 'number') {
properties[key] = reset ? base : scale.getPixelForValue(v, index);
}
}
}
function calculatePolarScale(properties, data, scale, reset, options) {
const animationOpts = options.animation;
const keys = [`${scale.axis}Min`, `${scale.axis}Max`];
const toAngle = (v) => {
const valueRadius = scale.getDistanceFromCenterForValue(v);
const resetRadius = animationOpts.animateScale ? 0 : valueRadius;
return reset ? resetRadius : valueRadius;
};
for (const key of keys) {
const v = data[key];
if (Array.isArray(v)) {
properties[key] = v.map(toAngle);
}
else if (typeof v === 'number') {
properties[key] = toAngle(v);
}
}
}
const errorBarDefaults = {
errorBarLineWidth: { v: [1, 3] },
errorBarColor: { v: ['#2c2c2c', '#1f1f1f'] },
errorBarWhiskerLineWidth: { v: [1, 3] },
errorBarWhiskerRatio: { v: [0.2, 0.25] },
errorBarWhiskerSize: { v: [20, 24] },
errorBarWhiskerColor: { v: ['#2c2c2c', '#1f1f1f'] },
};
const errorBarDescriptors = {
_scriptable: true,
_indexable: (name) => name !== 'v',
};
const styleKeys = Object.keys(errorBarDefaults);
function resolveMulti(vMin, vMax) {
const vMinArr = Array.isArray(vMin) ? vMin : [vMin];
const vMaxArr = Array.isArray(vMax) ? vMax : [vMax];
if (vMinArr.length === vMaxArr.length) {
return vMinArr.map((v, i) => [v, vMaxArr[i]]);
}
const max = Math.max(vMinArr.length, vMaxArr.length);
return Array(max).map((_, i) => [vMinArr[i % vMinArr.length], vMaxArr[i % vMaxArr.length]]);
}
function resolveOption(val, index) {
if (typeof val === 'string' || typeof val === 'number') {
return val;
}
const v = Array.isArray(val) ? val : val.v;
return v[index % v.length];
}
function calculateHalfSize(total, options, i) {
const ratio = resolveOption(options.errorBarWhiskerRatio, i);
if (total != null && ratio > 0) {
return total * ratio * 0.5;
}
const size = resolveOption(options.errorBarWhiskerSize, i);
return size * 0.5;
}
function drawErrorBarVertical(props, vMin, vMax, options, ctx) {
ctx.save();
ctx.translate(props.x, 0);
const bars = resolveMulti(vMin == null ? props.y : vMin, vMax == null ? props.y : vMax);
bars.reverse().forEach(([mi, ma], j) => {
const i = bars.length - j - 1;
const halfWidth = calculateHalfSize(props.width, options, i);
ctx.lineWidth = resolveOption(options.errorBarLineWidth, i);
ctx.strokeStyle = resolveOption(options.errorBarColor, i);
ctx.beginPath();
ctx.moveTo(0, mi);
ctx.lineTo(0, ma);
ctx.stroke();
ctx.lineWidth = resolveOption(options.errorBarWhiskerLineWidth, i);
ctx.strokeStyle = resolveOption(options.errorBarWhiskerColor, i);
ctx.beginPath();
ctx.moveTo(-halfWidth, mi);
ctx.lineTo(halfWidth, mi);
ctx.moveTo(-halfWidth, ma);
ctx.lineTo(halfWidth, ma);
ctx.stroke();
});
ctx.restore();
}
function drawErrorBarHorizontal(props, vMin, vMax, options, ctx) {
ctx.save();
ctx.translate(0, props.y);
const bars = resolveMulti(vMin == null ? props.x : vMin, vMax == null ? props.x : vMax);
bars.reverse().forEach(([mi, ma], j) => {
const i = bars.length - j - 1;
const halfHeight = calculateHalfSize(props.height, options, i);
ctx.lineWidth = resolveOption(options.errorBarLineWidth, i);
ctx.strokeStyle = resolveOption(options.errorBarColor, i);
ctx.beginPath();
ctx.moveTo(mi, 0);
ctx.lineTo(ma, 0);
ctx.stroke();
ctx.lineWidth = resolveOption(options.errorBarWhiskerLineWidth, i);
ctx.strokeStyle = resolveOption(options.errorBarWhiskerColor, i);
ctx.beginPath();
ctx.moveTo(mi, -halfHeight);
ctx.lineTo(mi, halfHeight);
ctx.moveTo(ma, -halfHeight);
ctx.lineTo(ma, halfHeight);
ctx.stroke();
});
ctx.restore();
}
function renderErrorBar(elem, ctx) {
var _a, _b, _c, _d;
const props = elem.getProps(['x', 'y', 'width', 'height', 'xMin', 'xMax', 'yMin', 'yMax']);
if (props.xMin != null || props.xMax != null) {
drawErrorBarHorizontal(props, (_a = props.xMin) !== null && _a !== void 0 ? _a : null, (_b = props.xMax) !== null && _b !== void 0 ? _b : null, elem.options, ctx);
}
if (props.yMin != null || props.yMax != null) {
drawErrorBarVertical(props, (_c = props.yMin) !== null && _c !== void 0 ? _c : null, (_d = props.yMax) !== null && _d !== void 0 ? _d : null, elem.options, ctx);
}
}
function drawErrorBarArc(props, vMin, vMax, options, ctx) {
ctx.save();
ctx.translate(props.x, props.y);
const angle = (props.startAngle + props.endAngle) / 2;
const cosAngle = Math.cos(angle);
const sinAngle = Math.sin(angle);
const v = {
x: -sinAngle,
y: cosAngle,
};
const length = Math.sqrt(v.x * v.x + v.y * v.y);
v.x /= length;
v.y /= length;
const bars = resolveMulti(vMin !== null && vMin !== void 0 ? vMin : props.outerRadius, vMax !== null && vMax !== void 0 ? vMax : props.outerRadius);
bars.reverse().forEach(([mi, ma], j) => {
const i = bars.length - j - 1;
const minCos = mi * cosAngle;
const minSin = mi * sinAngle;
const maxCos = ma * cosAngle;
const maxSin = ma * sinAngle;
const halfHeight = calculateHalfSize(null, options, i);
const eX = v.x * halfHeight;
const eY = v.y * halfHeight;
ctx.lineWidth = resolveOption(options.errorBarLineWidth, i);
ctx.strokeStyle = resolveOption(options.errorBarColor, i);
ctx.beginPath();
ctx.moveTo(minCos, minSin);
ctx.lineTo(maxCos, maxSin);
ctx.stroke();
ctx.lineWidth = resolveOption(options.errorBarWhiskerLineWidth, i);
ctx.strokeStyle = resolveOption(options.errorBarWhiskerColor, i);
ctx.beginPath();
ctx.moveTo(minCos + eX, minSin + eY);
ctx.lineTo(minCos - eX, minSin - eY);
ctx.moveTo(maxCos + eX, maxSin + eY);
ctx.lineTo(maxCos - eX, maxSin - eY);
ctx.stroke();
});
ctx.restore();
}
function renderErrorBarArc(elem, ctx) {
const props = elem.getProps(['x', 'y', 'startAngle', 'endAngle', 'rMin', 'rMax', 'outerRadius']);
if (props.rMin != null || props.rMax != null) {
drawErrorBarArc(props, props.rMin, props.rMax, elem.options, ctx);
}
}
class BarWithErrorBar extends chart_js.BarElement {
draw(ctx) {
super.draw(ctx);
renderErrorBar(this, ctx);
}
}
BarWithErrorBar.id = 'barWithErrorBar';
BarWithErrorBar.defaults = { ...chart_js.BarElement.defaults, ...errorBarDefaults };
BarWithErrorBar.defaultRoutes = chart_js.BarElement.defaultRoutes;
BarWithErrorBar.descriptors = errorBarDescriptors;
class PointWithErrorBar extends chart_js.PointElement {
draw(ctx, area) {
super.draw.call(this, ctx, area);
renderErrorBar(this, ctx);
}
}
PointWithErrorBar.id = 'pointWithErrorBar';
PointWithErrorBar.defaults = { ...chart_js.PointElement.defaults, ...errorBarDefaults };
PointWithErrorBar.defaultRoutes = chart_js.PointElement.defaultRoutes;
PointWithErrorBar.descriptors = errorBarDescriptors;
class ArcWithErrorBar extends chart_js.ArcElement {
draw(ctx) {
super.draw(ctx);
renderErrorBarArc(this, ctx);
}
}
ArcWithErrorBar.id = 'arcWithErrorBar';
ArcWithErrorBar.defaults = { ...chart_js.ArcElement.defaults, ...errorBarDefaults };
ArcWithErrorBar.defaultRoutes = chart_js.ArcElement.defaultRoutes;
ArcWithErrorBar.descriptors = errorBarDescriptors;
function reverseOrder(v) {
return Array.isArray(v) ? v.slice().reverse() : v;
}
function generateBarTooltip(item) {
const keys = modelKeys(item.element.horizontal);
const base = chart_js.Tooltip.defaults.callbacks.label.call(this, item);
const v = item.chart.data.datasets[item.datasetIndex].data[item.dataIndex];
if (v == null || keys.every((k) => v[k] == null)) {
return base;
}
return `${base} (${reverseOrder(v[keys[0]])} .. ${v[keys[1]]})`;
}
function generateTooltipScatter(item) {
const v = item.chart.data.datasets[item.datasetIndex].data[item.dataIndex];
const subLabel = (base, horizontal) => {
const keys = modelKeys(horizontal);
if (v == null || keys.every((k) => v[k] == null)) {
return base;
}
return `${base} [${reverseOrder(v[keys[0]])} .. ${v[keys[1]]}]`;
};
return `(${subLabel(item.label, true)}, ${subLabel(item.parsed.y, false)})`;
}
function generateTooltipPolar(item) {
const base = chart_js.PolarAreaController.overrides.plugins.tooltip.callbacks.label.call(this, item);
const v = item.chart.data.datasets[item.datasetIndex].data[item.dataIndex];
const keys = ['rMin', 'rMax'];
if (v == null || keys.every((k) => v[k] == null)) {
return base;
}
return `${base} [${reverseOrder(v[keys[0]])} .. ${v[keys[1]]}]`;
}
const interpolators = {
color(from, to, factor) {
const f = from || 'transparent';
const t = to || 'transparent';
if (f === t) {
return to;
}
const c0 = helpers.color(f);
const c1 = c0.valid && helpers.color(t);
return c1 && c1.valid ? c1.mix(c0, factor).hexString() : to;
},
number(from, to, factor) {
if (from === to) {
return to;
}
return from + (to - from) * factor;
},
};
function interpolateArrayOption(from, to, factor, type, interpolator) {
if (typeof from === type && typeof to === type) {
return interpolator(from, to, factor);
}
if (Array.isArray(from) && Array.isArray(to)) {
return from.map((f, i) => interpolator(f, to[i], factor));
}
const isV = (t) => t && Array.isArray(t.v);
if (isV(from) && isV(to)) {
return { v: from.v.map((f, i) => interpolator(f, to.v[i], factor)) };
}
return to;
}
function interpolateNumberOptionArray(from, to, factor) {
return interpolateArrayOption(from, to, factor, 'number', interpolators.number);
}
function interpolateColorOptionArray(from, to, factor) {
return interpolateArrayOption(from, to, factor, 'string', interpolators.color);
}
const animationHints = {
animations: {
numberArray: {
fn: interpolateNumberOptionArray,
properties: allModelKeys.concat(styleKeys.filter((d) => !d.endsWith('Color')), ['rMin', 'rMax']),
},
colorArray: {
fn: interpolateColorOptionArray,
properties: styleKeys.filter((d) => d.endsWith('Color')),
},
},
};
function getMinMax(scale, superMethod) {
const { axis } = scale;
scale.axis = `${axis}MinMin`;
const { min } = superMethod(scale);
scale.axis = `${axis}MaxMax`;
const { max } = superMethod(scale);
scale.axis = axis;
return { min, max };
}
function computeExtrema(v, vm, op) {
if (Array.isArray(vm)) {
return op(...vm);
}
if (typeof vm === 'number') {
return vm;
}
return v;
}
function parseErrorNumberData(parsed, scale, data, start, count) {
const axis = typeof scale === 'string' ? scale : scale.axis;
const vMin = `${axis}Min`;
const vMax = `${axis}Max`;
const vMinMin = `${axis}MinMin`;
const vMaxMax = `${axis}MaxMax`;
for (let i = 0; i < count; i += 1) {
const index = i + start;
const p = parsed[i];
p[vMin] = data[index][vMin];
p[vMax] = data[index][vMax];
p[vMinMin] = computeExtrema(p[axis], p[vMin], Math.min);
p[vMaxMax] = computeExtrema(p[axis], p[vMax], Math.max);
}
}
function parseErrorLabelData(parsed, scale, start, count) {
const { axis } = scale;
const labels = scale.getLabels();
for (let i = 0; i < count; i += 1) {
const index = i + start;
const p = parsed[i];
p[axis] = scale.parse(labels[index], index);
}
}
function patchController(type, config, controller, elements = [], scales = []) {
chart_js.registry.addControllers(controller);
if (Array.isArray(elements)) {
chart_js.registry.addElements(...elements);
}
else {
chart_js.registry.addElements(elements);
}
if (Array.isArray(scales)) {
chart_js.registry.addScales(...scales);
}
else {
chart_js.registry.addScales(scales);
}
const c = config;
c.type = type;
return c;
}
class BarWithErrorBarsController extends chart_js.BarController {
getMinMax(scale, canStack) {
return getMinMax(scale, (patchedScale) => super.getMinMax(patchedScale, canStack));
}
parseObjectData(meta, data, start, count) {
const parsed = super.parseObjectData(meta, data, start, count);
parseErrorNumberData(parsed, meta.vScale, data, start, count);
parseErrorLabelData(parsed, meta.iScale, start, count);
return parsed;
}
updateElement(element, index, properties, mode) {
if (typeof index === 'number') {
calculateScale(properties, this.getParsed(index), index, this._cachedMeta.vScale, mode === 'reset');
}
super.updateElement(element, index, properties, mode);
}
}
BarWithErrorBarsController.id = 'barWithErrorBars';
BarWithErrorBarsController.defaults = helpers.merge({}, [
chart_js.BarController.defaults,
animationHints,
{
dataElementType: BarWithErrorBar.id,
},
]);
BarWithErrorBarsController.overrides = helpers.merge({}, [
chart_js.BarController.overrides,
{
plugins: {
tooltip: {
callbacks: {
label: generateBarTooltip,
},
},
},
},
]);
BarWithErrorBarsController.defaultRoutes = chart_js.BarController.defaultRoutes;
class BarWithErrorBarsChart extends chart_js.Chart {
constructor(item, config) {
super(item, patchController('barWithErrorBars', config, BarWithErrorBarsController, BarWithErrorBar, [
chart_js.LinearScale,
chart_js.CategoryScale,
]));
}
}
BarWithErrorBarsChart.id = BarWithErrorBarsController.id;
class LineWithErrorBarsController extends chart_js.LineController {
getMinMax(scale, canStack) {
return getMinMax(scale, (patchedScale) => super.getMinMax(patchedScale, canStack));
}
parseObjectData(meta, data, start, count) {
const parsed = super.parseObjectData(meta, data, start, count);
parseErrorNumberData(parsed, meta.vScale, data, start, count);
parseErrorLabelData(parsed, meta.iScale, start, count);
return parsed;
}
updateElement(element, index, properties, mode) {
if (element instanceof PointWithErrorBar && typeof index === 'number') {
calculateScale(properties, this.getParsed(index), index, this._cachedMeta.vScale, mode === 'reset');
}
super.updateElement(element, index, properties, mode);
}
}
LineWithErrorBarsController.id = 'lineWithErrorBars';
LineWithErrorBarsController.defaults = helpers.merge({}, [
chart_js.LineController.defaults,
animationHints,
{
dataElementType: PointWithErrorBar.id,
},
]);
LineWithErrorBarsController.overrides = helpers.merge({}, [
chart_js.LineController.overrides,
{
plugins: {
tooltip: {
callbacks: {
label: generateBarTooltip,
},
},
},
},
]);
LineWithErrorBarsController.defaultRoutes = chart_js.LineController.defaultRoutes;
class LineWithErrorBarsChart extends chart_js.Chart {
constructor(item, config) {
super(item, patchController('lineWithErrorBars', config, LineWithErrorBarsController, PointWithErrorBar, [
chart_js.LinearScale,
chart_js.CategoryScale,
]));
}
}
LineWithErrorBarsChart.id = LineWithErrorBarsController.id;
class ScatterWithErrorBarsController extends chart_js.ScatterController {
getMinMax(scale, canStack) {
return getMinMax(scale, (patchedScale) => super.getMinMax(patchedScale, canStack));
}
parseObjectData(meta, data, start, count) {
const parsed = super.parseObjectData(meta, data, start, count);
parseErrorNumberData(parsed, meta.xScale, data, start, count);
parseErrorNumberData(parsed, meta.yScale, data, start, count);
return parsed;
}
updateElement(element, index, properties, mode) {
if (element instanceof PointWithErrorBar && typeof index === 'number') {
calculateScale(properties, this.getParsed(index), index, this._cachedMeta.xScale, mode === 'reset');
calculateScale(properties, this.getParsed(index), index, this._cachedMeta.yScale, mode === 'reset');
}
super.updateElement(element, index, properties, mode);
}
}
ScatterWithErrorBarsController.id = 'scatterWithErrorBars';
ScatterWithErrorBarsController.defaults = helpers.merge({}, [
chart_js.ScatterController.defaults,
animationHints,
{
dataElementType: PointWithErrorBar.id,
},
]);
ScatterWithErrorBarsController.overrides = helpers.merge({}, [
chart_js.ScatterController.overrides,
{
plugins: {
tooltip: {
callbacks: {
label: generateTooltipScatter,
},
},
},
},
]);
ScatterWithErrorBarsController.defaultRoutes = chart_js.LineController.defaultRoutes;
class ScatterWithErrorBarsChart extends chart_js.Chart {
constructor(item, config) {
super(item, patchController('scatterWithErrorBars', config, ScatterWithErrorBarsController, PointWithErrorBar, [chart_js.LinearScale]));
}
}
ScatterWithErrorBarsChart.id = ScatterWithErrorBarsController.id;
class PolarAreaWithErrorBarsController extends chart_js.PolarAreaController {
getMinMax(scale, canStack) {
return getMinMax(scale, (patchedScale) => super.getMinMax(patchedScale, canStack));
}
countVisibleElements() {
const meta = this._cachedMeta;
return meta.data.reduce((acc, _, index) => {
if (!Number.isNaN(meta._parsed[index].r) && this.chart.getDataVisibility(index)) {
return acc + 1;
}
return acc;
}, 0);
}
parseObjectData(meta, data, start, count) {
const parsed = new Array(count);
const scale = meta.rScale;
for (let i = 0; i < count; i += 1) {
const index = i + start;
const item = data[index];
const v = scale.parse(item[scale.axis], index);
parsed[i] = {
[scale.axis]: v,
};
}
parseErrorNumberData(parsed, scale, data, start, count);
return parsed;
}
updateElement(element, index, properties, mode) {
if (typeof index === 'number') {
calculatePolarScale(properties, this.getParsed(index), this._cachedMeta.rScale, mode === 'reset', this.chart.options);
}
super.updateElement(element, index, properties, mode);
}
updateElements(arcs, start, count, mode) {
const scale = this.chart.scales.r;
const bak = scale.getDistanceFromCenterForValue;
scale.getDistanceFromCenterForValue = function getDistanceFromCenterForValue(v) {
if (typeof v === 'number') {
return bak.call(this, v);
}
return bak.call(this, v.r);
};
super.updateElements(arcs, start, count, mode);
scale.getDistanceFromCenterForValue = bak;
}
}
PolarAreaWithErrorBarsController.id = 'polarAreaWithErrorBars';
PolarAreaWithErrorBarsController.defaults = helpers.merge({}, [
chart_js.PolarAreaController.defaults,
animationHints,
{
dataElementType: ArcWithErrorBar.id,
},
]);
PolarAreaWithErrorBarsController.overrides = helpers.merge({}, [
chart_js.PolarAreaController.overrides,
{
plugins: {
tooltip: {
callbacks: {
label: generateTooltipPolar,
},
},
},
},
]);
PolarAreaWithErrorBarsController.defaultRoutes = chart_js.PolarAreaController.defaultRoutes;
class PolarAreaWithErrorBarsChart extends chart_js.Chart {
constructor(item, config) {
super(item, patchController('polarAreaWithErrorBars', config, PolarAreaWithErrorBarsController, ArcWithErrorBar, [
chart_js.RadialLinearScale,
]));
}
}
PolarAreaWithErrorBarsChart.id = PolarAreaWithErrorBarsController.id;
chart_js.registry.addControllers(BarWithErrorBarsController, LineWithErrorBarsController, PolarAreaWithErrorBarsController, ScatterWithErrorBarsController);
chart_js.registry.addElements(BarWithErrorBar, ArcWithErrorBar, PointWithErrorBar);
exports.ArcWithErrorBar = ArcWithErrorBar;
exports.BarWithErrorBar = BarWithErrorBar;
exports.BarWithErrorBarsChart = BarWithErrorBarsChart;
exports.BarWithErrorBarsController = BarWithErrorBarsController;
exports.LineWithErrorBarsChart = LineWithErrorBarsChart;
exports.LineWithErrorBarsController = LineWithErrorBarsController;
exports.PointWithErrorBar = PointWithErrorBar;
exports.PolarAreaWithErrorBarsChart = PolarAreaWithErrorBarsChart;
exports.PolarAreaWithErrorBarsController = PolarAreaWithErrorBarsController;
exports.ScatterWithErrorBarsChart = ScatterWithErrorBarsChart;
exports.ScatterWithErrorBarsController = ScatterWithErrorBarsController;
Object.defineProperty(exports, '__esModule', { value: true });
}));
//# sourceMappingURL=index.umd.js.map

51
static/index.html Normal file
View file

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<script src="chart.js"></script>
<script src="chartjs-adapter-date-fns.bundle.js"></script>
<script src="chartjs-chart-error-bars.umd.js"></script>
<style>
body {
text-align: center;
}
div.chart {
margin: 20px auto;
position: relative;
height: 80vh;
width: 80vw;
}
label {
padding-left: 10px;
padding-right: 10px;
}
</style>
<title>💩📈</title>
</head>
<body>
<h1>💩📈</h1>
<div id="controls">
<label>
Start date:
<input id="startDate" type="date">
</label>
<label>
End date:
<input id="endDate" type="date">
</label>
</div>
<div class="chart">
<canvas id="northCanvas"></canvas>
</div>
<div class="chart">
<canvas id="southCanvas"></canvas>
</div>
<script src="poopGraph.js"></script>
<div>
Download data: <a href="/data.csv">CSV</a> <a href="/data.json">JSON</a>
</div>
<div>
<a href="https://www.mwra.com/biobot/biobotdata.htm">Source: Massachusetts Water Resources Authority</a>
</div>
</body>
</html>

240
static/poopGraph.js Normal file
View file

@ -0,0 +1,240 @@
fetch('/data.json')
.then(resp => resp.json())
.then(plot)
.catch(err => {
console.log(err);
});
function average(window) {
let y_sum = 0;
let count = 0;
for (const sample of window) {
if (sample['y'] !== undefined) {
y_sum += sample['y'];
count += 1;
}
}
if (count === 0) {
return undefined;
}
return y_sum / count;
}
function localLinearRegression(slopeWindowSize, averageWindowSize) {
return (window) => {
let x_sum = 0;
let y_sum = 0;
let count = 0;
for (const sample of window.slice(-slopeWindowSize)) {
if (sample['y'] !== undefined) {
x_sum += sample['x'];
y_sum += sample['y'];
count += 1;
}
}
if (count === 0) {
return undefined;
}
const x_avg = x_sum / count;
const y_avg = y_sum / count;
let numerator = 0;
let denominator = 0;
for (const sample of window) {
if (sample['y'] !== undefined) {
const x_err = sample['x'] - x_avg;
const y_err = sample['y'] - y_avg;
numerator += x_err * y_err;
denominator += x_err * x_err;
}
}
if (denominator === 0) {
return window.at(-1)['y'];
}
const slope = numerator / denominator;
let short_x_sum = 0;
let short_y_sum = 0;
let short_count = 0;
for (const sample of window.slice(-averageWindowSize)) {
if (sample['y'] !== undefined) {
short_x_sum += sample['x'];
short_y_sum += sample['y'];
short_count += 1;
}
}
if (short_count === 0) {
return y_avg + (window.at(-1)['x'] - x_avg) * slope;
}
const short_x_avg = short_x_sum / short_count;
const short_y_avg = short_y_sum / short_count;
return short_y_avg + (window.at(-1)['x'] - short_x_avg) * slope;
};
}
function smooth(data, column, windowSize, smoothFunc) {
const smoothed = [];
const window = [];
for (const row of data) {
window.push({ x: new Date(row['x']).getTime(), y: row[column] });
if (window.length > windowSize) {
window.shift();
}
smoothed.push({ x: row['x'], y: smoothFunc(window) });
}
return smoothed;
}
function extractWithErrorBars(data, region) {
const extracted = [];
for (const row of data['rows']) {
const y = row[region + ' (copies/mL)'];
if (y !== undefined) {
extracted.push({
x: row['Date'],
y: y,
yMin: y - (row[region + ' Low Confidence Interval'] || 0),
yMax: y + (row[region + ' High Confidence Interval'] || 0),
});
}
}
return extracted;
}
function plot(data) {
const northData = extractWithErrorBars(data, 'Northern');
const southData = extractWithErrorBars(data, 'Southern');
const northCtx = document.getElementById('northCanvas');
const southCtx = document.getElementById('southCanvas');
let options = {
maintainAspectRatio: false,
interaction: {
intersect: false,
},
scales: {
x: {
type: 'timeseries',
min: document.getElementById('startDate').value,
max: document.getElementById('endDate').value,
},
y: {
min: 0,
},
},
plugins: {
title: {
display: true,
text: 'Wastewater COVID RNA Signal (copies/mL)',
},
},
};
const northChart = new Chart(northCtx, {
data: {
datasets: [
{
type: 'scatterWithErrorBars',
label: 'North System',
data: northData,
backgroundColor: 'rgba(0,0,255,0.5)',
color: 'rgba(0,0,255,0.5)',
borderColor: 'rgba(0,0,255,0.5)',
spanGaps: true,
},
/* {
type: 'line',
label: 'smoothing 18/14',
data: smooth(northData, 'y', 18, localLinearRegression(18, 14)),
pointRadius: 0,
borderColor: 'rgba(0,0,255,0.6)',
},
{
type: 'line',
label: 'smoothing 14/14',
data: smooth(northData, 'y', 14, localLinearRegression(14, 14)),
pointRadius: 0,
borderColor: 'rgba(255,0,255,0.6)',
},*/
{
type: 'line',
label: 'North System 7-day average (low)',
data: smooth(northData, 'yMin', 7, average),
pointRadius: 0,
borderColor: 'rgba(0,255,0,0.6)',
fill: '+1',
backgroundColor: 'rgba(0,255,0,0.2)',
},
{
type: 'line',
label: 'North System 7-day average',
data: smooth(northData, 'y', 7, average),
pointRadius: 0,
borderColor: 'rgba(0,128,0,0.9)',
backgroundColor: 'rgba(0,128,0,0.9)',
},
{
type: 'line',
label: 'North System 7-day average (high)',
data: smooth(northData, 'yMax', 7, average),
pointRadius: 0,
borderColor: 'rgba(0,255,0,0.6)',
fill: '-1',
backgroundColor: 'rgba(0,255,0,0.2)',
},
]
},
options,
});
const southChart = new Chart(southCtx, {
data: {
datasets: [
{
type: 'scatterWithErrorBars',
label: 'South System',
data: southData,
backgroundColor: 'rgba(255,0,0,0.5)',
color: 'rgba(255,0,0,0.5)',
borderColor: 'rgba(255,0,0,0.5)',
spanGaps: true,
},
{
type: 'line',
label: 'South System 7-day average (low)',
data: smooth(southData, 'yMin', 7, average),
pointRadius: 0,
borderColor: 'rgba(255,127,0,0.6)',
fill: '+1',
backgroundColor: 'rgba(255,127,0,0.2)',
},
{
type: 'line',
label: 'South System 7-day average',
data: smooth(southData, 'y', 7, average),
pointRadius: 0,
borderColor: 'rgba(127,63,0,0.9)',
backgroundColor: 'rgba(127,63,0,0.9)',
},
{
type: 'line',
label: 'South System 7-day average (high)',
data: smooth(southData, 'yMax', 7, average),
pointRadius: 0,
borderColor: 'rgba(255,127,0,0.6)',
fill: '-1',
backgroundColor: 'rgba(255,127,0,0.2)',
},
],
},
options,
});
document.getElementById('startDate').addEventListener('input', (e) => {
options.scales.x.min = e.target.value;
northChart.update();
southChart.update();
});
document.getElementById('endDate').addEventListener('input', (e) => {
options.scales.x.max = e.target.value;
northChart.update();
southChart.update();
});
}