UNPKG

@eclipse-scout/chart

Version:
458 lines (384 loc) 14.2 kB
/* * Copyright (c) 2010, 2025 BSI Business Systems Integration AG * * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 */ import {AbstractSvgChartRenderer, Chart, VennAsync3Calculator, VennCircle, VennCircleHelper} from '../index'; import $ from 'jquery'; import {ChartValueGroup} from './Chart'; import {LegendPositions} from './AbstractSvgChartRenderer'; import {arrays} from '@eclipse-scout/core'; export class VennChartRenderer extends AbstractSvgChartRenderer { animationTriggered: boolean; data: ChartValueGroup[]; centerX: number; centerY: number; numberOfCircles: number; readyToDraw: boolean; wasCircle: boolean; vennCircleHelper: VennCircleHelper; async3Calculator: VennAsync3Calculator; vennNumber1: VennCircle; vennReal1: VennCircle; vennNumber2: VennCircle; vennReal2: VennCircle; vennNumber3: VennCircle; vennReal3: VennCircle; $v1: JQuery<SVGElement>; $v2: JQuery<SVGElement>; $v3: JQuery<SVGElement>; constructor(chart: Chart) { super(chart); this.animationTriggered = false; this.suppressLegendBox = true; let defaultConfig = { options: { venn: { numberOfCircles: undefined } } }; chart.config = $.extend(true, {}, defaultConfig, chart.config); } protected override _validate(): boolean { let chartData = this.chart.data; if (!chartData || chartData.axes.length !== 0 || chartData.chartValueGroups.length === 0 || chartData.chartValueGroups[0].values.length === 0) { return false; } return true; } protected override _renderInternal() { this.centerX = this.width / 2; this.centerY = this.height / 2; if (this.centerX === 0 || this.centerY === 0) { return; } // basic values this.data = this.chart.data.chartValueGroups; this.numberOfCircles = this.chart.config.options.venn.numberOfCircles; // render parameter let distR = 10, maxR = Math.min(this.centerX, this.centerY), minR = maxR / 15, total = this.data.reduce((s, e) => { return s + (e.values[0] as number); }, 1); this.vennCircleHelper = new VennCircleHelper(distR, maxR, minR, total); // create svg elements and venns if (this.numberOfCircles > 0) { this.$v1 = this._createCircle(0, arrays.ensure(this.data[0].colorHexValue)[0], this.data[0].cssClass); this.vennNumber1 = new VennCircle(this.$v1); this.vennReal1 = new VennCircle(this.$v1); } if (this.numberOfCircles > 1) { this.$v2 = this._createCircle(1, arrays.ensure(this.data[1].colorHexValue)[0], this.data[1].cssClass); this.vennNumber2 = new VennCircle(this.$v2); this.vennReal2 = new VennCircle(this.$v2); } if (this.numberOfCircles > 2) { this.$v3 = this._createCircle(2, arrays.ensure(this.data[2].colorHexValue)[0], this.data[2].cssClass); this.vennNumber3 = new VennCircle(this.$v3); this.vennReal3 = new VennCircle(this.$v3); } // Final callback // In the case of 3 circles, draw will be called async after render is completed. Therefore, the current animationDuration needs to be set again when _draw is finally called. const animationDurationRender = this.animationDuration; let draw = function() { this.readyToDraw = true; if (!this.$svg.isAttached()) { // user navigated away. do not try to render-> error return; } this.readyToDraw = false; const animationDuration = this.animationDuration; this.setAnimationDuration(animationDurationRender); this._draw(true, true); this.setAnimationDuration(animationDuration); }.bind(this); // save callback if user navigated away while calculating and _draw is not executed. this.readyToDraw = false; // calc venns and set legend if (this.numberOfCircles === 1) { this._calc1(this.vennNumber1); this._calc1(this.vennReal1); draw(); } else if (this.numberOfCircles === 2) { this._calc2(this.vennNumber1, this.vennNumber2, false); this._calc2(this.vennReal1, this.vennReal2, true); draw(); } else if (this.numberOfCircles === 3) { this._calc3(this.vennNumber1, this.vennNumber2, this.vennNumber3, false, () => { if (this.rendering || this.rendered) { this._calc3(this.vennReal1, this.vennReal2, this.vennReal3, true, draw); } }); } } override remove(requestAnimation = false, afterRemoveFunc?: (chartAnimationStopping?: boolean) => void) { this._cancelAsync3Calculator(); super.remove(requestAnimation, afterRemoveFunc); } // calculation protected _calc1(v1: VennCircle) { // set basic data let a = this.data[0].values[0] as number; // calc sizes if (a > 0) { v1.r = this.vennCircleHelper.calcR(a, 0.8); } else { v1.r = this.vennCircleHelper.calcR(a, 0); } v1.x = 0; v1.y = 0; // place legend and label v1.setLegend(this.data[0].groupName, 1, -1); v1.addLabel(a, v1.x, v1.y); } protected _calc2(v1: VennCircle, v2: VennCircle, real: boolean) { // set basic data let a = this.data[0].values[0] as number; let b = this.data[1].values[0] as number; let ab = this.data[2].values[0] as number; let d12; if (real) { // basics calculation v1.r = this.vennCircleHelper.calcR(a + ab, 0.8); v2.r = this.vennCircleHelper.calcR(b + ab, 0.8); d12 = this.vennCircleHelper.calcD(v1, v2, a, b, ab, true); // calc x v1.x = 0; v2.x = d12; } else { // eslint-disable-next-line no-multi-assign v1.r = v2.r = this.vennCircleHelper.calcR(-1, 0.7); v1.x = -v1.r * 0.6; v2.x = v2.r * 0.6; } // calc y ;) v1.y = 0; v2.y = 0; // balance circles this.vennCircleHelper.findBalance2(v1, v2); // prepare legend v1.setLegend(this.data[0].groupName, -1, -1); v2.setLegend(this.data[1].groupName, 1, -1); // draw labels, and fix legend if (real) { if (ab === 0) { v1.addLabel(a, v1.x, v1.y); v2.addLabel(b, v2.x, v2.y); } else if (a === 0 && b === 0) { v1.addLabel(ab, v1.x, v1.y); } else if (a === 0) { v2.addLabel(ab, v1.x, v1.y); v2.addLabel(b, v2.x - (d12 - v2.r - v1.r) / 2, v2.y); v1.legendR = v2.r - d12; } else if (b === 0) { v1.addLabel(ab, v2.x, v2.y); v1.addLabel(a, v1.x + (d12 - v2.r - v1.r) / 2, v1.y); v2.legendR = v1.r - d12; } else { v1.addLabel(a, v1.x + (d12 - v2.r - v1.r) / 2, v1.y); v1.addLabel(b, v2.x - (d12 - v2.r - v1.r) / 2, v2.y); v2.addLabel(ab, v1.x + v1.r + (d12 - v2.r - v1.r) / 2, v1.y); } } else { v1.addLabel(a, -v1.r * 0.9, 0); v1.addLabel(b, v1.r * 0.9, 0); v2.addLabel(ab, 0, 0); } } protected _calc3(v1: VennCircle, v2: VennCircle, v3: VennCircle, real: boolean, callback: () => void) { // set basic data let a = this.data[0].values[0] as number; let b = this.data[1].values[0] as number; let c = this.data[2].values[0] as number; let ab = this.data[3].values[0] as number; let ac = this.data[4].values[0] as number; let bc = this.data[5].values[0] as number; let abc = this.data[6].values[0] as number; let d12, d13, d23; // calc sizes if (real) { // basics calculation v1.r = this.vennCircleHelper.calcR(a + ab + ac + abc, 0.55); v2.r = this.vennCircleHelper.calcR(b + ab + bc + abc, 0.55); v3.r = this.vennCircleHelper.calcR(c + ac + bc + abc, 0.55); // find distance between a; may reduce r d12 = this.vennCircleHelper.calcD(v1, v2, a + ac, b + bc, ab + abc, true); d13 = this.vennCircleHelper.calcD(v1, v3, a + ab, c + bc, ab + abc, false); d23 = this.vennCircleHelper.calcD(v2, v3, b + ab, c + ac, ab + abc, false); // find coordinates of a and b v1.x = 0; v2.x = d12; v3.x = d13; v1.y = 0; v2.y = 0; v3.y = 0; // c is much more difficult..., only changes v3 this._cancelAsync3Calculator(); this.async3Calculator = new VennAsync3Calculator(this.vennCircleHelper, v1, v2, v3, a, b, c, ab, ac, bc, abc, d12, d13, d23); this.async3Calculator.start(() => { this.async3Calculator = null; // balance circles this.vennCircleHelper.findBalance3(v1, v2, v3); // prepare legend v1.setLegend(this.data[0].groupName, -1, 1); v2.setLegend(this.data[1].groupName, 1, 1); v3.setLegend(this.data[2].groupName, 1, -1); callback(); }); } else { // draw label // eslint-disable-next-line no-multi-assign v1.r = v2.r = v3.r = this.vennCircleHelper.calcR(-1, 0.55); v1.x = -v1.r * 0.73; v2.x = v2.r * 0.73; v3.x = 0; v1.y = v1.r * 0.58; v2.y = v2.r * 0.58; v3.y = -v3.r * 0.58; // prepare legend v1.setLegend(this.data[0].groupName, -1, 1); v2.setLegend(this.data[1].groupName, 1, 1); v3.setLegend(this.data[2].groupName, 1, -1); v1.addLabel(a, -v1.r, v1.r * 0.76); v2.addLabel(b, v1.r, v1.r * 0.76); v3.addLabel(c, 0, -v1.r * 0.82); v1.addLabel(ab, 0, v1.r * 0.76); v1.addLabel(ac, -v1.r * 0.49, -v1.r * 0.05); v2.addLabel(bc, v1.r * 0.49, -v1.r * 0.05); v1.addLabel(abc, 0, v1.r * 0.22); callback(); } } protected _cancelAsync3Calculator() { if (this.async3Calculator) { this.async3Calculator.cancel(); this.async3Calculator = null; } } // drawing protected _draw(animated: boolean, real: boolean) { if (!this.rendered && !this.rendering) { // additional check, because this method might be called from a setTimeout() return; } if (this.animationTriggered) { return; } this.animationTriggered = true; // remove labels and legends let that = this; this.$svg.children('.venn-legend, .venn-label, .venn-axis-white, .label-line') .stop() .animateSVG('opacity', 1, 0, null, true) .promise() .done(function() { this.remove(); that.animationTriggered = false; }); // find venns we will update let showVenn = []; if (this.numberOfCircles > 0) { showVenn.push(real ? this.vennReal1 : this.vennNumber1); } if (this.numberOfCircles > 1) { showVenn.push(real ? this.vennReal2 : this.vennNumber2); } if (this.numberOfCircles > 2) { showVenn.push(real ? this.vennReal3 : this.vennNumber3); } // update venn and draw labels for (let i = 0; i < showVenn.length; i++) { let venn = showVenn[i]; this._updateVenn(venn, animated); for (let j = 0; j < venn.labels.length; j++) { let label = venn.labels[j]; this._drawLabel(label.text, label.x, label.y, animated); } } } // handling of circles protected _createCircle(circleIndex: number, color: string, cssClass: string): JQuery<SVGElement> { let $circle = this.$svg.appendSVG('circle', 'venn-circle') .attr('cx', this.centerX) .attr('cy', this.centerY) .attr('r', 0) .on('mouseenter', { showReal: false }, this._show.bind(this)) .on('mouseleave', { showReal: true }, this._show.bind(this)); if (this.chart.config.options.autoColor) { $circle.addClass('auto-color color0'); } else if (cssClass) { $circle.addClass(cssClass); } else { $circle.attr('fill', color); } if (this.chart.config.options.clickable) { $circle.on('click', this._createClickObject(null, circleIndex), this._onChartValueClick.bind(this)); } return $circle; } // handling of venn, label and legend protected _updateVenn(venn: VennCircle, animated: boolean) { // move circle venn.$circle .animateSVG('cx', this.centerX + venn.x, animated ? this.animationDuration : 0, null, true) .animateSVG('cy', this.centerY + venn.y, animated ? this.animationDuration : 0, null, true) .animateSVG('r', venn.r, animated ? this.animationDuration : 0, null, true); // set up position legend let minR = this.vennCircleHelper.minR, x1 = this.centerX + venn.x + venn.legendH * Math.sin(Math.PI / 5) * venn.r, y1 = this.centerY + venn.y + venn.legendV * Math.cos(Math.PI / 5) * venn.r, x2 = this.centerX + venn.x + venn.legendH * Math.sin(Math.PI / 5) * (venn.legendR + minR * 1.5), y2 = this.centerY + venn.y + venn.legendV * Math.cos(Math.PI / 5) * (venn.legendR + minR * 1.5); let legendPositions = { x1: x1, x2: x2, y1: y1, y2: y2, v: venn.legendV, h: venn.legendH } as LegendPositions; this._renderWireLegend(venn.legend, legendPositions, 'venn-legend'); } protected _drawLabel(text: number, dx: number, dy: number, animated: boolean) { // draw label let $label = this.$svg.appendSVG('text', 'venn-label') .attr('x', this.centerX + dx) .attr('y', this.centerY + dy) .text(text); // animate if needed if (animated && this.animationDuration) { $label .attr('opacity', 0) .animateSVG('opacity', 1, this.animationDuration, null, true); } } // handling of show/hide numbers protected _show(event: JQuery.MouseEventBase) { if (this.numberOfCircles === 1) { return; // Nothing to do for only one circle } // target contains element that is entered, relatedTarget contains element that is left let toElement = (event.type === 'mouseenter' ? event.target : event.relatedTarget); // check if true enter or just from one circle to another let isCircle = toElement && $(toElement).hasClass('venn-circle'); if (this.wasCircle && isCircle) { return; } this.wasCircle = isCircle; // draw animated in every case this._draw(true, event.data.showReal); } }