Initial web components
This commit is contained in:
parent
bfaa701f18
commit
5f80f9e816
6 changed files with 407 additions and 1 deletions
82
elements/heatmap.js
Normal file
82
elements/heatmap.js
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import { withComponent, props } from 'skatejs';
|
||||||
|
import { Timeseries } from '../lib/timeseries';
|
||||||
|
|
||||||
|
const Component = withComponent();
|
||||||
|
|
||||||
|
class Heatmap extends Component {
|
||||||
|
static props = {
|
||||||
|
timeseries: props.string,
|
||||||
|
days: props.number({
|
||||||
|
default: 7,
|
||||||
|
}),
|
||||||
|
interpolate: props.boolean,
|
||||||
|
accumulate: props.boolean,
|
||||||
|
};
|
||||||
|
connected() {
|
||||||
|
this.enableFetch = true;
|
||||||
|
}
|
||||||
|
renderer(renderRoot, render) {
|
||||||
|
const root = renderRoot;
|
||||||
|
root.innerHtml = '';
|
||||||
|
root.appendChild(render());
|
||||||
|
}
|
||||||
|
|
||||||
|
render({ timeseries, interpolate, accumulate, days }) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
if (!this.enableFetch) {
|
||||||
|
// Not yet connected
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
if (this.ts) {
|
||||||
|
// We're re-rendering, cancel previous
|
||||||
|
this.ts.canceled = true;
|
||||||
|
}
|
||||||
|
const ts = new Timeseries(timeseries, new Date(), days);
|
||||||
|
const data = [{
|
||||||
|
x: ts.getSlotLabels(),
|
||||||
|
y: ts.getDayLabels(),
|
||||||
|
z: ts.prepareSlots(),
|
||||||
|
type: 'heatmap',
|
||||||
|
colorscale: [
|
||||||
|
['0.0', 'rgb(0, 0, 0)'],
|
||||||
|
['0.9', 'rgb(255, 0, 0)'],
|
||||||
|
['1.0', 'rgb(128, 0, 0)'],
|
||||||
|
],
|
||||||
|
showlegend: false,
|
||||||
|
showscale: false,
|
||||||
|
}];
|
||||||
|
const layout = {
|
||||||
|
yaxis: {
|
||||||
|
autorange: 'reversed',
|
||||||
|
tickfont: {
|
||||||
|
family: 'Source Code Pro',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
type: 'category',
|
||||||
|
tickfont: {
|
||||||
|
family: 'Source Code Pro',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
paper_bgcolor: 'transparent',
|
||||||
|
};
|
||||||
|
this.ts = ts;
|
||||||
|
ts.getData({
|
||||||
|
interpolate,
|
||||||
|
accumulate,
|
||||||
|
})
|
||||||
|
.then((values) => {
|
||||||
|
if (ts.canceled) {
|
||||||
|
el.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
data[0].z = values;
|
||||||
|
Plotly.newPlot(el, data, layout, {
|
||||||
|
staticPlot: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('cbase-heatmap', Heatmap);
|
108
elements/polar.js
Normal file
108
elements/polar.js
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
import { withComponent, props } from 'skatejs';
|
||||||
|
import { Timeseries } from '../lib/timeseries';
|
||||||
|
|
||||||
|
const Component = withComponent();
|
||||||
|
|
||||||
|
class Polar extends Component {
|
||||||
|
static props = {
|
||||||
|
timeseries: props.string,
|
||||||
|
days: props.number({
|
||||||
|
default: 7,
|
||||||
|
}),
|
||||||
|
interpolate: props.boolean,
|
||||||
|
accumulate: props.boolean,
|
||||||
|
percentage: props.boolean,
|
||||||
|
};
|
||||||
|
|
||||||
|
connected() {
|
||||||
|
this.enableFetch = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer(renderRoot, render) {
|
||||||
|
const root = renderRoot;
|
||||||
|
root.innerHtml = '';
|
||||||
|
root.appendChild(render());
|
||||||
|
}
|
||||||
|
|
||||||
|
render({ timeseries, interpolate, accumulate, percentage, days }) {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
if (!this.enableFetch) {
|
||||||
|
// Not yet connected
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
if (this.ts) {
|
||||||
|
// We're re-rendering, cancel previous
|
||||||
|
this.ts.canceled = true;
|
||||||
|
}
|
||||||
|
const ts = new Timeseries(timeseries, new Date(), days);
|
||||||
|
const layout = {
|
||||||
|
orientation: 270,
|
||||||
|
direction: 'clockwise',
|
||||||
|
angularaxis: {
|
||||||
|
type: 'category',
|
||||||
|
tickcolor: '#204a87',
|
||||||
|
},
|
||||||
|
radialaxis: {
|
||||||
|
ticksuffix: percentage ? '%' : '',
|
||||||
|
tickcolor: '#204a87',
|
||||||
|
},
|
||||||
|
font: {
|
||||||
|
family: ['Source Code Pro', 'sans-serif'],
|
||||||
|
size: 16,
|
||||||
|
color: '#fff',
|
||||||
|
outlineColor: 'transparent',
|
||||||
|
},
|
||||||
|
showlegend: false,
|
||||||
|
paper_bgcolor: 'transparent',
|
||||||
|
};
|
||||||
|
let data = [];
|
||||||
|
const dayLabels = ts.getDayLabels();
|
||||||
|
ts.prepareSlots().forEach((day, dayIdx) => {
|
||||||
|
data.push({
|
||||||
|
r: day,
|
||||||
|
t: ts.getSlotLabels(),
|
||||||
|
name: dayLabels[dayIdx],
|
||||||
|
type: 'area',
|
||||||
|
marker: {
|
||||||
|
color: '#f57900',
|
||||||
|
},
|
||||||
|
opacity: 0.8,
|
||||||
|
showlegend: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.ts = ts;
|
||||||
|
ts.getData({
|
||||||
|
interpolate,
|
||||||
|
accumulate,
|
||||||
|
usePreviousValue: false,
|
||||||
|
})
|
||||||
|
.then((values) => {
|
||||||
|
if (ts.canceled) {
|
||||||
|
el.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (percentage) {
|
||||||
|
data = data.slice(0);
|
||||||
|
const result = ts.getSlotLabels().map(() => 0);
|
||||||
|
values.forEach((day) => {
|
||||||
|
day.forEach((val, idx) => {
|
||||||
|
result[idx] += val;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
data[0].r = result.map(r => (r / ts.days) * 100);
|
||||||
|
} else {
|
||||||
|
values.forEach((day, dayIdx) => {
|
||||||
|
day.forEach((val, idx) => {
|
||||||
|
data[dayIdx].r[idx] = val;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Plotly.newPlot(el, data, layout, {
|
||||||
|
staticPlot: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define('cbase-polar', Polar);
|
7
index.js
Normal file
7
index.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import heatmap from './elements/heatmap';
|
||||||
|
import polar from './elements/polar';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
heatmap,
|
||||||
|
polar,
|
||||||
|
};
|
176
lib/timeseries.js
Normal file
176
lib/timeseries.js
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
export class Timeseries {
|
||||||
|
constructor(id, endDate, days = 7, slots = 24) {
|
||||||
|
this.id = id;
|
||||||
|
this.endDate = endDate;
|
||||||
|
this.startDate = this.getStartDate(endDate, days);
|
||||||
|
this.days = this.getDays();
|
||||||
|
this.slots = slots;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStartDate(endDate, days) {
|
||||||
|
const startDate = new Date();
|
||||||
|
startDate.setDate(startDate.getDate() - days);
|
||||||
|
startDate.setHours(0, 0, 0);
|
||||||
|
return startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDays() {
|
||||||
|
return Math.ceil((this.endDate - this.startDate) / (1000 * 60 * 60 * 24));
|
||||||
|
}
|
||||||
|
|
||||||
|
prepareSlots(defaultValue = 0) {
|
||||||
|
const timeSlots = [];
|
||||||
|
for (let i = 0; i < this.days; i++) {
|
||||||
|
const daySlot = [];
|
||||||
|
for (let ii = 0; ii < this.slots; ii++) {
|
||||||
|
daySlot.push(defaultValue);
|
||||||
|
}
|
||||||
|
timeSlots.push(daySlot);
|
||||||
|
}
|
||||||
|
return timeSlots;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDayLabels() {
|
||||||
|
const weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||||
|
const labels = [];
|
||||||
|
const day = new Date(this.startDate.getTime());
|
||||||
|
for (let i = 0; i < this.days; i++) {
|
||||||
|
labels.push(`${day.getDate()} ${weekDays[day.getDay()]}`);
|
||||||
|
day.setHours(day.getHours() + 24);
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSlotLabels() {
|
||||||
|
const labels = [];
|
||||||
|
const day = new Date(this.startDate.getTime());
|
||||||
|
const pad = (time) => {
|
||||||
|
let val = `${time}`;
|
||||||
|
if (val.length === 1) {
|
||||||
|
val = `0${time}`;
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
};
|
||||||
|
|
||||||
|
day.setHours(0, 0, 0);
|
||||||
|
for (let i = 0; i < this.slots; i++) {
|
||||||
|
const from = day.getHours();
|
||||||
|
day.setHours(day.getHours() + (24 / this.slots));
|
||||||
|
let to = day.getHours();
|
||||||
|
if (to === 0) {
|
||||||
|
to = 24;
|
||||||
|
}
|
||||||
|
if (this.slots === 24) {
|
||||||
|
labels.push(`${pad(from)}`);
|
||||||
|
} else {
|
||||||
|
labels.push(`${pad(from)}-${pad(to)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseValue(val) {
|
||||||
|
if (typeof val === 'boolean') {
|
||||||
|
if (val) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return parseFloat(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
flattenData(data, fromDay = 0, fromHour = 0) {
|
||||||
|
const flattened = [];
|
||||||
|
data.forEach((dayData, dayIdx) => {
|
||||||
|
if (dayIdx < fromDay) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dayData.forEach((point, hourIdx) => {
|
||||||
|
if (dayIdx === fromDay && hourIdx <= fromHour) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
flattened.push(point);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return flattened;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapData(data, options) {
|
||||||
|
const slots = this.prepareSlots(null);
|
||||||
|
data.forEach((point) => {
|
||||||
|
const pointDate = new Date(point.timestamp);
|
||||||
|
const pointValue = this.parseValue(point.value);
|
||||||
|
const daySlot = Math.floor((pointDate - this.startDate) / (1000 * 60 * 60 * 24));
|
||||||
|
const timeSlot = Math.floor(pointDate.getHours() / (24 / this.slots));
|
||||||
|
if (slots[daySlot][timeSlot] === null) {
|
||||||
|
slots[daySlot][timeSlot] = 0;
|
||||||
|
}
|
||||||
|
if (options.accumulate) {
|
||||||
|
slots[daySlot][timeSlot] += pointValue;
|
||||||
|
} else {
|
||||||
|
slots[daySlot][timeSlot] = Math.max(pointValue, slots[daySlot][timeSlot]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Deal with slots without data
|
||||||
|
let prevVal = 0;
|
||||||
|
slots.forEach((dayData, dayIdx) => {
|
||||||
|
let prevValIdx = 0;
|
||||||
|
const dayVals = dayData;
|
||||||
|
dayVals.forEach((val, idx) => {
|
||||||
|
if (val !== null) {
|
||||||
|
prevVal = val;
|
||||||
|
prevValIdx = idx;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (options.interpolate) {
|
||||||
|
const flattened = this.flattenData(slots, dayIdx, idx);
|
||||||
|
for (let nextSlot = 0; nextSlot < flattened.length; nextSlot++) {
|
||||||
|
if (flattened[nextSlot] === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const nextVal = flattened[nextSlot];
|
||||||
|
if (nextVal === prevVal) {
|
||||||
|
dayVals[idx] = prevVal;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let difference = 0;
|
||||||
|
if (nextVal > prevVal) {
|
||||||
|
difference = nextVal - prevVal;
|
||||||
|
} else {
|
||||||
|
difference = prevVal - nextVal;
|
||||||
|
}
|
||||||
|
const distance = nextSlot;
|
||||||
|
let totalDistance = (nextSlot + idx) - prevValIdx;
|
||||||
|
if (totalDistance < 1) {
|
||||||
|
totalDistance = 1;
|
||||||
|
}
|
||||||
|
dayVals[idx] = Math.max(((difference / totalDistance) * distance) + nextVal, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dayVals[idx] = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (options.usePreviousValue) {
|
||||||
|
dayVals[idx] = prevVal;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dayVals[idx] = 0;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return slots;
|
||||||
|
}
|
||||||
|
|
||||||
|
getUrl() {
|
||||||
|
return `http://openmct.cbrp3.c-base.org/telemetry/${this.id}?start=${this.startDate.getTime()}&end=${this.endDate.getTime()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getData(options) {
|
||||||
|
return fetch(this.getUrl())
|
||||||
|
.then(data => data.json())
|
||||||
|
.then(data => this.mapData(data, options));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Timeseries;
|
|
@ -7,7 +7,7 @@
|
||||||
"build": "webpack",
|
"build": "webpack",
|
||||||
"pretest": "eslint lib/*.js elements/*.js index.js",
|
"pretest": "eslint lib/*.js elements/*.js index.js",
|
||||||
"start": "http-server . -p 3000 -s",
|
"start": "http-server . -p 3000 -s",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "npm run pretest"
|
||||||
},
|
},
|
||||||
"author": "Henri Bergius <henri.bergius@iki.fi>",
|
"author": "Henri Bergius <henri.bergius@iki.fi>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|
33
screens/test.html
Normal file
33
screens/test.html
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Components test</title>
|
||||||
|
<script src="../node_modules/@webcomponents/webcomponentsjs/custom-elements-es5-adapter.js"></script>
|
||||||
|
<script src="../node_modules/plotly.js/dist/plotly.min.js"></script>
|
||||||
|
<script src="../dist/infoscreens.js"></script>
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Source+Code+Pro" rel="stylesheet">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: rgba(0, 0, 0, 0.98);
|
||||||
|
color: #fff;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 50vw 50vw;
|
||||||
|
grid-template-rows: 80vh 80vh;
|
||||||
|
padding: 0px;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<cbase-heatmap timeseries="crew.online" interpolate></cbase-heatmap>
|
||||||
|
<cbase-polar timeseries="bar.open" days=90 percentage></cbase-polar>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
Loading…
Add table
Reference in a new issue