377 lines
14 KiB
JavaScript
377 lines
14 KiB
JavaScript
/**
|
|
* @augments EicComponent
|
|
* @category EICUI/Components
|
|
*/
|
|
class Chart extends EicComponent {
|
|
|
|
constructor(el, options) {
|
|
super(el, options);
|
|
if(this._constructed) return this;
|
|
|
|
this.el.innerHTML = ``;
|
|
|
|
this.plotter = ui.create(`<svg height="${this.options.height == 'auto' ? '100%': this.options.height}" width="100%"></svg>`)
|
|
this.el.append(this.plotter);
|
|
this.el.setAttribute(this.options.identifier, '')
|
|
|
|
}
|
|
|
|
set data(value) {
|
|
this.options.data = value;
|
|
this.redraw()
|
|
}
|
|
get data() { return this.options.data }
|
|
|
|
redraw() {}
|
|
|
|
clear() {
|
|
this.options.data = [];
|
|
this.redraw();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @augments EicComponent
|
|
* @category EICUI/Components
|
|
*/
|
|
class BarChart extends Chart {
|
|
|
|
constructor(el, options) {
|
|
let defaultOptions = {
|
|
title: '',
|
|
subtitle: '',
|
|
height: 'auto',
|
|
showLabels: true,
|
|
showValues: true,
|
|
maxPlot: 0,
|
|
data: [],
|
|
identifier: 'eicbarchart'
|
|
}
|
|
super(el, {...defaultOptions, ...(options || null)});
|
|
if(this._constructed) return this;
|
|
|
|
this.redraw();
|
|
}
|
|
|
|
redraw() {
|
|
let series = this.options.data;
|
|
let minValue = 0;
|
|
let maxValue = 0;
|
|
|
|
let w = this.position.width;
|
|
let h = this.options.height == 'auto' ? this.position.height: this.options.height;
|
|
let bh = Math.max(h - 30, 0);
|
|
|
|
this.plotter.innerHTML = '';
|
|
|
|
if(this.options.title) {
|
|
this.plotter.append(ui.create(`<svg class="title" x="0" y="0"><text x="50%" y="16" text-anchor="middle">${this.options.title}</text></g>`))
|
|
}
|
|
if(series.length > 0) {
|
|
let start = this.options.maxPlot > 0 && this.options.maxPlot < series.length ? series.length - this.options.maxPlot: 0;
|
|
|
|
for(let item of series) {
|
|
if(item.value > maxValue) maxValue = item.value;
|
|
if(item.value < minValue) minValue = item.value;
|
|
}
|
|
|
|
let index = 0;
|
|
|
|
for(let i = start; i < series.length; i++) {
|
|
let item = series[i];
|
|
let g = ui.create(
|
|
`<svg class="plot" ${item.severity || ''} x="${100 * index / (series.length - start)}%" y="0" width="${100 / (series.length - start)}%">
|
|
<rect x="10%" y="${bh + 24 - bh * (item.value / maxValue)}" width="80%" height="${bh * (item.value / maxValue)}">
|
|
${item.tip ? `<title>${item.tip}</title>`: ''}
|
|
</rect>
|
|
${this.options.showValue ? `<text x="50%" y="${bh + 12 - bh * (item.value / maxValue)}" text-anchor="middle">${item.value}</text>`: ''}
|
|
${this.options.showLabel ? `<text x="50%" y="${h - 6}" text-anchor="middle">${item.label}</text>`: ''}
|
|
</svg>`);
|
|
|
|
this.plotter.append(g);
|
|
|
|
index++;
|
|
}
|
|
}
|
|
}
|
|
|
|
add(data) {
|
|
this.options.data.push(data);
|
|
this.redraw();
|
|
}
|
|
}
|
|
|
|
class LineChart extends Chart {
|
|
|
|
constructor(el, options) {
|
|
let defaultOptions = {
|
|
title: '',
|
|
subtitle: '',
|
|
height: 'auto',
|
|
showLabels: true,
|
|
showValues: true,
|
|
maxPlot: 0,
|
|
data: [],
|
|
identifier: 'eiclinechart'
|
|
}
|
|
super(el, {...defaultOptions, ...(options || null)});
|
|
if(this._constructed) return this;
|
|
|
|
this.redraw();
|
|
}
|
|
|
|
redraw() {
|
|
let series = this.options.data;
|
|
let minValue = 0;
|
|
let maxValue = 0;
|
|
|
|
let w = this.position.width;
|
|
let h = this.options.height == 'auto' ? this.position.height: this.options.height;
|
|
let bh = Math.max(h - 30, 0);
|
|
|
|
this.plotter.innerHTML = '';
|
|
|
|
if(this.options.title) {
|
|
this.plotter.append(ui.create(`<svg class="title" x="0" y="0"><text x="50%" y="16" text-anchor="middle">${this.options.title}</text></g>`))
|
|
}
|
|
|
|
if(series.length > 0) {
|
|
let start = this.options.maxPlot > 0 && this.options.maxPlot < series.length ? series.length - this.options.maxPlot: 0;
|
|
|
|
for(let serie of series) {
|
|
for(let value of serie.data) {
|
|
if(value > maxValue) maxValue = value;
|
|
if(value < minValue) minValue = value;
|
|
}
|
|
}
|
|
|
|
let dots = []
|
|
|
|
for(let item of series) {
|
|
let path = '';
|
|
let index = 0;
|
|
|
|
for(let value of item.data) {
|
|
let x = 5 + ((this.position.width - 10) * index / (item.data.length - start - 1));
|
|
let y = bh + 24 - (bh * (value / maxValue));
|
|
path += `${index == 0 ? 'M': 'L'} ${x} ${y} `
|
|
let dot = ui.create(`<svg class="plot" ${item.severity || ''} x="0" y="0" width="100%" height="100%">
|
|
<rect x="${x-4}" y="${y-4}" width="8" height="8" class="dot">
|
|
<title>${this.options.labels[index]}: ${value}${item.unit ? item.unit: ''}</title>
|
|
</rect>
|
|
</svg>`)
|
|
dots.push(dot)
|
|
index++;
|
|
}
|
|
|
|
let line = ui.create(
|
|
`<svg class="plot" ${item.severity || ''} x="0" y="0" width="100%" height="100%">
|
|
<path d="${path}" />
|
|
</svg>`);
|
|
|
|
this.plotter.append(line);
|
|
}
|
|
|
|
for(let dot of dots) this.plotter.append(dot)
|
|
}
|
|
}
|
|
|
|
add(data) {
|
|
this.options.data.push(data);
|
|
this.redraw();
|
|
}
|
|
}
|
|
|
|
class PieChart extends Chart {
|
|
|
|
constructor(el, options) {
|
|
let defaultOptions = {
|
|
title: '',
|
|
subtitle: '',
|
|
height: 'auto',
|
|
showLabels: true,
|
|
showValues: true,
|
|
maxPlot: 0,
|
|
data: [],
|
|
identifier: 'eicpiechart'
|
|
}
|
|
super(el, {...defaultOptions, ...(options || null)});
|
|
if(this._constructed) return this;
|
|
|
|
this.redraw();
|
|
}
|
|
|
|
redraw() {
|
|
let series = this.options.data;
|
|
let r = 40;
|
|
let center = ['50%', '50%'];
|
|
|
|
this.plotter.setAttribute( 'viewBox' , '0 0 100 100')
|
|
this.plotter.innerHTML = '';
|
|
|
|
if(this.options.title) {
|
|
this.plotter.append(ui.create(`<svg class="title" x="0" y="0"><text x="50%" y="16" text-anchor="middle">${this.options.title}</text></g>`))
|
|
}
|
|
|
|
series = series.sort((a,b) => a.value < b.value ? -1 : 0)
|
|
|
|
if(series.length > 0) {
|
|
let total = 0;
|
|
for(let item of series) { total += item.value }
|
|
// build the slices
|
|
let o = 0;
|
|
let l = r * Math.PI;
|
|
for(let item of series) {
|
|
if(item.value == 0) continue;
|
|
o = o + Math.round(l * item.value / total)
|
|
|
|
let g = ui.create(`<svg class="plot" x="0" y="0" width="100%" height="100%">
|
|
<circle class="slice" ${item.severity} stroke-dasharray="${o + ' ' + l}" stroke-width="${r}" cx="${center[0]}" cy="${center[1]}" r="${r / 2}">
|
|
</svg>`);
|
|
|
|
this.plotter.prepend(g);
|
|
}
|
|
// add pie background
|
|
this.plotter.append( ui.create(`<svg class="plot" x="0" y="0" width="100%" height="100%"><circle class="back" cx="${center[0]}" cy="${center[1]}" r="${r / 1.7}" /></svg>`) );
|
|
}
|
|
//this.el.html(this.el.html())
|
|
}
|
|
|
|
add(data) {
|
|
this.options.data.push(data);
|
|
this.redraw();
|
|
}
|
|
}
|
|
|
|
class PieTreeChart extends Chart {
|
|
constructor(el, options) {
|
|
let defaultOptions = {
|
|
title: '',
|
|
subtitle: '',
|
|
height: 'auto',
|
|
showLabels: true,
|
|
showValues: true,
|
|
maxPlot: 0,
|
|
data: [],
|
|
sort: ((a,b) => a.value < b.value ? -1 : 1), // -1 : 1 => CCW 1 : -1 => CW
|
|
identifier: 'eicpiechart',
|
|
center:['50%', '50%'],
|
|
innerRadius: 35, // in % of the viewbox, thus max 50
|
|
outerRadius: 50, // in % of the viewbox, thus max 50
|
|
layersGap: 1, // in px
|
|
angularGap: 1, // in deg
|
|
}
|
|
super(el, {...defaultOptions, ...(options || null)});
|
|
if(this._constructed) return this;
|
|
|
|
this.flatSeries = []
|
|
if(typeof(this.options.sort)=='function') this.options.data.sort(this.options.sort)
|
|
this.serieToFlat(this.options.data)
|
|
|
|
this.redraw();
|
|
}
|
|
|
|
serieToFlat(serie){
|
|
const cleanSerie = serie.map(obj => { const {children, ...cleanObj} = obj; return(cleanObj) }) // Remove children
|
|
this.flatSeries.push(cleanSerie)
|
|
|
|
if(serie.some(item => Array.isArray(item.children))){
|
|
let layer = []
|
|
for(let item of serie){
|
|
if(Array.isArray(item.children)){
|
|
if(typeof(this.options.sort)=='function') item.children.sort(this.options.sort)
|
|
const ChildrenSum = item.children.reduce((acc, item) => acc + item.value, 0);
|
|
const cleanSubSerie = item.children.map(obj => {
|
|
obj.value = (obj.value/ChildrenSum)*item.value
|
|
obj.parentSeverity = item.severity=='inherit' ? item.parentSeverity : item.severity
|
|
return(obj)
|
|
})
|
|
layer = [...layer, ...cleanSubSerie]
|
|
} else { // simulate a single full layer
|
|
layer.push( { severity: '', hide:true, value: item.value },)
|
|
}
|
|
}
|
|
this.serieToFlat(layer)
|
|
}
|
|
|
|
}
|
|
|
|
redraw() {
|
|
this.plotter.setAttribute( 'viewBox' , '0 0 100 100')
|
|
this.plotter.setAttribute('shape-rendering', 'geometricPrecision')
|
|
this.plotter.innerHTML = '';
|
|
|
|
if(this.options.title) {
|
|
this.plotter.append(ui.create(`<svg class="title" x="0" y="0"><text x="50%" y="16" text-anchor="middle">${this.options.title}</text></g>`))
|
|
}
|
|
|
|
const nbLayers = this.flatSeries.length
|
|
const bandsWidth = Math.floor((this.options.outerRadius - this.options.innerRadius)/nbLayers)
|
|
for(let layer=0; layer<nbLayers; layer++){
|
|
this.drawLayer(this.flatSeries[layer], this.options.innerRadius+(layer*bandsWidth), this.options.innerRadius+((layer+1)*bandsWidth)-this.options.layersGap )
|
|
}
|
|
if(typeof(this.options.click)=='function'){
|
|
this.plotter.querySelectorAll('circle.slice').forEach(el => el.addEventListener('click', this.options.click))
|
|
}
|
|
}
|
|
|
|
drawLayer(serie, innerRadius, outerRadius){
|
|
let sectorWidth = outerRadius>innerRadius ? outerRadius - innerRadius : 1
|
|
let radius = innerRadius + (sectorWidth/2)
|
|
if(serie.length > 0) {
|
|
let total = 0;
|
|
for(let item of serie) { total += item.value }
|
|
// Ideal dash pattern in our case : [ Space(start angle) Mark(value) Space(complement) ]
|
|
// But dasharray must have an even count, thus 4, and it always starts with a mark.
|
|
// Therefore: [ Mark(1) Space(start angle) Mark(value) Space(complement) ] then shift left by 1 (thus hide mark(1)) with stroke-dashoffset.
|
|
// This is much cleaner than the cumulative & draw-over technique which quickly creates graphical artefacts (dirt at borders) on non-trivial drawings.
|
|
const circonf = (2 * radius * Math.PI)
|
|
let offset = 0
|
|
for(let item of serie) {
|
|
if(item.value == 0) continue;
|
|
const dashLength = circonf * item.value / total
|
|
let markup = `<svg class="plot" x="0" y="0" width="100%" height="100%">`
|
|
markup += `<circle class="slice" ${item.hide ? 'stroke="#FFF"' : item.severity=='inherit' ? item.parentSeverity : item.severity}
|
|
stroke-dasharray="1 ${offset} ${dashLength} ${circonf-dashLength-offset}"
|
|
stroke-dashoffset="1"
|
|
stroke-width="${sectorWidth}"
|
|
${item.brightness ? 'style="filter: brightness('+item.brightness+');"' : ''}
|
|
cx="${this.options.center[0]}"
|
|
cy="${this.options.center[1]}"
|
|
r="${radius}"
|
|
${(typeof(item.dataset)=='object') ? Object.entries(item.dataset).map(([k, v]) => `data-${k}="${v}"`).join(' ') : ''}
|
|
>
|
|
`
|
|
if(item.title) markup += `<title>${item.title}</title>`
|
|
markup += '</circle></svg>'
|
|
offset += dashLength
|
|
this.plotter.prepend(ui.create(markup))
|
|
}
|
|
|
|
if(this.options.angularGap>0){
|
|
offset = 0
|
|
let markup = `<svg class="plot" x="0" y="0" width="100%" height="100%">`
|
|
for(let item of serie) {
|
|
if(item.value == 0) continue
|
|
const dashLength = circonf * item.value / total
|
|
const offsetAngle = offset / radius
|
|
const x1 = parseInt(this.options.center[0]) + (innerRadius * Math.cos(offsetAngle))
|
|
const y1 = parseInt(this.options.center[1]) + (innerRadius * Math.sin(offsetAngle))
|
|
const x2 = parseInt(this.options.center[0]) + (outerRadius * Math.cos(offsetAngle) * 1.1)
|
|
const y2 = parseInt(this.options.center[1]) + (outerRadius * Math.sin(offsetAngle) * 1.1)
|
|
markup += `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="#FFF" stroke-width="${this.options.angularGap}"/>`
|
|
offset += dashLength
|
|
}
|
|
markup += '</svg>'
|
|
this.plotter.append(ui.create(markup))
|
|
}
|
|
}
|
|
}
|
|
|
|
add(data) {
|
|
this.options.data.push(data);
|
|
this.redraw();
|
|
}
|
|
}
|
|
|