@eclipse-scout/chart
Version:
Eclipse Scout chart
218 lines (185 loc) • 7.65 kB
text/typescript
/*
* 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 {aria, arrays, objects, scout} from '@eclipse-scout/core';
import {AbstractSvgChartRenderer, Chart} from '../index';
import $ from 'jquery';
import {UpdateChartOptions} from './Chart';
export class FulfillmentChartRenderer extends AbstractSvgChartRenderer {
animationTriggered: boolean;
segmentSelectorForAnimation: string;
r: number;
fullR: number;
constructor(chart: Chart) {
super(chart);
this.animationTriggered = false;
this.segmentSelectorForAnimation = '.fulfillment-chart';
this.suppressLegendBox = true;
let defaultConfig = {
options: {
fulfillment: {
startValue: undefined
}
}
};
chart.config = $.extend(true, {}, defaultConfig, chart.config);
}
protected override _validate(): boolean {
let chartValueGroups = this.chart.data.chartValueGroups;
if (chartValueGroups.length !== 2 ||
chartValueGroups[0].values.length !== 1 ||
chartValueGroups[1].values.length !== 1) {
return false;
}
return true;
}
protected override _renderInternal() {
// Calculate percentage
let chartData = this.chart.data;
let value = chartData.chartValueGroups[0].values[0] as number;
let total = chartData.chartValueGroups[1].values[0] as number;
this.fullR = (Math.min(this.chartBox.height, this.chartBox.width) / 2) - 2;
this._renderInnerCircle();
this._renderPercentage(value, total);
}
protected _renderPercentage(value: number, total: number) {
// arc segment
let arcClass = 'fulfillment-chart',
color = arrays.ensure(this.chart.data.chartValueGroups[0].colorHexValue)[0],
chartGroupCss = this.chart.data.chartValueGroups[0].cssClass;
if (this.chart.config.options.autoColor) {
arcClass += ' auto-color';
} else if (chartGroupCss) {
arcClass += ' ' + chartGroupCss;
}
let startValue = scout.nvl(this.chart.config.options.fulfillment.startValue, 0);
let end = 0;
let lastEnd = 0;
if (total) {
// use slightly less than 1.0 as max value because otherwise, the SVG element would not be drawn correctly.
end = Math.min(value / total, 0.999999);
lastEnd = Math.min(startValue / total, 0.999999);
}
this.r = this.fullR;
let that = this;
let tweenInFunc = function(now, fx) {
let $this = $(this);
let start = $this.data('animation-start'),
end = $this.data('animation-end');
$this.attr('d', that.pathSegment(start * fx.pos, lastEnd + (end - lastEnd) * fx.pos));
};
let $arc = this.$svg.appendSVG('path', arcClass)
.data('animation-start', 0)
.data('animation-end', end);
let radius2 = (this.fullR / 8) * 6.7;
let $transparentCircle = this._renderCirclePath('fulfillment-chart-inner-circle-transparent', 'InnerCircle3', radius2);
$transparentCircle.css('fill', this.firstOpaqueBackgroundColor);
// Label
let percentage = (total ? Math.round((value / total) * 100) : 0);
let $label = this.$svg.appendSVG('text', 'fulfillment-chart-label ')
.attr('x', this.chartBox.mX())
.attr('y', this.chartBox.mY())
.css('font-size', (this.fullR / 2) + 'px') // font of label in center relative to circle radius
.attr('dy', '0.3em') // workaround for 'dominant-baseline: central' which is not supported in IE
.attrXLINK('href', '#InnerCircle')
.text(percentage + '%');
if (this.chart.config.options.clickable) {
$arc.on('click', this._createClickObject(null, null), this._onChartValueClick.bind(this));
}
if (!this.chart.config.options.autoColor && !chartGroupCss) {
$arc.attr('fill', color);
}
if (this.animationDuration) {
$arc
.attr('d', this.pathSegment(0, lastEnd))
.animate({
tabIndex: 0
}, this._createAnimationObjectWithTabIndexRemoval(tweenInFunc, this.animationDuration));
$label
.attr('opacity', 0)
.animateSVG('opacity', 1, this.animationDuration, null, true);
} else {
$arc.attr('d', this.pathSegment(0, end));
}
aria.description(this.$svg, this.session.text('ui.FulfillmentChartAriaDescription', percentage));
}
pathSegment(start: number, end: number): string {
let s = start * 2 * Math.PI,
e = end * 2 * Math.PI,
pathString = '';
pathString += 'M' + (this.chartBox.mX() + this.r * Math.sin(s)) + ',' + (this.chartBox.mY() - this.r * Math.cos(s));
pathString += 'A' + this.r + ', ' + this.r;
pathString += (end - start < 0.5) ? ' 0 0,1 ' : ' 0 1,1 ';
pathString += (this.chartBox.mX() + this.r * Math.sin(e)) + ',' + (this.chartBox.mY() - this.r * Math.cos(e));
pathString += 'L' + this.chartBox.mX() + ',' + this.chartBox.mY() + 'Z';
return pathString;
}
protected _renderCirclePath(cssClass: string, id: string, radius: number): JQuery<SVGElement> {
let chartGroupCss = this.chart.data.chartValueGroups[0].cssClass;
let color = arrays.ensure(this.chart.data.chartValueGroups[1].colorHexValue)[0];
if (this.chart.config.options.autoColor) {
cssClass += ' auto-color';
} else if (chartGroupCss) {
cssClass += ' ' + chartGroupCss;
}
let radius2 = radius * 2;
let $path = this.$svg.appendSVG('path', cssClass)
.attr('id', id)
.attr('d', 'M ' + this.chartBox.mX() + ' ' + this.chartBox.mY() +
' m 0, ' + (-radius) +
' a ' + radius + ',' + radius + ' 0 1, 1 0,' + radius2 +
' a ' + radius + ',' + radius + ' 0 1, 1 0,' + (-radius2));
if (!this.chart.config.options.autoColor && !chartGroupCss) {
$path
.attr('fill', color)
.attr('stroke', color);
}
return $path;
}
protected _renderInnerCircle() {
let radius = (this.fullR / 8) * 7.5,
radius2 = (this.fullR / 8) * 7.2;
this._renderCirclePath('fulfillment-chart-inner-circle', 'InnerCircle', radius);
let $transparentCircle = this._renderCirclePath('fulfillment-chart-inner-circle-transparent', 'InnerCircle2', radius2);
$transparentCircle.css('fill', this.firstOpaqueBackgroundColor);
}
/**
* Do not animate the removal of the chart if the chart data has been updated and the startValue option is set.
* If startValue is not set use default implementation.
*/
override shouldAnimateRemoveOnUpdate(opts: UpdateChartOptions): boolean {
let startValue = objects.optProperty(this.chart, 'config', 'options', 'fulfillment', 'startValue');
if (!objects.isNullOrUndefined(startValue)) {
return false;
}
return super.shouldAnimateRemoveOnUpdate(opts);
}
protected override _removeAnimated(afterRemoveFunc: (chartAnimationStopping?: boolean) => void) {
if (this.animationTriggered) {
return;
}
let that = this;
let tweenOut = function(now, fx) {
let $this = $(this);
let start = $this.data('animation-start'),
end = $this.data('animation-end');
$this.attr('d', that.pathSegment(start * (1 - fx.pos), end * (1 - fx.pos)));
};
this.animationTriggered = true;
this.$svg.children(this.segmentSelectorForAnimation)
.animate({
tabIndex: 0
}, this._createAnimationObjectWithTabIndexRemoval(tweenOut))
.promise()
.done(() => {
this._remove(afterRemoveFunc);
this.animationTriggered = false;
});
}
}