Compare commits
5 commits
aa508a43cb
...
b63e8865e3
Author | SHA1 | Date | |
---|---|---|---|
xenofem | b63e8865e3 | ||
xenofem | f21c2e2dc6 | ||
xenofem | 79663dc132 | ||
xenofem | 9595b406ca | ||
xenofem | 8aaf95c374 |
|
@ -3,6 +3,7 @@ name = "poop-graph"
|
|||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["xenofem <xenofem@xeno.science>"]
|
||||
license = "MIT"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
|
|
9
LICENSE
Normal file
9
LICENSE
Normal file
|
@ -0,0 +1,9 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2022 xenofem
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
13269
static/chart.js
Normal file
13269
static/chart.js
Normal file
File diff suppressed because it is too large
Load diff
6319
static/chartjs-adapter-date-fns.bundle.js
Normal file
6319
static/chartjs-adapter-date-fns.bundle.js
Normal file
File diff suppressed because it is too large
Load diff
606
static/chartjs-chart-error-bars.umd.js
Normal file
606
static/chartjs-chart-error-bars.umd.js
Normal 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
51
static/index.html
Normal file
|
@ -0,0 +1,51 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||
<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;
|
||||
max-width: 750px;
|
||||
}
|
||||
#controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px 20px;
|
||||
}
|
||||
</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>
|
248
static/poopGraph.js
Normal file
248
static/poopGraph.js
Normal file
|
@ -0,0 +1,248 @@
|
|||
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 = {
|
||||
aspectRatio: 1,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
min: document.getElementById('startDate').value,
|
||||
max: document.getElementById('endDate').value,
|
||||
time: {
|
||||
tooltipFormat: 'MMM d, yyyy',
|
||||
},
|
||||
},
|
||||
y: {
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Wastewater COVID RNA Signal (copies/mL)',
|
||||
},
|
||||
},
|
||||
elements: {
|
||||
pointWithErrorBar: {
|
||||
errorBarWhiskerSize: 5,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue