| /* |
| * Copyright (c) 2010-2020 BSI Business Systems Integration AG. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * BSI Business Systems Integration AG - initial API and implementation |
| */ |
| import {arrays, DateColumn, icons, NumberColumn, objects, scout, strings, styles, TableControl, TableMatrix, tooltips} from '@eclipse-scout/core'; |
| import {Chart, ChartTableControlLayout, ChartTableUserFilter} from '../../index'; |
| import $ from 'jquery'; |
| |
| export default class ChartTableControl extends TableControl { |
| |
| constructor() { |
| super(); |
| this.tooltipText = '${textKey:ui.Chart}'; |
| this.chartAggregation = { |
| modifier: TableMatrix.NumberGroup.COUNT |
| }; |
| this.chartType = Chart.Type.BAR; |
| this.oldChartType = null; |
| this.chartColorScheme = 'chart-table-control'; |
| |
| // chart config selection |
| this.$chartSelect = null; |
| this.$xAxisSelect = null; |
| this.$yAxisSelect = null; |
| this.$dataSelect = null; |
| |
| this.xAxis = null; |
| this.yAxis = null; |
| |
| this.dateGroup = null; |
| |
| this._tableUpdatedHandler = this._onTableUpdated.bind(this); |
| this._tableColumnStructureChangedHandler = this._onTableColumnStructureChanged.bind(this); |
| this._chartValueClickedHandler = this._onChartValueClick.bind(this); |
| } |
| |
| static DATE_GROUP_FLAG = 0x100; |
| static MAX_AXIS_COUNT = 100; |
| |
| _init(model) { |
| super._init(model); |
| this.table.on('columnStructureChanged', this._tableColumnStructureChangedHandler); |
| |
| this.chart = scout.create('Chart', { |
| parent: this |
| }); |
| } |
| |
| _destroy() { |
| this.table.off('columnStructureChanged', this._tableColumnStructureChangedHandler); |
| super._destroy(); |
| } |
| |
| _computeEnabled(inheritAccessibility, parentEnabled) { |
| if (!this._hasColumns() && !this.selected) { |
| return false; |
| } |
| return super._computeEnabled(inheritAccessibility, parentEnabled); |
| } |
| |
| _renderChart() { |
| if (this.chart) { |
| this.chart.render(this.$contentContainer); |
| this.chart.$container.addClass(this.denseClass); |
| } |
| } |
| |
| _createLayout() { |
| return new ChartTableControlLayout(this); |
| } |
| |
| _renderChartType() { |
| this._selectChartType(); |
| if (this.oldChartType) { |
| this.$yAxisSelect.animateAVCSD('width', this.chartType === Chart.Type.BUBBLE ? 175 : 0); |
| this.$yAxisSelect.animateAVCSD('margin-right', this.chartType === Chart.Type.BUBBLE ? 15 : 0); |
| } else { |
| this.$yAxisSelect.css('width', this.chartType === Chart.Type.BUBBLE ? 175 : 0); |
| this.$yAxisSelect.css('margin-right', this.chartType === Chart.Type.BUBBLE ? 15 : 0); |
| } |
| |
| if (this.contentRendered) { |
| this.chart.$container.animateAVCSD('opacity', 0, () => { |
| this.chart.$container.css('opacity', 1); |
| this._drawChart(); |
| }); |
| } |
| } |
| |
| _selectChartType() { |
| objects.values(this._chartTypeMap).forEach($element => { |
| $element.removeClass('selected'); |
| }); |
| this._chartTypeMap[this.chartType].addClass('selected'); |
| } |
| |
| _renderChartGroup1() { |
| this._renderChartGroup(1); |
| } |
| |
| _renderChartGroup2() { |
| this._renderChartGroup(2); |
| } |
| |
| _renderChartGroup(groupId) { |
| if (!this._hasColumns()) { |
| return; |
| } |
| let groupName = 'chartGroup' + groupId; |
| let map = '_' + groupName + 'Map'; |
| let chartGroup = this[groupName]; |
| if (chartGroup) { |
| let $element = this[map][chartGroup.id]; |
| $element.siblings().animateAVCSD('height', 30); |
| $element.selectOne('selected'); |
| |
| if (chartGroup.modifier > 0) { |
| let dateGroupIndex = chartGroup.modifier ^ ChartTableControl.DATE_GROUP_FLAG; |
| $element.animateAVCSD('height', 42); |
| $element.children('.select-axis-group').text(this.dateGroup[dateGroupIndex][1]); |
| } |
| if (this.contentRendered) { |
| this._drawChart(); |
| } |
| } |
| } |
| |
| _renderChartAggregation() { |
| let $element = this._aggregationMap[this.chartAggregation.id || 'all']; |
| if ($element) { |
| $element.selectOne('selected'); |
| $element |
| .removeClass('data-sum') |
| .removeClass('data-avg'); |
| $element.addClass(this._getAggregationCssClass()); |
| if (this.contentRendered) { |
| this._drawChart(); |
| } |
| } |
| } |
| |
| _getAggregationCssClass() { |
| switch (this.chartAggregation.modifier) { |
| case TableMatrix.NumberGroup.COUNT: |
| return 'data-count'; |
| case TableMatrix.NumberGroup.SUM: |
| return 'data-sum'; |
| case TableMatrix.NumberGroup.AVG: |
| return 'data-avg'; |
| default: |
| return null; |
| } |
| } |
| |
| _renderChartSelect(cssClass, chartType, renderSvgIcon) { |
| let $svg = this.$chartSelect |
| .appendSVG('svg', cssClass + ' select-chart') |
| .toggleClass('disabled', !this.enabledComputed || !this._hasColumns()) |
| .data('chartType', chartType); |
| |
| if (this.enabledComputed && this._hasColumns()) { |
| $svg.on('click', this._onClickChartType.bind(this)); |
| } |
| |
| this._chartTypeMap[chartType] = $svg; |
| |
| renderSvgIcon($svg); |
| } |
| |
| /** |
| * Appends a chart selection divs to this.$contentContainer and sets the this.$chartSelect property. |
| * */ |
| _renderChartSelectContainer() { |
| // create container |
| this.$chartSelect = this.$contentContainer.appendDiv('chart-select'); |
| |
| // create chart types for selection |
| this._chartTypeMap = {}; |
| |
| let supportedChartTypes = this._getSupportedChartTypes(); |
| |
| if (scout.isOneOf(Chart.Type.BAR, supportedChartTypes)) { |
| this._renderChartSelect('chart-bar', Chart.Type.BAR, renderSvgIconBar); |
| } |
| if (scout.isOneOf(Chart.Type.BAR_HORIZONTAL, supportedChartTypes)) { |
| this._renderChartSelect('chart-stacked', Chart.Type.BAR_HORIZONTAL, renderSvgIconStacked); |
| } |
| if (scout.isOneOf(Chart.Type.LINE, supportedChartTypes)) { |
| this._renderChartSelect('chart-line', Chart.Type.LINE, renderSvgIconLine); |
| } |
| if (scout.isOneOf(Chart.Type.PIE, supportedChartTypes)) { |
| this._renderChartSelect('chart-pie', Chart.Type.PIE, renderSvgIconPie.bind(this)); |
| } |
| if (scout.isOneOf(Chart.Type.BUBBLE, supportedChartTypes)) { |
| this._renderChartSelect('chart-bubble', Chart.Type.BUBBLE, renderSvgIconBubble); |
| } |
| |
| function renderSvgIconBar($svg) { |
| let show = [2, 4, 3, 3.5, 5]; |
| |
| for (let s = 0; s < show.length; s++) { |
| $svg.appendSVG('rect', 'select-fill') |
| .attr('x', s * 13) |
| .attr('y', 50 - show[s] * 9) |
| .attr('width', 11) |
| .attr('height', show[s] * 9); |
| } |
| } |
| |
| function renderSvgIconStacked($svg) { |
| let show = [2, 4, 3.5, 4.5]; |
| |
| for (let s = 0; s < show.length; s++) { |
| $svg.appendSVG('rect', 'select-fill') |
| .attr('x', 0) |
| .attr('y', 16 + s * 9) |
| .attr('width', show[s] * 14) |
| .attr('height', 7); |
| } |
| } |
| |
| function renderSvgIconLine($svg) { |
| let show = [0, 1.7, 1, 2, 1.5, 2.5], |
| pathPoints = []; |
| |
| for (let s = 0; s < show.length; s++) { |
| pathPoints.push(2 + (s * 12) + ',' + (45 - show[s] * 11)); |
| } |
| |
| $svg |
| .appendSVG('path', 'select-fill-line') |
| .attr('d', 'M' + pathPoints.join('L')); |
| } |
| |
| function renderSvgIconPie($svg) { |
| let show = [ |
| [0, 0.1], |
| [0.1, 0.25], |
| [0.25, 1] |
| ]; |
| |
| for (let s = 0; s < show.length; s++) { |
| $svg |
| .appendSVG('path', 'select-fill-pie') |
| .attr('d', this._pathSegment(37, 30, 24, show[s][0], show[s][1])); |
| } |
| } |
| |
| function renderSvgIconBubble($svg) { |
| $svg.appendSVG('line', 'select-fill-line') |
| .attr('x1', 3).attr('y1', 53) |
| .attr('x2', 63).attr('y2', 53); |
| |
| $svg.appendSVG('line', 'select-fill-line') |
| .attr('x1', 8).attr('y1', 12) |
| .attr('x2', 8).attr('y2', 58); |
| |
| $svg.appendSVG('circle', 'select-fill') |
| .attr('cx', 22).attr('cy', 40) |
| .attr('r', 5); |
| |
| $svg.appendSVG('circle', 'select-fill') |
| .attr('cx', 50).attr('cy', 26) |
| .attr('r', 11); |
| } |
| } |
| |
| _getSupportedChartTypes() { |
| return [ |
| Chart.Type.BAR, |
| Chart.Type.BAR_HORIZONTAL, |
| Chart.Type.LINE, |
| Chart.Type.PIE, |
| Chart.Type.BUBBLE |
| ]; |
| } |
| |
| _onClickChartType(event) { |
| let $target = $(event.currentTarget), |
| chartType = $target.data('chartType'); |
| this.setChartType(chartType); |
| } |
| |
| _onClickChartGroup(event) { |
| let $target = $(event.currentTarget), |
| groupId = $target.parent().data('groupId'), |
| column = $target.data('column'), |
| origModifier = $target.data('modifier'); |
| |
| // do nothing when item is disabled |
| if (!$target.isEnabled()) { |
| return; |
| } |
| |
| let modifier = $target.isSelected() ? this._nextDateModifier(origModifier) : origModifier; |
| $target.data('modifier', modifier); |
| |
| let config = { |
| id: column ? column.id : null, |
| modifier: modifier |
| }; |
| |
| this._setChartGroup(groupId, config); |
| } |
| |
| _onClickAggregation(event) { |
| let $target = $(event.currentTarget); |
| // update modifier |
| let origModifier = $target.data('modifier'); |
| let modifier = $target.isSelected() ? this._nextModifier(origModifier) : origModifier; |
| $target.data('modifier', modifier); |
| |
| let column = $target.data('column'); |
| let aggregation = { |
| id: column ? column.id : null, |
| modifier: modifier |
| }; |
| |
| this._setChartAggregation(aggregation); |
| } |
| |
| _nextDateModifier(modifier) { |
| switch (modifier) { |
| case TableMatrix.DateGroup.DATE: |
| return TableMatrix.DateGroup.MONTH; |
| case TableMatrix.DateGroup.MONTH: |
| return TableMatrix.DateGroup.WEEKDAY; |
| case TableMatrix.DateGroup.WEEKDAY: |
| return TableMatrix.DateGroup.YEAR; |
| case TableMatrix.DateGroup.YEAR: |
| return TableMatrix.DateGroup.DATE; |
| default: |
| return modifier; |
| } |
| } |
| |
| _nextModifier(modifier) { |
| switch (modifier) { |
| case TableMatrix.NumberGroup.SUM: |
| return TableMatrix.NumberGroup.AVG; |
| case TableMatrix.NumberGroup.AVG: |
| return TableMatrix.NumberGroup.SUM; |
| default: |
| return modifier; |
| } |
| } |
| |
| _setChartAggregation(chartAggregation) { |
| if (chartAggregation === this.chartAggregation) { |
| return; |
| } |
| this._setProperty('chartAggregation', chartAggregation); |
| if (this.contentRendered) { |
| this._renderChartAggregation(); |
| } |
| } |
| |
| _setChartGroup1(chartGroup) { |
| this._setChartGroup(1, chartGroup); |
| } |
| |
| _setChartGroup2(chartGroup) { |
| this._setChartGroup(2, chartGroup); |
| } |
| |
| _setChartGroup(groupId, chartGroup) { |
| let propertyName = 'chartGroup' + groupId; |
| this._changeProperty(propertyName, chartGroup); |
| } |
| |
| _changeProperty(prop, value) { |
| if (value === this[prop]) { |
| return; |
| } |
| this._setProperty(prop, value); |
| if (this.contentRendered) { |
| this['_render' + prop.charAt(0).toUpperCase() + prop.slice(1)](); |
| } |
| } |
| |
| setChartType(chartType) { |
| this.oldChartType = this.chartType; |
| this.setProperty('chartType', chartType); |
| } |
| |
| _hasColumns() { |
| return this.table.columns.length !== 0; |
| } |
| |
| _axisCount(columnCount, column) { |
| let i, tmpColumn; |
| for (i = 0; i < columnCount.length; i++) { |
| tmpColumn = columnCount[i][0]; |
| if (tmpColumn === column) { |
| return columnCount[i][1]; |
| } |
| } |
| return 0; |
| } |
| |
| _plainAxisText(column, text) { |
| if (column.headerHtmlEnabled) { |
| let plainText = strings.plainText(text); |
| return plainText.replace(/\n/g, ' '); |
| } |
| return text; |
| } |
| |
| _renderContent($parent) { |
| this.$contentContainer = $parent.appendDiv('chart-container'); |
| |
| // scrollbars |
| this._installScrollbars(); |
| |
| this._renderChartSelectContainer(); |
| |
| // group functions for dates |
| this.dateGroup = [ |
| [TableMatrix.DateGroup.YEAR, this.session.text('ui.groupedByYear')], |
| [TableMatrix.DateGroup.MONTH, this.session.text('ui.groupedByMonth')], |
| [TableMatrix.DateGroup.WEEKDAY, this.session.text('ui.groupedByWeekday')], |
| [TableMatrix.DateGroup.DATE, this.session.text('ui.groupedByDate')] |
| ]; |
| |
| // listeners |
| this._filterResetListener = this.table.on('filterReset', event => { |
| if (this.chart) { |
| this.chart.setCheckedItems([]); |
| } |
| }); |
| |
| this._addListeners(); |
| |
| // add addition rectangle for hover and event handling |
| $('.select-chart', this.$contentContainer) |
| .appendSVG('rect', 'select-events') |
| .attr('width', 65) |
| .attr('height', 60) |
| .attr('fill', 'none') |
| .attr('pointer-events', 'all'); |
| |
| let columnCount = this._renderAxisSelectors(); |
| |
| // draw first chart |
| this._renderChart(); |
| |
| this._initializeSelection(columnCount); |
| |
| this._renderChartParts(); |
| |
| this._drawChart(); |
| } |
| |
| _addListeners() { |
| this.table.on('rowsInserted', this._tableUpdatedHandler); |
| this.table.on('rowsDeleted', this._tableUpdatedHandler); |
| this.table.on('allRowsDeleted', this._tableUpdatedHandler); |
| this.chart.on('valueClick', this._chartValueClickedHandler); |
| } |
| |
| _renderAxisSelectors() { |
| // create container for x/y-axis |
| this.$xAxisSelect = this.$contentContainer |
| .appendDiv('xaxis-select') |
| .data('groupId', 1); |
| this.$yAxisSelect = this.$contentContainer |
| .appendDiv('yaxis-select') |
| .data('groupId', 2); |
| |
| // map for selection (column id, $element) |
| this._chartGroup1Map = {}; |
| this._chartGroup2Map = {}; |
| |
| // find best x and y axis: best is 9 different entries |
| let matrix = new TableMatrix(this.table, this.session), |
| columnCount = matrix.columnCount(false); // filterNumberColumns false: number columns will be filtered below |
| columnCount.sort((a, b) => { |
| return Math.abs(a[1] - 8) - Math.abs(b[1] - 8); |
| }); |
| |
| let axisCount, enabled, |
| numberOfAxisItems = 0, |
| columns = matrix.columns(false); // filterNumberColumns false: number columns will be filtered below |
| |
| // all x/y-axis for selection |
| for (let c1 = 0; c1 < columns.length; c1++) { |
| let content, $div, $yDiv, |
| column1 = columns[c1]; |
| |
| // Check if data-spread is too large. This is a problem in large tables where a column has unique values. |
| // We cannot create DOM elements for each unique value because this causes all browser to stop script |
| // execution. May be in a later release we could implement some sort of data aggregation, but this is not |
| // a simple task on the UI layer, because it requires some know-how about the entity represented by the table, |
| // which we don't have in the UI. Another possible solution: make the charts scrollable, however this is |
| // probably not a good idea, because with a lot of data, the chart fails to provide an oversight over the data |
| // when the user must scroll and only sees a small part of the chart. |
| if (column1 instanceof DateColumn) { |
| // dates are always aggregated and thus we must not check if the chart has "too much data". |
| enabled = true; |
| } else { |
| axisCount = this._axisCount(columnCount, column1); |
| enabled = (axisCount <= ChartTableControl.MAX_AXIS_COUNT); |
| } |
| |
| content = this._axisContentForColumn(column1); |
| |
| $div = this.$contentContainer |
| .makeDiv('select-axis', this._plainAxisText(column1, content.text)) |
| .data('column', column1) |
| .setEnabled(enabled); |
| |
| if (!enabled) { |
| if (this.chartGroup1 && this.chartGroup1.id === column1.id) { |
| this.chartGroup1 = null; |
| this.chartGroup2 = null; |
| } |
| if (this.chartGroup2 && this.chartGroup2.id === column1.id) { |
| this.chartGroup2 = null; |
| } |
| } |
| |
| if (content.icon) { |
| $div.addClass(content.icon.appendCssClass('font-icon')); |
| } |
| |
| if (column1 instanceof DateColumn) { |
| $div |
| .data('modifier', TableMatrix.DateGroup.YEAR) |
| .appendDiv('select-axis-group', this.dateGroup[0][1]); |
| } |
| |
| // install click handler or tooltip |
| if (enabled) { |
| $div.on('click', this._onClickChartGroup.bind(this)); |
| tooltips.installForEllipsis($div, { |
| parent: this |
| }); |
| } else { |
| tooltips.install($div, { |
| parent: this, |
| text: this.session.text('ui.TooMuchData') |
| }); |
| } |
| |
| numberOfAxisItems++; |
| $yDiv = $div.clone(true); |
| this._chartGroup1Map[column1.id] = $div; |
| this._chartGroup2Map[column1.id] = $yDiv; |
| this.$xAxisSelect.append($div); |
| this.$yAxisSelect.append($yDiv); |
| } |
| |
| if (numberOfAxisItems < 2) { |
| let $bubbleSelect = this.$contentContainer.find('.chart-bubble.select-chart'); |
| if ($bubbleSelect) { |
| $bubbleSelect.remove(); |
| } |
| } |
| |
| // map for selection (column id, $element) |
| this._aggregationMap = {}; |
| |
| if (this._hasColumns()) { |
| // create container for data |
| this.$dataSelect = this.$contentContainer.appendDiv('data-select'); |
| |
| // add data-count for no column restriction (all columns) |
| let countDesc = this.session.text('ui.Count'); |
| this._aggregationMap.all = this.$dataSelect |
| .appendDiv('select-data data-count', countDesc) |
| .data('column', null) |
| .data('modifier', TableMatrix.NumberGroup.COUNT); |
| |
| // all data for selection |
| for (let c2 = 0; c2 < columns.length; c2++) { |
| let column2 = columns[c2]; |
| let fakeNumberLabelCol2 = c2 + 1; |
| |
| if (column2 instanceof NumberColumn) { |
| let columnText; |
| if (strings.hasText(column2.text)) { |
| columnText = this._plainAxisText(column2, column2.text); |
| } else if (strings.hasText(column2.headerTooltipText)) { |
| columnText = column2.headerTooltipText; |
| } else { |
| columnText = '[' + fakeNumberLabelCol2 + ']'; |
| } |
| |
| this._aggregationMap[column2.id] = this.$dataSelect |
| .appendDiv('select-data data-sum', columnText) |
| .data('column', column2) |
| .data('modifier', TableMatrix.NumberGroup.SUM); |
| } |
| } |
| |
| // click handling for data |
| $('.select-data', this.$contentContainer) |
| .on('click', this._onClickAggregation.bind(this)); |
| } |
| |
| return columnCount; |
| } |
| |
| _initializeSelection(columnCount) { |
| let $axisColumns; |
| |
| if (!this.chartType) { |
| this.setChartType(Chart.Type.BAR); |
| } |
| |
| // no id selected |
| if (!this.chartAggregation || !this._aggregationMap[this.chartAggregation.id]) { |
| this._setChartAggregation({ |
| id: null, |
| modifier: TableMatrix.NumberGroup.COUNT |
| }); |
| } |
| |
| // apply default selection |
| if (!this.chartGroup1 || !this.chartGroup1.id || !this._chartGroup1Map[this.chartGroup1.id]) { |
| $axisColumns = this.$xAxisSelect.children(':not(.disabled)'); |
| this._setDefaultSelectionForGroup(1, columnCount, $axisColumns, 0 /* only use the first column for the first group */); |
| } |
| if (!this.chartGroup2 || !this.chartGroup2.id || !this._chartGroup2Map[this.chartGroup2.id]) { |
| $axisColumns = this.$yAxisSelect.children(':not(.disabled)'); |
| this._setDefaultSelectionForGroup(2, columnCount, $axisColumns, 1 /* try to use the second column for the second group (if available). Otherwise the first column is used. */); |
| } |
| } |
| |
| /** |
| * Applies the default column selection for the specified chartGroup. |
| * The implementation only considers columns that are part of the specified columnCount matrix and $candidates array. |
| * From all these columns the last match that is lower or equal to the specified maxIndex is set as default chart group. |
| * |
| * @param {number} chartGroup The number of the chart group (1 or 2) for which the default column should be set. |
| * @param {matrix} columnCount Column-count matrix as returned by TableMatrix#columnCount(). Holds possible grouping columns. |
| * @param {array} $candidates jQuery array holding all axis columns that could be used as default. |
| * @param {number} maxIndex The maximum column index to use as default column for the specified chartGroup. |
| */ |
| _setDefaultSelectionForGroup(chartGroup, columnCount, $candidates, maxIndex) { |
| let col = this._getDefaultSelectedColumn(columnCount, $candidates, maxIndex); |
| if (col) { |
| this._setChartGroup(chartGroup, this._getDefaultChartGroup(col)); |
| } |
| } |
| |
| _getDefaultSelectedColumn(columnCount, $candidates, maxIndex) { |
| let matchCounter = 0, |
| curColumn, |
| result; |
| for (let j = 0; j < columnCount.length && matchCounter <= maxIndex; j++) { |
| curColumn = columnCount[j][0]; |
| if (this._existsInAxisColumns($candidates, curColumn)) { |
| result = curColumn; // remember possible result |
| matchCounter++; |
| } |
| } |
| return result; |
| } |
| |
| _existsInAxisColumns($candidates, columnToSearch) { |
| for (let i = 0; i < $candidates.length; i++) { |
| if ($candidates.eq(i).data('column') === columnToSearch) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| _getDefaultChartGroup(column) { |
| let modifier; |
| if (column instanceof DateColumn) { |
| modifier = 256; |
| } |
| return { |
| id: column.id, |
| modifier: modifier |
| }; |
| } |
| |
| _renderChartParts() { |
| this._renderChartType(); |
| this._renderChartAggregation(); |
| this._renderChartGroup1(); |
| this._renderChartGroup2(); |
| } |
| |
| _drawChart() { |
| if (!this._hasColumns()) { |
| this._hideChart(); |
| return; |
| } |
| |
| let cube = this._calculateValues(); |
| |
| if (cube.length) { |
| this.chart.setVisible(true); |
| } else { |
| this._hideChart(); |
| return; |
| } |
| |
| let config = { |
| type: this.chartType, |
| options: { |
| handleResize: true, |
| colorScheme: this.chartColorScheme, |
| maxSegments: 5, |
| legend: { |
| display: false |
| } |
| } |
| }; |
| |
| let iconClasses = []; |
| config.data = this._computeData(iconClasses, cube); |
| this._adjustFont(config, iconClasses); |
| |
| this._adjustConfig(config); |
| |
| this.chart.setConfig(config); |
| |
| let checkedItems = this._computeCheckedItems(config.data.datasets[0].deterministicKeys); |
| this.chart.setCheckedItems(checkedItems); |
| } |
| |
| _hideChart() { |
| this.chart.setConfig({ |
| type: this.chartType |
| }); |
| this.chart.setVisible(false); |
| } |
| |
| _getDatasetLabel() { |
| let elem = this._aggregationMap[this.chartAggregation.id || 'all']; |
| return (elem ? elem.text() : null) || this.session.text('ui.Value'); |
| } |
| |
| _calculateValues() { |
| // build matrix |
| let matrix = new TableMatrix(this.table, this.session); |
| |
| // aggregation (data axis) |
| let tableData = this.chartAggregation.id ? this._aggregationMap[this.chartAggregation.id].data('column') : -1; |
| matrix.addData(tableData, this.chartAggregation.modifier); |
| |
| // find xAxis |
| if (this.chartGroup1) { |
| let axis = this._chartGroup1Map[this.chartGroup1.id].data('column'); |
| this.xAxis = matrix.addAxis(axis, this.chartGroup1.modifier); |
| } |
| |
| // find yAxis |
| // in case of bubble |
| if (this.chartType === Chart.Type.BUBBLE && this.chartGroup2) { |
| let axis2 = this._chartGroup2Map[this.chartGroup2.id].data('column'); |
| this.yAxis = matrix.addAxis(axis2, this.chartGroup2.modifier); |
| } else { |
| this.yAxis = null; |
| } |
| |
| // return not possible to draw chart |
| if (matrix.isEmpty() || !matrix.isMatrixValid()) { |
| return false; |
| } |
| |
| // calculate matrix |
| return matrix.calculate(); |
| } |
| |
| _getXAxis() { |
| return this.xAxis; |
| } |
| |
| _getYAxis() { |
| return this.yAxis; |
| } |
| |
| _computeData(iconClasses, cube) { |
| let data = { |
| datasets: [{ |
| label: this._getDatasetLabel() |
| }] |
| }; |
| if (!cube) { |
| return data; |
| } |
| iconClasses = iconClasses || []; |
| |
| let segments = []; |
| |
| if (this.chartType === Chart.Type.BUBBLE) { |
| segments = this._computeBubbleData(iconClasses, cube); |
| } else { |
| let xAxis = this._getXAxis(); |
| for (let x = 0; x < xAxis.length; x++) { |
| let label, |
| keyX = xAxis[x]; |
| if (xAxis.column instanceof NumberColumn) { |
| label = keyX; |
| } else { |
| label = this._handleIconLabel(xAxis.format(keyX), xAxis, iconClasses); |
| } |
| segments.push({ |
| value: cube.getValue([keyX])[0], |
| label: label, |
| deterministicKey: xAxis.keyToDeterministicKey(keyX) |
| }); |
| } |
| if (this.chartType === Chart.Type.PIE) { |
| segments.sort((a, b) => { |
| return (b.value - a.value); |
| }); |
| } |
| } |
| let dataset = data.datasets[0], |
| labels = []; |
| |
| dataset.data = []; |
| dataset.deterministicKeys = []; |
| |
| segments.forEach(elem => { |
| dataset.data.push(elem.value); |
| dataset.deterministicKeys.push(elem.deterministicKey); |
| if (!objects.isNullOrUndefined(elem.label)) { |
| labels.push(elem.label); |
| } |
| }); |
| |
| if (labels.length) { |
| data.labels = labels; |
| } |
| |
| // duplicate the dataset for pie charts, this is necessary for datalabels on the segments and outside of the pie chart |
| if (this.chartType === Chart.Type.PIE) { |
| data.datasets[1] = $.extend(true, {}, dataset); |
| } |
| |
| return data; |
| } |
| |
| _computeBubbleData(iconClasses, cube) { |
| if (!cube) { |
| return []; |
| } |
| iconClasses = iconClasses || []; |
| |
| let xAxis = this._getXAxis(), |
| yAxis = this._getYAxis(), |
| segments = []; |
| for (let x = 0; x < xAxis.length; x++) { |
| let keyX = xAxis[x], |
| xValue = keyX; |
| this._handleIconLabel(xAxis.format(keyX), xAxis, iconClasses); |
| if (!(xAxis.column instanceof NumberColumn) && xValue === null) { |
| xValue = xAxis.max; |
| } |
| if (xAxis.column instanceof DateColumn) { |
| xValue = xValue - xAxis.min; |
| } |
| for (let y = 0; y < yAxis.length; y++) { |
| let keyY = yAxis[y], |
| yValue = keyY, |
| cubeValues = cube.getValue([keyX, keyY]); |
| this._handleIconLabel(yAxis.format(keyY), yAxis, iconClasses); |
| if (cubeValues && cubeValues.length) { |
| if (!(yAxis.column instanceof NumberColumn) && yValue === null) { |
| yValue = yAxis.max; |
| } |
| if (yAxis.column instanceof DateColumn) { |
| yValue = yValue - yAxis.min; |
| } |
| segments.push({ |
| value: { |
| x: xValue, |
| y: yValue, |
| z: cubeValues[0] |
| }, |
| deterministicKey: [xAxis.keyToDeterministicKey(keyX), yAxis.keyToDeterministicKey(keyY)] |
| }); |
| } |
| } |
| } |
| return segments; |
| } |
| |
| _handleIconLabel(label, axis, iconClasses) { |
| if (axis && axis.textIsIcon) { |
| let icon = icons.parseIconId(label); |
| if (icon && icon.isFontIcon()) { |
| iconClasses.push(...icon.appendCssClass('font-icon').split(' ')); |
| return icon.iconCharacter; |
| } |
| } |
| return label; |
| } |
| |
| _adjustFont(config, iconClasses) { |
| if (!config || !iconClasses) { |
| return; |
| } |
| |
| iconClasses = iconClasses.filter((value, index, self) => { |
| return self.indexOf(value) === index; |
| }); |
| if (iconClasses.length) { |
| let fontFamily = styles.get(iconClasses, 'font-family').fontFamily; |
| if (this.chartType !== Chart.Type.PIE) { |
| config.options = $.extend(true, {}, config.options, { |
| scales: { |
| xAxes: [{ |
| ticks: { |
| fontFamily: fontFamily |
| } |
| }], |
| yAxes: [{ |
| ticks: { |
| fontFamily: fontFamily |
| } |
| }] |
| } |
| }); |
| } |
| config.options = $.extend(true, {}, config.options, { |
| tooltips: { |
| titleFontFamily: fontFamily |
| } |
| }); |
| } |
| } |
| |
| _adjustLabels(config) { |
| if (!config) { |
| return; |
| } |
| |
| let xAxis = this._getXAxis(), |
| yAxis = this._getYAxis(); |
| if (this.chartType === Chart.Type.BUBBLE) { |
| if (!(xAxis.column instanceof NumberColumn)) { |
| config.options = $.extend(true, {}, config.options, { |
| scales: { |
| xAxes: [{ |
| ticks: { |
| callback: label => this._formatLabel(label, xAxis) |
| } |
| }] |
| } |
| }); |
| } |
| if (!(yAxis.column instanceof NumberColumn)) { |
| config.options = $.extend(true, {}, config.options, { |
| scales: { |
| yAxes: [{ |
| ticks: { |
| callback: label => this._formatLabel(label, yAxis) |
| } |
| }] |
| } |
| }); |
| } |
| } else { |
| if (xAxis.column instanceof NumberColumn) { |
| config.options = $.extend(true, {}, config.options, { |
| reformatLabels: true |
| }); |
| } |
| } |
| } |
| |
| _formatLabel(label, axis) { |
| if (axis) { |
| if (axis.column instanceof DateColumn) { |
| label = label + axis.min; |
| if (label !== parseInt(label) || (axis.length < 2 && (label < axis.min || label > axis.max))) { |
| return null; |
| } |
| } |
| if (axis.indexOf(null) !== -1) { |
| if (label === axis.max) { |
| label = null; |
| } else if (label > axis.max) { |
| return null; |
| } |
| } |
| label = axis.format(label); |
| if (axis.textIsIcon) { |
| let icon = icons.parseIconId(label); |
| if (icon && icon.isFontIcon()) { |
| label = icon.iconCharacter; |
| } |
| } |
| } |
| return label; |
| } |
| |
| _adjustConfig(config) { |
| if (!config) { |
| return; |
| } |
| |
| this._adjustLabels(config); |
| this._adjustClickable(config); |
| |
| if (this.chartType === Chart.Type.BUBBLE) { |
| this._adjustBubble(config); |
| } else if (this.chartType === Chart.Type.PIE) { |
| this._adjustPie(config); |
| } else { |
| this._adjustScales(config); |
| } |
| } |
| |
| _adjustClickable(config) { |
| if (!config) { |
| return; |
| } |
| |
| if (this._isChartClickable()) { |
| config.options = $.extend(true, {}, config.options, { |
| clickable: true, |
| checkable: true, |
| otherSegmentClickable: true |
| }); |
| } |
| } |
| |
| _isChartClickable() { |
| return true; |
| } |
| |
| _adjustBubble(config) { |
| if (!config || this.chartType !== Chart.Type.BUBBLE) { |
| return; |
| } |
| |
| config.bubble = $.extend(true, {}, config.bubble, { |
| sizeOfLargestBubble: 25, |
| minBubbleSize: 5 |
| }); |
| } |
| |
| _adjustPie(config) { |
| if (!config || this.chartType !== Chart.Type.PIE) { |
| return; |
| } |
| |
| // first dataset is hidden but datalabels are displayed outside of the chart |
| config.data.datasets[0].weight = 0; |
| config.data.datasets[0].datalabels = { |
| display: 'auto', |
| color: styles.get([this.chartColorScheme, this.chartType + '-chart', 'elements', 'label'], 'fill').fill, |
| formatter: (value, context) => { |
| return context.chart.data.labels[context.dataIndex]; |
| }, |
| anchor: 'end', |
| align: 'end', |
| clamp: true, |
| offset: 10, |
| padding: 4 |
| }; |
| |
| config.options = $.extend(true, {}, config.options, { |
| plugins: { |
| datalabels: { |
| display: true |
| } |
| } |
| }); |
| // Compensate the margin of the container so that the chart is always centered vertically |
| let margin = this.chart.$container.cssMarginTop() - this.chart.$container.cssMarginBottom(); |
| config.options = $.extend(true, {}, config.options, { |
| layout: { |
| padding: { |
| top: 30 + (Math.sign(margin) < 0 ? Math.abs(margin) : 0), |
| bottom: 30 + (Math.sign(margin) > 0 ? margin : 0) |
| } |
| } |
| }); |
| } |
| |
| _adjustScales(config) { |
| if (!config) { |
| return; |
| } |
| |
| config.options = $.extend(true, {}, config.options, { |
| scales: { |
| xAxes: [{ |
| ticks: { |
| beginAtZero: true |
| } |
| }], |
| yAxes: [{ |
| ticks: { |
| beginAtZero: true |
| } |
| }] |
| } |
| }); |
| } |
| |
| _computeCheckedItems(deterministicKeys) { |
| if (!deterministicKeys) { |
| return []; |
| } |
| |
| let xAxis = this._getXAxis(), |
| yAxis = this._getYAxis(), |
| tableFilter = this.table.getFilter(ChartTableUserFilter.TYPE), |
| filters = [], |
| checkedIndices = []; |
| |
| if (tableFilter && (tableFilter.xAxis || {}).column === (xAxis || {}).column && (tableFilter.yAxis || {}).column === (yAxis || {}).column) { |
| filters = tableFilter.filters; |
| } |
| |
| deterministicKeys.forEach((deterministicKey, idx) => { |
| if (filters.filter(filter => (Array.isArray(filter.deterministicKey) && Array.isArray(deterministicKey)) ? arrays.equals(filter.deterministicKey, deterministicKey) : filter.deterministicKey === deterministicKey).length) { |
| checkedIndices.push(idx); |
| } |
| }); |
| let datasetIndex = 0; |
| if (this.chartType === Chart.Type.PIE) { |
| let maxSegments = this.chart.config.options.maxSegments, |
| collapsedIndices = arrays.init(deterministicKeys.length - maxSegments).map((elem, idx) => idx + maxSegments); |
| if (!arrays.containsAll(checkedIndices, collapsedIndices)) { |
| arrays.remove(checkedIndices, maxSegments - 1); |
| } |
| arrays.removeAll(checkedIndices, collapsedIndices); |
| |
| // first dataset is hidden on pie charts |
| datasetIndex = 1; |
| } |
| |
| let checkedItems = []; |
| if (checkedIndices.length) { |
| checkedIndices.forEach(index => { |
| checkedItems.push({ |
| datasetIndex: datasetIndex, |
| dataIndex: index |
| }); |
| }); |
| } |
| |
| return checkedItems; |
| } |
| |
| _onChartValueClick() { |
| // prepare filter |
| let filters = []; |
| if (this.chart && this.chart.config.data) { |
| let maxSegments = this.chart.config.options.maxSegments, |
| // first dataset is hidden on pie charts |
| datasetIndex = this.chartType === Chart.Type.PIE ? 1 : 0, |
| dataset = this.chart.config.data.datasets[datasetIndex], |
| getFilters = index => ({deterministicKey: dataset.deterministicKeys[index]}); |
| if (this.chartType === Chart.Type.PIE) { |
| getFilters = index => { |
| index = parseInt(index); |
| if (maxSegments && maxSegments === index + 1) { |
| return arrays.init(dataset.deterministicKeys.length - index).map((elem, idx) => ({deterministicKey: dataset.deterministicKeys[idx + index]})); |
| } |
| return {deterministicKey: dataset.deterministicKeys[index]}; |
| }; |
| } |
| |
| let checkedIndices = this.chart.checkedItems.filter(item => item.datasetIndex === datasetIndex) |
| .map(item => item.dataIndex); |
| checkedIndices.forEach(index => { |
| arrays.pushAll(filters, getFilters(index)); |
| }); |
| } |
| |
| // filter function |
| if (filters.length) { |
| let filter = scout.create('ChartTableUserFilter', { |
| session: this.session, |
| table: this.table, |
| text: this.tooltipText, |
| xAxis: this._getXAxis(), |
| yAxis: this._getYAxis(), |
| filters: filters |
| }); |
| |
| this.table.addFilter(filter); |
| } else { |
| this.table.removeFilterByKey(ChartTableUserFilter.TYPE); |
| } |
| |
| this.table.filter(); |
| } |
| |
| _axisContentForColumn(column) { |
| let icon, |
| text = column.text; |
| |
| if (strings.hasText(text)) { |
| return { |
| text: text |
| }; |
| } |
| |
| if (column.headerIconId) { |
| icon = icons.parseIconId(column.headerIconId); |
| if (icon.isFontIcon()) { |
| return { |
| text: icon.iconCharacter, |
| icon: icon |
| }; |
| } |
| } |
| |
| if (column.headerTooltipText) { |
| return { |
| text: column.headerTooltipText |
| }; |
| } |
| |
| return { |
| text: '[' + (this.table.visibleColumns().indexOf(column) + 1) + ']' |
| }; |
| } |
| |
| _removeContent() { |
| this._removeScrollbars(); |
| this.$contentContainer.remove(); |
| this.chart.remove(); |
| this.table.events.removeListener(this._filterResetListener); |
| this._removeListeners(); |
| this.oldChartType = null; |
| this.recomputeEnabled(); |
| } |
| |
| _removeScrollbars() { |
| this.$xAxisSelect.each((index, element) => { |
| tooltips.uninstall($(element)); |
| }); |
| this.$yAxisSelect.each((index, element) => { |
| tooltips.uninstall($(element)); |
| }); |
| this._uninstallScrollbars(); |
| } |
| |
| _removeListeners() { |
| this.table.off('rowsInserted', this._tableUpdatedHandler); |
| this.table.off('rowsDeleted', this._tableUpdatedHandler); |
| this.table.off('allRowsDeleted', this._tableUpdatedHandler); |
| this.chart.off('valueClick', this._chartValueClickedHandler); |
| } |
| |
| _pathSegment(mx, my, r, start, end) { |
| let s = start * 2 * Math.PI, |
| e = end * 2 * Math.PI, |
| pathString = ''; |
| |
| pathString += 'M' + (mx + r * Math.sin(s)) + ',' + (my - r * Math.cos(s)); |
| pathString += 'A' + r + ', ' + r; |
| pathString += (end - start < 0.5) ? ' 0 0,1 ' : ' 0 1,1 '; |
| pathString += (mx + r * Math.sin(e)) + ',' + (my - r * Math.cos(e)); |
| pathString += 'L' + mx + ',' + my + 'Z'; |
| |
| return pathString; |
| } |
| |
| _onTableUpdated(event) { |
| if (this._tableUpdatedTimeOutId) { |
| return; |
| } |
| |
| this._tableUpdatedTimeOutId = setTimeout(() => { |
| this._tableUpdatedTimeOutId = null; |
| |
| if (!this.rendered) { |
| return; |
| } |
| |
| this._setChartGroup1(null); |
| this._setChartGroup2(null); |
| this.removeContent(); |
| this.renderContent(); |
| }); |
| } |
| |
| _onTableColumnStructureChanged() { |
| this.recomputeEnabled(); |
| if (this.contentRendered && this.selected) { |
| this._onTableUpdated(); |
| } |
| } |
| } |