/** * This class is used to initialize the dimensions defined on the pivot grid leftAxis, * topAxis and aggregate. */Ext.define('Ext.pivot.dimension.Item', { alternateClassName: [ 'Mz.aggregate.dimension.Item' ], requires: [ 'Ext.pivot.MixedCollection', 'Ext.pivot.filter.Label', 'Ext.pivot.filter.Value', 'Ext.app.bind.Template' ], $configPrefixed: false, $configStrict: false, config: { /** * @cfg {String} id * Unique id of this dimension. */ id: null, /** * @cfg {String} header (required) * * This text is visible in the pivot grid in the following cases: * * - the dimension is defined on the left axis. The pivot grid will generate one grid column * per dimension and this header will go into the grid column header. * * - the dimension is defined on the aggregate. The pivot grid will generate one grid column * per dimension per top axis label. If there are at least 2 aggregate dimensions then this * header will be visible. When only one is defined the aggregate dimension header * is replaced by the top axis label. * * - if the {@link Ext.pivot.plugin.Configurator Configurator plugin} is used then this * header will be visible * in the axis panels. * */ header: '', /** * @cfg {String} dataIndex (required) * The field name on the record from where this dimension extracts data. */ dataIndex: '', /** * @cfg {String} sortIndex * Field name on the record used when sorting this dimension results. Defaults to * {@link #dataIndex} if none is specified. */ sortIndex: '', /** * @cfg {Number} [width=100] * Default column width when this dimension is a left axis or aggregate dimension. * Used by the generated columns. */ width: 100, /** * @cfg {Number} [flex=0] * Column flex when this dimension is a left axis or aggregate dimension. * Used by the generated columns. */ flex: 0, /** * @cfg {String} [align="left"] * Column alignment when this dimension is a left axis or aggregate dimension. * Used by the generated columns. */ align: 'left', /** * @cfg {Boolean} [sortable=true] * Is this dimension sortable when the pivot is generated? */ sortable: true, /** * @cfg {"ASC"/"DESC"} [direction="ASC"] * If this dimension is sortable then this is the type of sorting. */ direction: 'ASC', /** * @cfg {Function} sorterFn * Provide here your own sorting function for this dimension. * If none is specified then the defaultSorterFn is used. */ sorterFn: null, /** * @cfg {Boolean} [caseSensitiveSort=true] * If this dimension is sortable, should we do a case sensitive sort? */ caseSensitiveSort: true, /** * @cfg {Ext.pivot.filter.Base} filter * Provide a filter configuration to filter your axis items. * This works only on left/top axis dimensions. * * Example for a label filter: * * { * dataIndex: 'year', * header: 'Year', * filter: { * type: 'label', * operator: '=', * value: 2012 * } * } * * * Example for a value filter: * * { * dataIndex: 'year', * header: 'Year', * filter: { * type: 'value', * operator: 'between', * value: [2012, 2015] * } * } * * * Example for a top 10 value filter: * * { * dataIndex: 'year', * header: 'Year', * filter: { * type: 'value', * operator: 'top10', * dimensionId: 'value', // this is the id of an aggregate dimension * topType: 'items', * topOrder: 'bottom' * } * } */ filter: null, /** * @cfg {String/Function} labelRenderer * * Callback function or the name of the callback function to execute when labels * are generated for this dimension. * * **Note:** This works when the dimension is used as either left or top axis dimension. * * Example: * * { * xtype: 'pivot', * * matrix: { * topAxis: [{ * dataIndex: 'month' * labelRenderer: function(monthValue){ * return Ext.Date.monthNames[monthValue]; * } * }] * * // ... * } * } * * The above labelRenderer will convert the month value to a textual month name. * * @param {Mixed} value Value that needs to be formatted * @return {String} The label value displayed in the pivot grid */ labelRenderer: null, /* eslint-disable max-len */ /** * @cfg {String/Function} renderer * * Callback function or the name of the callback function that will be attached to the grid * column generated for this dimension. * * **Note:** This works when the dimension is used as either left axis or aggregate * dimension. * * The following example describes how columns are generated by the pivot grid: * * { * xtype: 'pivot', * * matrix: { * leftAxis: [{ * dataIndex: 'country' * }], * * topAxis: [{ * dataIndex: 'year', * labelRenderer: function(v) { * return 'Year ' + v; * } * }], * * aggregate: [{ * dataIndex: 'value', * aggregator: 'sum', * renderer: function(value, metaData, record, rowIndex, colIndex, store, view) { * metaData.tdCls = (value < 0) ? 'redCls' : 'greenCls'; * return Ext.util.Format(value, '0,000.00'); * } * },{ * dataIndex: 'qty', * aggregator: 'sum', * renderer: function(value, metaData, record, rowIndex, colIndex, store, view) { * metaData.tdCls = (value < 0) ? 'redCls' : 'greenCls'; * return Ext.util.Format(value, '0.00'); * } * }] * } * } * * Let's say that we have records for the years 2015 and 2016. In this scenario the resulting grid will have: * * - 1 column for the left axis dimension defined. This column will use the renderer defined on the left * axis dimension * - 4 columns calculated for the top axis results (2) multiplied by the number of aggregate dimensions (2). These columns will * use the renderers defined on the aggregate dimensions and each group column header is generated using * labelRenderer defined on the top axis dimension. * * Read more about grid column renderers {@link Ext.grid.column.Column#renderer here}. * */ /* eslint-enable max-len */ renderer: null, /** * @cfg {String} formatter * * This formatter will be attached to the grid column generated for this dimension. * * **Note:** This works when the dimension is used either as a left axis or an aggregate * dimension. * * Read more about grid column formatters {@link Ext.grid.column.Column#formatter here}. */ formatter: null, /** * @cfg {Object} column * * Configuration object that will be used when the grid columns are generated. Beware that * this object will be merged into each column generated for each aggregate and left axis * dimensions. * * **Note:** This works when the dimension is used either as a left axis or an aggregate * dimension. */ column: null, /** * @cfg {Ext.exporter.file.Style/Ext.exporter.file.Style[]} exportStyle * * Style used during export by the {@link Ext.pivot.plugin.Exporter exporter plugin}. * This style will be applied to the columns generated for the aggregate or left axis * dimensions in the exported file. * * You could define it as a single object that will be used by all exporters: * * aggregate: [{ * dataIndex: 'price', * header: 'Total', * aggregator: 'sum', * exportStyle: { * format: 'Currency', * alignment: { * horizontal: 'Right' * }, * font: { * italic: true * } * } * }] * * You could also define it as an array of objects, each object having a `type` that * specifies by which exporter will be used: * * aggregate: [{ * dataIndex: 'price', * header: 'Total', * aggregator: 'sum', * exportStyle: [{ * type: 'html', // used by the `html` exporter * format: 'Currency', * alignment: { * horizontal: 'Right' * }, * font: { * italic: true * } * }, { * type: 'csv', // used by the `csv` exporter * format: 'General' * }] * }] * * * Or you can define it as an array of objects that has: * * - one object with no `type` key that is considered the style to use by all exporters * - objects with the `type` key defined that are exceptions of the above rule * * * aggregate: [{ * dataIndex: 'price', * header: 'Total', * aggregator: 'sum', * exportStyle: [{ * // no type defined means this is the default * format: 'Currency', * alignment: { * horizontal: 'Right' * }, * font: { * italic: true * } * }, { * type: 'csv', // only the CSV exporter has a special style * format: 'General' * }] * }] * */ exportStyle: null, /** * @cfg {Object} scope * * Scope to run all custom functions defined on the dimension item. */ scope: null, /** * @cfg {Function} grouperFn * * This function is used when the groups are generated for the axis. * It will return the value that will uniquely identify a group on the axis. * * ie: you have a Date field that you want to group by year. * This renderer could return the year from that Date value. * * The function receives one parameter and that is the record. * * It will run using Ext.callback so you can also provide a String that resolves to * the view controller. * * @param {Ext.data.Model} record Record used to extract the group value * @return {String/Number} */ grouperFn: null, /** * @cfg {String} [blankText="(blank)"] * Default text to use when a group name is blank. This value is applied even if you set * your own label renderer. */ blankText: '(blank)', /** * @cfg {Boolean} [showZeroAsBlank=false] * Should 0 values be displayed as blank? This config is used when * this is an aggregate dimension. */ showZeroAsBlank: false, /** * @cfg {String/Function} [aggregator="sum"] * This is the function that should be used to aggregate when this is an aggregate * dimension. * * You can either provide a function name available in {@link Ext.pivot.Aggregators} or * set your own function. * * It's probably best to override {@link Ext.pivot.Aggregators} to add you own function * and use that function name on this config. This way the stateles pivot will save this * value. */ aggregator: 'sum', /** * @cfg {Ext.util.MixedCollection} values * Collection of unique values on this dimension; each item has a "value" and a "display". */ values: [] }, /** * @property {Boolean} isAggregate * True to identify a dimension of an aggregate configuration. */ isAggregate: false, /** * @property {Ext.pivot.matrix.Base} matrix * @readonly * Reference to the matrix object. */ matrix: null, constructor: function(config) { var me = this; this.initConfig(config); if (!me.getId()) { // generate an internal id used by the matrix me.setId(Ext.id()); } //<debug> if (Ext.isEmpty(me.dataIndex)) { Ext.raise('No dataIndex provided to the dimension!'); } //</debug> if (!me.grouperFn) { me.groupFn = Ext.bind(me.defaultGrouperFn, me); } if (me.sortable) { if (!me.sorterFn) { me.sortFn = Ext.bind(me.defaultSorterFn, me); } } else { me.sortFn = Ext.bind(me.manualSorterFn, me); } if (Ext.isEmpty(me.getSortIndex())) { me.setSortIndex(me.getDataIndex()); } if (me.isAggregate && !me.getFormatter() && !me.getRenderer()) { // in most cases the aggregate value is a number me.setRenderer(me.getDefaultFormatRenderer('0,000.00')); } return this.callParent([config]); }, destroy: function() { this.setConfig({ values: null, grouperFn: null, sorterFn: null, filter: null, renderer: null, labelRenderer: null, aggregator: null }); this.callParent(); }, /** * Returns the serialized dimension data. * @return {Object} */ serialize: function() { return this.getConfiguration(true); }, /** * Returns the actual configuration of this dimension. * * @param {Boolean} [serializable=false] Set to true if the result is serializable * and should not include functions * @return {Object} */ getConfiguration: function(serializable) { var me = this, cfg = me.getConfig(); delete(cfg.values); if (cfg.filter) { cfg.filter = cfg.filter.serialize(); } if (serializable && typeof cfg.aggregator === 'function') { cfg.aggregator = 'sum'; } if (serializable && typeof cfg.renderer === 'function') { cfg.renderer = null; } if (serializable && typeof cfg.labelRenderer === 'function') { cfg.labelRenderer = null; } return cfg; }, applyId: function(id) { return id ? id : Ext.id(); }, updateExportStyle: function(style) { if (style && !style.id) { style.id = this.getId(); } }, applyFilter: function(filter, oldFilter) { if (filter == null) { return filter; } if (filter && filter.isFilter) { filter.parent = this; return filter; } if (Ext.isObject(filter)) { Ext.applyIf(filter, { type: 'label', parent: this }); filter = Ext.Factory.pivotfilter(filter); } else { filter = false; } return filter; }, updateAggregator: function(fn) { var aggregators = Ext.pivot.Aggregators; if (Ext.isString(fn) && Ext.isFunction(aggregators[fn])) { this.aggregatorFn = Ext.bind(aggregators[fn], aggregators); } else { this.aggregatorFn = fn || 'sum'; } }, updateGrouperFn: function(fn) { this.groupFn = (Ext.isFunction(fn) ? Ext.bind(fn, this) : fn); }, updateSorterFn: function(fn) { this.sortFn = (Ext.isFunction(fn) ? Ext.bind(fn, this) : fn); }, /** * Add unique values available for this dimension. These are used when filtering. * * @param value * @param display */ addValue: function(value, display) { var values = this.values; if (!values.getByKey(value)) { values.add({ sortValue: value, value: value, display: display }); } }, applyValues: function(values, oldValues) { var ret; Ext.destroy(oldValues); if (values && !values.isInstance) { ret = new Ext.pivot.MixedCollection(); ret.getKey = function(item) { return item.value; }; ret.addAll(values); return ret; } return values; }, sortValues: function() { if (this.sortable) { this.values.sortBy(this.sortFn); } }, /** * Default sorter function used to sort the axis dimensions on the same tree level. * * @param o1 * @param o2 * * @return {Number} */ defaultSorterFn: function(o1, o2) { var me = this, s1 = o1.sortValue, s2 = o2.sortValue, result; if (s1 instanceof Date) { s1 = s1.getTime(); } if (s2 instanceof Date) { s2 = s2.getTime(); } if (!me.caseSensitiveSort) { s1 = typeof s1 === 'string' ? s1.toUpperCase() : s1; s2 = typeof s2 === 'string' ? s2.toUpperCase() : s2; } if (me.matrix.useNaturalSorting) { result = me.matrix.naturalSort(s1, s2); } else { result = (s1 === s2 ? 0 : (s1 < s2 ? -1 : 1)); } if (result < 0 && me.direction === 'DESC') { return 1; } if (result > 0 && me.direction === 'DESC') { return -1; } return result; }, /** * When we have manual sorting then we need to sort the items by the order they appear in the * internal `values` collection (unique values for this dimension). * * @param o1 * @param o2 * @return {number} * @private */ manualSorterFn: function(o1, o2) { // sort items by their appearance in the list of values var v = this.values, i1 = v ? v.indexOfKey(o1.value) : 0, i2 = v ? v.indexOfKey(o2.value) : 0; return (i1 === i2 ? 0 : (i1 < i2 ? -1 : 1)); }, /** * Builds a renderer function by using the specified format. * * @param format Could be either a function or a string */ getDefaultFormatRenderer: function(format) { var me = this; return function(v) { var positive; if (Ext.isEmpty(format)) { return v; } if (Ext.isFunction(format)) { return format.apply(me, arguments); } if (!Ext.isNumber(v)) { return v; } if (me.isAggregate && v === 0 && me.showZeroAsBlank) { return ''; } positive = (v >= 0); v = Math.abs(v); v = Ext.util.Format.number(v, format); return positive ? v : '-' + v; }; }, /** * Default grouper function used for rendering axis item values. * The grouper function can be used to group together multiple items. * Returns a group value * * @param record * @return {String/Number/Date} */ defaultGrouperFn: function(record) { return record.get(this.dataIndex); }, getFormatterFn: function() { var me = this, format = me.getFormatter(), scoped; if (format) { scoped = format.indexOf('this.') === 0; if (scoped) { format = format.substring(5); } format = Ext.app.bind.Template.prototype.parseFormat(format); if (scoped) { format.scope = null; } return function(v) { // eslint-disable-next-line max-len return format.format(v, format.scope || me.getScope() || me.matrix.cmp.resolveListenerScope('self.controller') || this); }; } }, /** * @method * This function is used when we summarize the records for a left/top pair. * * @private */ aggregatorFn: Ext.emptyFn, /** * @method * This function is used when the axis item value is generated. It will either be * the defaultGrouperFn or a custom one. It will run using Ext.callback to you can also provide * a String that resolves to the view controller. * * @private */ groupFn: Ext.emptyFn, /** * @method * This function is used for sorting axis items * * @private */ sortFn: Ext.emptyFn});