@eox/chart
Advanced tools
Comparing version 2.0.0-alpha1 to 2.0.0-alpha10
@@ -1,1 +0,1 @@ | ||
(function(n,e){typeof exports=="object"&&typeof module<"u"?e(exports):typeof define=="function"&&define.amd?define(["exports"],e):(n=typeof globalThis<"u"?globalThis:n||self,e(n.Interface={}))})(this,function(n){"use strict";var u=Object.defineProperty;var f=(n,e,o)=>e in n?u(n,e,{enumerable:!0,configurable:!0,writable:!0,value:o}):n[e]=o;var c=(n,e,o)=>(f(n,typeof e!="symbol"?e+"":e,o),o);const e=new MessageChannel,o=e.port1;class l{constructor(t){c(this,"iframe");this.iframe=t}setData(t){o.postMessage({type:"setData",body:{data:t}})}setSignalsData(t){o.postMessage({type:"setSignalsData",body:{data:t}})}setSignalsEndpoint(t){o.postMessage({type:"setSignalsEndpoint",body:{options:t}})}setOptions(t){o.postMessage({type:"setOptions",body:{options:t}})}getFoo(){return new Promise(t=>{const s=Date.now();o.onmessage=a=>{a.data.ts===s&&t(a.data.body)},o.postMessage({ts:s,type:"getFoo",body:"hello world"})})}}const p=r=>r?new Promise(t=>{var i;const s=document.createElement("iframe");s.style.cssText="width: 100%; height: 100%; display: block; margin: 0; border: none;",s.setAttribute("src",(i=typeof document>"u"&&typeof location>"u"?require("url").pathToFileURL(__filename).href:typeof document>"u"?location.href:document.currentScript&&document.currentScript.src||new URL("interface.umd.js",document.baseURI).href)!=null&&i.includes("localhost")?"http://localhost:5173/index.html":"https://www.unpkg.com/@eox/chart/dist/index.html"),s.setAttribute("id","EOxChart"),r==null||r.appendChild(s);let a=!1;s.onload=()=>{var d;a||((d=s.contentWindow)==null||d.postMessage("init","*",[e.port2]),a=!0,t(new l(s)))}}):(console.error("no div selected"),null);n.createChart=p,Object.defineProperty(n,Symbol.toStringTag,{value:"Module"})}); | ||
(function(n,s){typeof exports=="object"&&typeof module<"u"?s(exports):typeof define=="function"&&define.amd?define(["exports"],s):(n=typeof globalThis<"u"?globalThis:n||self,s(n.Interface={}))})(this,function(n){"use strict";var v=Object.defineProperty;var S=(n,s,o)=>s in n?v(n,s,{enumerable:!0,configurable:!0,writable:!0,value:o}):n[s]=o;var y=(n,s,o)=>(S(n,typeof s!="symbol"?s+"":s,o),o);const g={name:"@eox/chart",version:"2.0.0-alpha8",dependencies:{"chart.js":"^4.2.1","chartjs-adapter-luxon":"^1.3.1","chartjs-chart-error-bars":"^4.1.2"},devDependencies:{"@eox/eslint-config":"^1.0.0","@types/luxon":"^3.2.0","@typescript-eslint/eslint-plugin":"^5.50.0","@typescript-eslint/parser":"^5.50.0",cypress:"^12.6.0","cypress-vite":"^1.3.0","eslint-plugin-cypress":"^2.12.1","eslint-plugin-typescript":"^0.14.0",install:"^0.13.0",luxon:"^3.3.0",npm:"^9.6.3","p-retry":"^5.1.2","pre-commit":"^1.2.2","start-server-and-test":"^1.15.2",typescript:"^4.9.3",vite:"^4.0.0"},engines:{npm:">=8.0.0",node:">=18.0.0"},files:["dist","scripts","src","typings"],main:"./dist/interface.mjs",scripts:{start:"vite",build:"tsc && vite build --config vite.config.interface.ts && vite build --config vite.config.map.ts",watch:"node scripts/watch.mjs",preview:"vite preview",format:"prettier --write .",lint:"eslint --ext .js,.ts .","lint:fix":"eslint --ext .js,.ts . --fix",cypress:"start-server-and-test start 5173 'cypress open'",test:"start-server-and-test start 5173 'cypress run'"},"pre-commit":["format","lint:fix"]},d=new MessageChannel,i=d.port1;class b{constructor(e){y(this,"iframe");this.iframe=e}setData(e){i.postMessage({type:"setData",body:{data:e}})}setSignalsData(e){i.postMessage({type:"setSignalsData",body:{data:e}})}setSignalsEndpoint(e){i.postMessage({type:"setSignalsEndpoint",body:{options:e}})}setSignalsGeometry(e){i.postMessage({type:"setSignalsGeometry",body:{geometry:e}})}setOptions(e){i.postMessage({type:"setOptions",body:{options:e}})}getFoo(){return new Promise(e=>{const t=Date.now();i.onmessage=c=>{c.data.ts===t&&e(c.data.body)},i.postMessage({ts:t,type:"getFoo",body:"hello world"})})}}const w=r=>r?new Promise(e=>{var l,u;const t=document.createElement("iframe");if(t.style.cssText="width: 100%; height: 100%; display: block; margin: 0; border: none;",t.setAttribute("src",(l=typeof document>"u"&&typeof location>"u"?require("url").pathToFileURL(__filename).href:typeof document>"u"?location.href:document.currentScript&&document.currentScript.src||new URL("interface.umd.js",document.baseURI).href)!=null&&l.includes("localhost")?"http://localhost:5173/index.html":"about:blank"),t.setAttribute("id","EOxChart"),r==null||r.appendChild(t),!((u=typeof document>"u"&&typeof location>"u"?require("url").pathToFileURL(__filename).href:typeof document>"u"?location.href:document.currentScript&&document.currentScript.src||new URL("interface.umd.js",document.baseURI).href)!=null&&u.includes("localhost"))){const a=`https://www.unpkg.com/@eox/chart@${g.version}/dist`;fetch(`${a}/index.html`).then(p=>p.text()).then(p=>{var f,m,h;const x=p.replace("./assets/",`${a}/assets/`);(f=t.contentDocument)==null||f.open(),(m=t.contentDocument)==null||m.write(x),(h=t.contentDocument)==null||h.close()})}let c=!1;t.onload=()=>{var a;c||((a=t.contentWindow)==null||a.postMessage("init","*",[d.port2]),c=!0,e(new b(t)))}}):(console.error("no div selected"),null);n.createChart=w,Object.defineProperty(n,Symbol.toStringTag,{value:"Module"})}); |
{ | ||
"name": "@eox/chart", | ||
"version": "2.0.0-alpha1", | ||
"version": "2.0.0-alpha10", | ||
"dependencies": { | ||
"chart.js": "^4.2.1", | ||
"chartjs-adapter-luxon": "^1.3.1", | ||
"chartjs-chart-error-bars": "^4.1.1" | ||
"chartjs-chart-error-bars": "^4.1.2" | ||
}, | ||
@@ -31,2 +31,9 @@ "devDependencies": { | ||
}, | ||
"files": [ | ||
"dist", | ||
"scripts", | ||
"src", | ||
"typings" | ||
], | ||
"main": "./dist/interface.mjs", | ||
"scripts": { | ||
@@ -33,0 +40,0 @@ "start": "vite", |
@@ -10,1 +10,9 @@ # Chart widget | ||
``` | ||
## Contribute | ||
``` | ||
npm version <new version> | ||
npm run build | ||
npm publish (requires OTP) | ||
``` |
import { DateTime } from "luxon"; | ||
import SignalsDataManager from "./signalsDataManager"; | ||
import SignalsDataManager, { SDMOptions } from "./signalsDataManager"; | ||
@@ -9,2 +9,3 @@ export interface ChartControlOptions {} | ||
sdm: SignalsDataManager; | ||
sdmOptions: SDMOptions; | ||
options?: ChartControlOptions; | ||
@@ -15,2 +16,3 @@ | ||
sdm: SignalsDataManager, | ||
sdmOptions: SDMOptions, | ||
options?: ChartControlOptions | ||
@@ -20,13 +22,118 @@ ) { | ||
this.sdm = sdm; | ||
this.sdm.on("ready", (info) => { | ||
console.log(info); | ||
}); | ||
this.options = options; | ||
this.generateSelectionOptions(); | ||
this.sdmOptions = sdmOptions; | ||
this.generateTimeAggregationOptions(); | ||
this.addNormalizeCheckbox(); | ||
this.addShowMinMax(); | ||
this.addCSVDownload(); | ||
this.generateTimeSelectionOptions(); | ||
const spinner = document.createElement("span"); | ||
spinner.id = "loadingIndicator"; | ||
spinner.className = "loader hidden"; | ||
this.element.appendChild(spinner); | ||
} | ||
private generateSelectionOptions() { | ||
private createMessagesElements() { | ||
const messages = document.createElement("div"); | ||
messages.id = "messages"; | ||
this.element.appendChild(messages); | ||
} | ||
private removeStartEndInputs() { | ||
const start = document.getElementById("startInput"); | ||
const end = document.getElementById("endInput"); | ||
const button = document.getElementById("setTime"); | ||
if (start && end && button) { | ||
start.remove(); | ||
end.remove(); | ||
button.remove(); | ||
} | ||
} | ||
private addStartEndInputs() { | ||
const startEl = document.createElement("input"); | ||
startEl.id = "startInput"; | ||
startEl.size = 8; | ||
startEl.value = this.sdm.startTime.toISODate(); | ||
const endEl = document.createElement("input"); | ||
endEl.id = "endInput"; | ||
endEl.size = 8; | ||
endEl.value = this.sdm.endTime.toISODate(); | ||
this.element.appendChild(startEl); | ||
this.element.appendChild(endEl); | ||
var button = document.createElement("button"); | ||
button.id = "setTime"; | ||
button.textContent = "ok"; | ||
this.element.appendChild(button); | ||
button.addEventListener("click", () => { | ||
const startDate = DateTime.fromISO(startEl.value); | ||
const endDate = DateTime.fromISO(endEl.value); | ||
if (startDate.isValid && endDate.isValid) { | ||
this.sdm.setTimeInterval(startDate, endDate); | ||
this.removeStartEndInputs(); | ||
} else { | ||
startEl.className = startDate.isValid ? "" : "parsingError"; | ||
endEl.className = endDate.isValid ? "" : "parsingError"; | ||
} | ||
}); | ||
} | ||
private addNormalizeCheckbox() { | ||
var checkbox = document.createElement("input"); | ||
checkbox.type = "checkbox"; | ||
checkbox.id = "normalize"; | ||
checkbox.value = "normalize"; | ||
checkbox.checked = this.sdmOptions.normalize; | ||
var label = document.createElement("label"); | ||
label.htmlFor = "normalize"; | ||
label.appendChild(document.createTextNode("normalize")); | ||
this.element.appendChild(label); | ||
this.element.appendChild(checkbox); | ||
checkbox.addEventListener("change", () => { | ||
this.sdm.setNormalize(checkbox.checked); | ||
}); | ||
} | ||
private addShowMinMax() { | ||
var checkbox = document.createElement("input"); | ||
checkbox.type = "checkbox"; | ||
checkbox.id = "showMinMax"; | ||
checkbox.value = "showMinMax"; | ||
checkbox.checked = this.sdmOptions.showMinMax; | ||
var label = document.createElement("label"); | ||
label.htmlFor = "showMinMax"; | ||
label.appendChild(document.createTextNode("show min/max")); | ||
this.element.appendChild(label); | ||
this.element.appendChild(checkbox); | ||
checkbox.addEventListener("change", () => { | ||
this.sdm.setShowMinMax(checkbox.checked); | ||
}); | ||
} | ||
private addCSVDownload() { | ||
var button = document.createElement("button"); | ||
button.id = "csvDownload"; | ||
button.textContent = "download csv"; | ||
this.element.appendChild(button); | ||
button.addEventListener("click", () => { | ||
this.sdm.triggerCSVDownload(); | ||
}); | ||
} | ||
private generateTimeSelectionOptions() { | ||
const options = [ | ||
{ text: "3 months back", value: "months3" }, | ||
{ text: "6 months back", value: "months6" }, | ||
{ text: "1 year back", value: "year1" }, | ||
{ text: "2 years back", value: "year2" }, | ||
{ text: "3 years back", value: "year3" }, | ||
{ text: "3 months back", value: "month-3" }, | ||
{ text: "6 months back", value: "month-6" }, | ||
{ text: "1 year back", value: "year-1" }, | ||
{ text: "2 years back", value: "year-2" }, | ||
{ text: "3 years back", value: "year-3" }, | ||
{ text: "custom", value: "custom" }, | ||
]; | ||
@@ -43,21 +150,60 @@ const selectEl = document.createElement("select"); | ||
const currDate = DateTime.now(); | ||
switch ((evt.target as HTMLSelectElement).value) { | ||
case "months3": | ||
this.sdm.setTimeInterval(currDate.minus({ months: 3 }), currDate); | ||
const [type, val] = (evt.target as HTMLSelectElement).value.split("-"); | ||
this.removeStartEndInputs(); | ||
switch (type) { | ||
case "month": | ||
this.sdm.setTimeInterval( | ||
currDate.minus({ month: Number(val) }), | ||
currDate | ||
); | ||
break; | ||
case "months6": | ||
this.sdm.setTimeInterval(currDate.minus({ months: 6 }), currDate); | ||
case "year": | ||
this.sdm.setTimeInterval( | ||
currDate.minus({ year: Number(val) }), | ||
currDate | ||
); | ||
break; | ||
case "year1": | ||
this.sdm.setTimeInterval(currDate.minus({ years: 1 }), currDate); | ||
case "custom": | ||
this.addStartEndInputs(); | ||
break; | ||
case "year2": | ||
this.sdm.setTimeInterval(currDate.minus({ years: 2 }), currDate); | ||
} | ||
}); | ||
} | ||
private generateTimeAggregationOptions() { | ||
const options = [ | ||
{ text: "no aggregation", value: "none" }, | ||
{ text: "1 week", value: "week-1" }, | ||
{ text: "2 weeks", value: "week-2" }, | ||
{ text: "1 month", value: "month-1" }, | ||
{ text: "3 months", value: "month-3" }, | ||
{ text: "6 months", value: "month-6" }, | ||
]; | ||
const selectEl = document.createElement("select"); | ||
options.forEach((entry) => { | ||
const option = document.createElement("option"); | ||
option.text = entry.text; | ||
option.value = entry.value; | ||
selectEl.appendChild(option); | ||
}); | ||
this.element.appendChild(selectEl); | ||
selectEl.addEventListener("change", (evt) => { | ||
const [type, val] = (evt.target as HTMLSelectElement).value.split("-"); | ||
switch (type) { | ||
case "none": | ||
this.sdm.setTimeAggregation(null); | ||
break; | ||
case "year3": | ||
this.sdm.setTimeInterval(currDate.minus({ years: 3 }), currDate); | ||
case "week": | ||
this.sdm.setTimeAggregation({ week: Number(val) }); | ||
break; | ||
case "month": | ||
this.sdm.setTimeAggregation({ month: Number(val) }); | ||
break; | ||
case "year": | ||
this.sdm.setTimeAggregation({ year: Number(val) }); | ||
break; | ||
} | ||
}); | ||
const spinner = document.createElement("span"); | ||
spinner.id = "loadingIndicator"; | ||
spinner.className = "loader hidden"; | ||
@@ -64,0 +210,0 @@ this.element.appendChild(spinner); |
@@ -0,1 +1,3 @@ | ||
import pkg from "../package.json"; | ||
const channel = new MessageChannel(); | ||
@@ -24,5 +26,9 @@ const port1 = channel.port1; | ||
endTime?: string; | ||
colors?: string[]; | ||
}) { | ||
port1.postMessage({ type: "setSignalsEndpoint", body: { options } }); | ||
} | ||
setSignalsGeometry(geometry: object) { | ||
port1.postMessage({ type: "setSignalsGeometry", body: { geometry } }); | ||
} | ||
setOptions(options: object) { | ||
@@ -57,6 +63,19 @@ port1.postMessage({ type: "setOptions", body: { options } }); | ||
? "http://localhost:5173/index.html" | ||
: "https://www.unpkg.com/@eox/chart/dist/index.html" | ||
: "about:blank" | ||
); | ||
iframe.setAttribute("id", "EOxChart"); | ||
div?.appendChild(iframe); | ||
if (!import.meta?.url?.includes("localhost")) { | ||
const hostUrl = `https://www.unpkg.com/@eox/chart@${pkg.version}/dist`; | ||
fetch(`${hostUrl}/index.html`) | ||
.then((response) => { | ||
return response.text(); | ||
}) | ||
.then((text) => { | ||
const html = text.replace("./assets/", `${hostUrl}/assets/`); | ||
iframe.contentDocument?.open(); | ||
iframe.contentDocument?.write(html); | ||
iframe.contentDocument?.close(); | ||
}); | ||
} | ||
let iframeLoaded = false; | ||
@@ -63,0 +82,0 @@ iframe.onload = () => { |
@@ -13,5 +13,30 @@ import Chart, { ChartDataset } from "chart.js/auto"; | ||
}, | ||
plugins: [ | ||
{ | ||
id: "selectionLineHighlight", | ||
afterDraw: (chart) => { | ||
if (chart && chart.tooltip) { | ||
const active = chart.tooltip.getActiveElements(); | ||
if (active?.length) { | ||
let x = active[0].element.x; | ||
let yAxis = chart.scales.y; | ||
let ctx = chart.ctx; | ||
ctx.save(); | ||
ctx.beginPath(); | ||
ctx.moveTo(x, yAxis.top); | ||
ctx.lineTo(x, yAxis.bottom); | ||
ctx.lineWidth = 1; | ||
ctx.strokeStyle = "rgba(0, 0, 255, 0.4)"; | ||
ctx.stroke(); | ||
ctx.restore(); | ||
} | ||
} | ||
}, | ||
}, | ||
], | ||
} | ||
); | ||
let sdmInstance: SignalsDataManager | null = null; | ||
let application: MessagePort; | ||
@@ -43,2 +68,5 @@ | ||
break; | ||
case "setSignalsGeometry": | ||
setSignalsGeometry(event.data.body.options); | ||
break; | ||
case "setSignalsData": | ||
@@ -76,5 +104,11 @@ eoxchart.data = { | ||
}) => { | ||
const sdm = new SignalsDataManager(eoxchart, options); | ||
new ChartControls(document.getElementById("controls"), sdm); | ||
sdm.setActiveFields(options.active); | ||
sdmInstance = new SignalsDataManager(eoxchart, options); | ||
new ChartControls(document.getElementById("controls"), sdmInstance, options); | ||
sdmInstance.setActiveFields(options.active); | ||
}; | ||
const setSignalsGeometry = (geometry: object) => { | ||
if (sdmInstance) { | ||
sdmInstance.setGeometry(geometry); | ||
} | ||
}; |
import Chart, { | ||
ChartDataset /*, LinearScale, CategoryScale */, | ||
ChartDataset, | ||
LinearScale, | ||
CategoryScale, | ||
ChartOptions, | ||
@@ -7,3 +9,3 @@ } from "chart.js/auto"; | ||
import { DateTime } from "luxon"; | ||
/* | ||
import { | ||
@@ -17,2 +19,4 @@ LineWithErrorBarsController, | ||
import { EventEmitter } from "events"; | ||
Chart.register( | ||
@@ -26,3 +30,3 @@ LineWithErrorBarsController, | ||
CategoryScale | ||
);*/ | ||
); | ||
@@ -45,2 +49,7 @@ type status = "ready" | "loading" | "error"; | ||
interface yAxisObject { | ||
id: "string"; | ||
containedSignals: string[]; | ||
} | ||
export interface SDMOptions { | ||
@@ -54,8 +63,15 @@ source: string; | ||
endTime?: string; | ||
timeAggregation?: object; | ||
retries?: number; | ||
normalize?: boolean; | ||
showMinMax?: boolean; | ||
colors?: string[]; | ||
additionalYAxis?: yAxisObject[]; | ||
} | ||
class SignalsDataManager { | ||
class SignalsDataManager extends EventEmitter { | ||
chart: Chart; | ||
startTime: DateTime; | ||
endTime: DateTime; | ||
timeAggregation: object; | ||
options: SDMOptions; | ||
@@ -66,4 +82,6 @@ chartOptions: ChartOptions; | ||
activeFields: string[]; | ||
additionalYAxis: yAxisObject[] | null; | ||
constructor(chart: Chart, options: SDMOptions) { | ||
super(); | ||
this.chart = chart; | ||
@@ -78,2 +96,15 @@ this.options = options; | ||
} | ||
if (this.options.timeAggregation) { | ||
this.timeAggregation = this.options.timeAggregation; | ||
} | ||
this.options.retries = this.options.retries ? this.options.retries : 5; | ||
this.options.normalize = this.options.normalize | ||
? this.options.normalize | ||
: false; | ||
this.options.showMinMax = this.options.showMinMax | ||
? this.options.showMinMax | ||
: false; | ||
this.additionalYAxis = this.options.additionalYAxis | ||
? this.options.additionalYAxis | ||
: null; | ||
this.dataStorage = {}; | ||
@@ -94,9 +125,16 @@ // Initialize data storage | ||
}, | ||
/*y: { | ||
min: 0, | ||
max: 0.5, | ||
},*/ | ||
}, | ||
interaction: { | ||
axis: "x", | ||
mode: "nearest", | ||
intersect: false, | ||
}, | ||
plugins: { | ||
legend: { | ||
labels: { | ||
usePointStyle: true, | ||
pointStyle: "rect", | ||
padding: 5, | ||
sort: (a, b) => a.text.localeCompare(b.text), | ||
}, | ||
position: "right", | ||
@@ -114,6 +152,43 @@ onClick: (_, legendItem) => { | ||
}, | ||
tooltip: { | ||
callbacks: { | ||
title: (tooltipItem) => { | ||
const raw: any = tooltipItem[0]?.raw; | ||
return raw.x.toISODate(); | ||
}, | ||
label: (tooltipItem) => { | ||
const raw: any = tooltipItem.raw; | ||
return `${tooltipItem.dataset.label}: ↔ ${raw.y.toPrecision( | ||
3 | ||
)}; ↓ ${raw.yMin.toPrecision(3)}, ↑ ${raw.yMax.toPrecision(3)}`; | ||
}, | ||
}, | ||
}, | ||
}, | ||
}; | ||
// Adding possible additional y axis scales | ||
for (let index = 0; index < this.additionalYAxis.length; index++) { | ||
const scale = this.additionalYAxis[index]; | ||
this.chartOptions.scales[scale.id] = { | ||
type: "linear", | ||
position: "right", | ||
}; | ||
} | ||
this.emit("ready", "someinfo"); | ||
} | ||
private checkLoadingStatus() { | ||
const spinner = document.getElementById("loadingIndicator"); | ||
let loading = false; | ||
// TODO: Showing when error state happened | ||
Object.keys(this.dataStorage).forEach((_, groupIdx) => { | ||
Object.keys(this.dataStorage[groupIdx]).forEach((timeKey) => { | ||
if (this.dataStorage[groupIdx][timeKey].status === "fetching") { | ||
loading = true; | ||
} | ||
}); | ||
}); | ||
spinner.className = loading ? "loader" : "loader hidden"; | ||
} | ||
private fetchSignals(groupIndex: number, start?: DateTime, end?: DateTime) { | ||
@@ -152,13 +227,16 @@ const features = this.options.features[groupIndex] | ||
); | ||
throw new Error(); | ||
}, | ||
retries: 5, | ||
retries: this.options.retries, | ||
}).then(function (res) { | ||
return res.json(); | ||
}); | ||
this.checkLoadingStatus(); | ||
} | ||
private updateChart() { | ||
this.checkLoadingStatus(); | ||
this.chart.options = this.chartOptions; | ||
const datasets: ChartDataset[] = []; | ||
this.options.features.flat().forEach((key) => { | ||
this.options.features.flat().forEach((key, dsIndex) => { | ||
// Find group | ||
@@ -189,21 +267,145 @@ let groupIndex: number = -1; | ||
}); | ||
datasets.push({ | ||
// type: "lineWithErrorBars", | ||
let actualDataAdded = false; | ||
let max = Number.MIN_VALUE; | ||
let min = Number.MAX_VALUE; | ||
let signalData = <any>data.map((datapoint) => { | ||
let ds = {}; | ||
if (datapoint && datapoint.date !== "missing") { | ||
const stats = datapoint.basicStats; | ||
if (this.options.showMinMax) { | ||
min = stats.min < min ? stats.min : min; | ||
max = stats.max > max ? stats.max : max; | ||
} else { | ||
min = stats.mean < min ? stats.mean : min; | ||
max = stats.mean > max ? stats.mean : max; | ||
} | ||
ds = { | ||
x: DateTime.fromISO(datapoint.date).setZone("UTC"), | ||
y: datapoint.basicStats.mean, | ||
yMin: datapoint.basicStats.min, | ||
yMax: datapoint.basicStats.max, | ||
}; | ||
actualDataAdded = true; | ||
} | ||
return ds; | ||
}); | ||
if (actualDataAdded && this.timeAggregation) { | ||
// If we aggregate data we recalculate min max so we reset it | ||
max = Number.MIN_VALUE; | ||
min = Number.MAX_VALUE; | ||
const aggrData = []; | ||
let currDataIdx = 0; | ||
let currDate = DateTime.fromObject({ | ||
year: this.startTime.year, | ||
month: this.startTime.month, | ||
day: this.startTime.day, | ||
}); | ||
while (currDate < this.endTime) { | ||
let dataSum = 0; | ||
let minSum = 0; | ||
let maxSum = 0; | ||
while ( | ||
currDataIdx < signalData.length && | ||
(Object.keys(signalData[currDataIdx]).length === 0 || | ||
signalData[currDataIdx].x < currDate) | ||
) { | ||
currDataIdx++; | ||
} | ||
const x1 = currDataIdx; | ||
while ( | ||
currDataIdx < signalData.length && | ||
(Object.keys(signalData[currDataIdx]).length === 0 || | ||
signalData[currDataIdx].x < currDate.plus(this.timeAggregation)) | ||
) { | ||
const sd = signalData[currDataIdx]; | ||
dataSum += sd.y; | ||
minSum += sd.yMin; | ||
maxSum += sd.yMax; | ||
currDataIdx++; | ||
} | ||
const x2 = currDataIdx; | ||
const count = x2 - x1; | ||
// If datapoints are within the interval we push the aggregation to the aggregated data | ||
if (count > 0) { | ||
const yMean = dataSum / count; | ||
const yMinMean = minSum / count; | ||
const yMaxMean = maxSum / count; | ||
if (this.options.showMinMax) { | ||
min = yMinMean < min ? yMinMean : min; | ||
max = yMaxMean > max ? yMaxMean : max; | ||
} else { | ||
min = yMean < min ? yMean : min; | ||
max = yMean > max ? yMean : max; | ||
} | ||
// TODO: can we save a time interval here? | ||
aggrData.push({ | ||
x: currDate, | ||
y: yMean, | ||
yMin: yMinMean, | ||
yMax: yMaxMean, | ||
}); | ||
} | ||
currDate = currDate.plus(this.timeAggregation); | ||
} | ||
signalData = aggrData; | ||
} | ||
if (actualDataAdded && this.options.normalize) { | ||
signalData = signalData.map( | ||
(dp: { x: DateTime; y: number; yMin: number; yMax: number }) => { | ||
dp.y = (dp.y - min) / (max - min); | ||
dp.yMin = (dp.yMin - min) / (max - min); | ||
dp.yMax = (dp.yMax - min) / (max - min); | ||
return dp; | ||
} | ||
); | ||
} | ||
let ds: ChartDataset = { | ||
type: "line", | ||
label: key, | ||
// casting to any because an array of object should also be possible | ||
data: <any>data.map((datapoint) => { | ||
let ds = {}; | ||
if (datapoint && datapoint.date !== "missing") { | ||
ds = { | ||
x: DateTime.fromISO(datapoint.date).setZone("UTC"), | ||
y: datapoint.basicStats.mean, | ||
yMin: [datapoint.basicStats.min], | ||
yMax: [datapoint.basicStats.max], | ||
}; | ||
data: signalData, | ||
hidden: !this.activeFields.includes(key), | ||
}; | ||
if (this.options.showMinMax) { | ||
ds = { | ||
type: "lineWithErrorBars", | ||
label: key, | ||
data: signalData, | ||
hidden: !this.activeFields.includes(key), | ||
errorBarWhiskerLineWidth: 2, | ||
errorBarColor: "#00000000", | ||
}; | ||
} | ||
if (this.options.colors && this.options.colors.length >= dsIndex) { | ||
const color = this.options.colors[dsIndex]; | ||
ds.backgroundColor = color; | ||
ds.borderColor = color; | ||
if (this.options.showMinMax) { | ||
ds = { | ||
type: "lineWithErrorBars", | ||
label: key, | ||
data: signalData, | ||
hidden: !this.activeFields.includes(key), | ||
errorBarWhiskerLineWidth: 2, | ||
errorBarColor: "#00000000", | ||
errorBarWhiskerColor: color, | ||
errorBarWhiskerSize: 10, | ||
backgroundColor: color, | ||
borderColor: color, | ||
}; | ||
} | ||
} | ||
// Check if additional y axis should be used | ||
if (this.additionalYAxis !== null) { | ||
for (let index = 0; index < this.additionalYAxis.length; index++) { | ||
const axis = this.additionalYAxis[index]; | ||
if (axis.containedSignals.includes(key)) { | ||
ds.yAxisID = axis.id; | ||
} | ||
return ds; | ||
}), | ||
hidden: !this.activeFields.includes(key), | ||
}); | ||
} | ||
} | ||
datasets.push(ds); | ||
} | ||
@@ -259,7 +461,12 @@ }); | ||
.minus({ second: 1 }) | ||
).then((data) => { | ||
this.dataStorage[index][timeslot].data = data; | ||
this.dataStorage[index][timeslot].status = "finished"; | ||
this.updateChart(); | ||
}); | ||
) | ||
.then((data) => { | ||
this.dataStorage[index][timeslot].data = data; | ||
this.dataStorage[index][timeslot].status = "finished"; | ||
this.updateChart(); | ||
}) | ||
.catch(() => { | ||
this.dataStorage[index][timeslot].status = "failed"; | ||
this.updateChart(); | ||
}); | ||
this.dataStorage[index][timeslot] = { | ||
@@ -275,2 +482,97 @@ request, | ||
triggerCSVDownload() { | ||
const completeData: { | ||
date: string; | ||
basicStats?: { mean: number; min: number; max: number }; | ||
}[][] = []; | ||
const headers: string[] = []; | ||
const allSignals = this.activeFields; | ||
allSignals.forEach((key, signalIdx) => { | ||
// Find group | ||
let groupIndex: number = -1; | ||
this.options.features.forEach((group, idx) => { | ||
if (group.includes(key)) { | ||
groupIndex = idx; | ||
} | ||
}); | ||
if (groupIndex !== -1) { | ||
let data: { | ||
date: string; | ||
basicStats?: { mean: number; min: number; max: number }; | ||
}[] = []; | ||
Object.keys(this.dataStorage[groupIndex]).forEach((timekey) => { | ||
const dsEntry = this.dataStorage[groupIndex][timekey]; | ||
if (dsEntry.status === "finished") { | ||
if ( | ||
Object.keys(dsEntry.data).length !== 0 && | ||
Object.keys(dsEntry.data).includes(key) | ||
) { | ||
data.push(...dsEntry.data[key]); | ||
} | ||
} | ||
}); | ||
if (data.length > 0) { | ||
headers.push(allSignals[signalIdx]); | ||
completeData.push(data); | ||
} | ||
} | ||
}); | ||
// We need to try and bring the different timed datasets into "same timegrid" lets do some binning | ||
const binnedData: { | ||
[key: string]: { | ||
[key: string]: { | ||
mean: number; | ||
max: number; | ||
min: number; | ||
}; | ||
}; | ||
} = {}; | ||
headers.forEach((datakey, idx) => { | ||
completeData[idx].forEach((dp) => { | ||
if (binnedData[dp.date]) { | ||
binnedData[dp.date][datakey] = dp.basicStats; | ||
} else { | ||
binnedData[dp.date] = {}; | ||
binnedData[dp.date][datakey] = dp.basicStats; | ||
} | ||
}); | ||
}); | ||
// Now we try to construct the csv adding empty values when timestamp not available for a dataset | ||
let csv = "time,"; | ||
const allHeaders = headers.map((val) => [val, `${val}_max`, `${val}_min`]); | ||
csv += allHeaders.flat().join(","); | ||
csv += "\n"; | ||
Object.keys(binnedData).forEach((dateKey) => { | ||
const row: (string | number)[] = [dateKey]; | ||
headers.forEach((signalKey) => { | ||
const stats = binnedData[dateKey][signalKey] | ||
? binnedData[dateKey][signalKey] | ||
: null; | ||
if (stats != null) { | ||
row.push(...[stats.mean, stats.max, stats.min]); | ||
} else { | ||
row.push(...["", "", ""]); | ||
} | ||
}); | ||
csv += row.join(","); | ||
csv += "\n"; | ||
}); | ||
var hiddenElement = document.createElement("a"); | ||
hiddenElement.href = "data:text/csv;charset=utf-8," + encodeURI(csv); | ||
hiddenElement.target = "_blank"; | ||
hiddenElement.download = "chart_data.csv"; | ||
hiddenElement.click(); | ||
} | ||
setGeometry(geometry: object) { | ||
// Clear all currently saved data | ||
this.dataStorage = {}; | ||
// Initialize data storage | ||
this.options.features.forEach((_, idx) => (this.dataStorage[idx] = {})); | ||
this.options.geometry = geometry; | ||
this.retrieveMissingData(); | ||
this.updateChart(); | ||
} | ||
setActiveFields(activeFields: string[]) { | ||
@@ -290,2 +592,24 @@ this.activeFields = activeFields; | ||
} | ||
setNormalize(normalize: boolean) { | ||
this.options.normalize = normalize; | ||
this.updateChart(); | ||
} | ||
setShowMinMax(showMinMax: boolean) { | ||
this.options.showMinMax = showMinMax; | ||
this.updateChart(); | ||
} | ||
setTimeAggregation( | ||
aggregation: { | ||
day?: number; | ||
week?: number; | ||
month?: number; | ||
year?: number; | ||
} | null | ||
) { | ||
this.timeAggregation = aggregation; | ||
this.updateChart(); | ||
} | ||
/* | ||
@@ -292,0 +616,0 @@ setAggregation(aggregation: string) {} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
339343
3
18
14
2281
5