Files
2025-08-27 07:03:09 +00:00

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();
}
}