/** * @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(``) 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(`${this.options.title}`)) } 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( ` ${item.tip ? `${item.tip}`: ''} ${this.options.showValue ? `${item.value}`: ''} ${this.options.showLabel ? `${item.label}`: ''} `); 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(`${this.options.title}`)) } 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(` ${this.options.labels[index]}: ${value}${item.unit ? item.unit: ''} `) dots.push(dot) index++; } let line = ui.create( ` `); 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(`${this.options.title}`)) } 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(` `); this.plotter.prepend(g); } // add pie background this.plotter.append( ui.create(``) ); } //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(`${this.options.title}`)) } const nbLayers = this.flatSeries.length const bandsWidth = Math.floor((this.options.outerRadius - this.options.innerRadius)/nbLayers) for(let layer=0; layer 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 = `` markup += ` `data-${k}="${v}"`).join(' ') : ''} > ` if(item.title) markup += `${item.title}` markup += '' offset += dashLength this.plotter.prepend(ui.create(markup)) } if(this.options.angularGap>0){ offset = 0 let markup = `` 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 += `` offset += dashLength } markup += '' this.plotter.append(ui.create(markup)) } } } add(data) { this.options.data.push(data); this.redraw(); } }