@furo/ui5
Version:
SAP UI5 Web Components data bindings for furo-web
505 lines (471 loc) • 13.4 kB
JavaScript
import { LitElement, css } from 'lit';
/**
* furo-ui5-chart connects data objects (repeaterNodes) with the charting lib.
*
* Use multiple binders if you need more then one series per chart.
*
* ```html
* <furo-ui5-chart-display chart-type="bar">
* <furo-ui5-chart
* fn-bind-data="--projectDAO(*.entities)"
* data-field="data.cost_limit.units"
* category-field="data.description"
* ></furo-ui5-chart>
* </furo-ui5-chart-display>
* ```
*
* @fires {Fieldnode} data-point-clicked - Fired when a marker for this data source was clicked
* @fires {data-series} data-updated - Fired when datasource has updated data
*
* @summary connect data to a chart
* @element furo-ui5-chart
* @demo demo-furo-ui5-chart-mini Small Charts
* @demo demo-furo-ui5-chart Basic Usage
* @demo demo-furo-ui5-chart-mixed Mixed Charts
* @demo demo-furo-ui5-chart-stacked Stacked Charts
* @demo demo-furo-ui5-chart-timeline Timeline Chart
* @demo demo-furo-ui5-chart-treemap Treemap
* @demo demo-furo-ui5-chart-bubble Bubble
* @appliesMixin FBP
*/
export class FuroUi5Chart extends LitElement {
constructor() {
super();
this.strokeWidth = 1;
this.markerSize = 0;
this.strokeCurve = 'straight'; // 'smooth', 'straight', 'stepline'
/**
* Apex options object
* @type {Object} Apex options
* @private
*/
this.options = {
seriesName: undefined,
legendLabel: undefined, // custom field
opposite: false,
axisTicks: {
show: false,
borderType: 'solid',
color: '#666666',
width: 6,
offsetX: 0,
offsetY: 0,
},
axisBorder: {
show: false,
color: '#666666',
stroke: 1,
},
labels: {
show: false,
style: {
colors: '#666666',
},
},
title: {
style: {
color: '#666666',
},
},
tooltip: {
enabled: false,
offsetX: 0,
},
};
this._initEmptySeries();
}
/**
* @private
* @return {Object}
*/
static get properties() {
return {
/**
* Define the data field or fields here. For most charts this is **the y axis**.
*
* Some charts requires more then one data field (i.e. bubbles want at least 3 fields `data-field="data.start.day, data.end.day, data.start.day"` ).
*
* @type String
*/
dataField: { type: String, attribute: 'data-field' },
/**
* Define the category field here (only 1 field). You can think of this as it is the x axis for your chart.
*
* @type String
*/
categoryField: { type: String, attribute: 'category-field' },
/**
* Label the Series for the legend. This text is also shown on the tooltips. This is useful when you have more then one data series.
*
* @type String
*/
legendLabel: { type: String, attribute: 'legend-label' },
/**
* Series with same name will get the same y-axis
*
* @type String
*/
seriesName: { type: String, attribute: 'series-name' },
/**
* **Use this for mixed charts scenarios only**, prefer to define the chart-type on the chart-display.
* Specify the default type on the display and set the custom type on this binder.
*
* @type String
*/
chartType: { type: String, attribute: 'chart-type' },
/**
* If you need to give an explicit color for the series you can use this attribute.
*
* @type String
*/
chartColor: { type: String, attribute: 'chart-color' },
/**
* Put the axis label on the opposite site (usually right)
*
* @type Boolean
*/
axisLabelOpposite: { type: Boolean, attribute: 'axis-label-opposite' },
/**
* Text for the y axis
*
* @type String
*/
axisLabel: { type: String, attribute: 'axis-label' },
/**
* Custom color for the y axis description text
*
* @type String
*/
axisLabelColor: { type: String, attribute: 'axis-label-color' },
/**
* Show a border on the right side of the y axis descriptions and labels.
*
* @type Boolean
*/
axisBorder: { type: Boolean, attribute: 'axis-border' },
/**
* Custom color for the border.
*
* @type String
*/
axisBorderColor: { type: String, attribute: 'axis-border-color' },
/**
* Show tick marks on the y axis.
*
* @type Boolean
*/
axisTicks: { type: Boolean, attribute: 'axis-ticks' },
/**
* Show a tooltip with the current value while hovering.
*
* @type Boolean
*/
axisTooltip: { type: Boolean, attribute: 'axis-tooltip' },
/**
* Custom color for the ticks
*
* @type String
*/
axisTicksColor: { type: String, attribute: 'axis-ticks-color' },
/**
* Define the curve style for line and area charts.
*
* Possible values: `smooth`, `straight`, `stepline`
*
* @type String
*/
strokeCurve: { type: String, attribute: 'chart-curve' },
/**
* Define the thickness of the lines in px.
*
* @type Number
*/
strokeWidth: { type: Number, attribute: 'chart-stroke-width' },
/**
* Set the size of the markers (hover state) in px.
*
* @type Number
*/
markerSize: { type: Number, attribute: 'chart-marker-size' },
/**
*
*
* @type String
*/
xaxis: { type: String },
};
}
set axisLabelOpposite(v) {
this.options.opposite = v;
}
set axisLabel(v) {
this.options.title.text = v;
this.options.labels.show = true;
}
set axisLabelColor(v) {
this.options.labels.style.colors = v;
this.options.title.style.color = v;
}
set axisBorder(v) {
this.options.axisBorder.show = v;
}
set axisBorderColor(v) {
this.options.axisBorder.color = v;
}
set axisTicks(v) {
this.options.axisTicks.show = v;
}
set axisTooltip(v) {
this.options.tooltip.enabled = v;
}
set axisTicksColor(v) {
this.options.axisTicks.color = v;
}
set seriesName(v) {
this.options.seriesName = v;
}
set legendLabel(v) {
this.options.legendLabel = v;
}
/**
* Binds a RepeaterNode to the component.
*
* Supported types: repeated types
*
* @param data {RepeaterNode}
*/
bindData(data) {
if (data._isRepeater === true) {
this.repeater = data;
this.repeater.addEventListener('this-repeated-field-changed', () => {
this._convertData();
});
} else {
// eslint-disable-next-line no-console
console.warn('Please bind a repeater node', this);
}
}
/**
*
* @param e
* @param context
* @param config
* @private
*/
_dataPointSelection(e, context, config) {
if (config.dataPointIndex !== undefined) {
const customEvent = new Event('data-point-clicked', {
composed: true,
bubbles: true,
});
customEvent.detail = this.repeater.repeats[config.dataPointIndex];
this.dispatchEvent(customEvent);
}
}
/**
* Prepares the data for the according chart display
*
* some charts want 1 dimensional data (pie, donut) and others expect more dimensions (boxplot,...)
*
* @private
*/
_convertData() {
this._initEmptySeries();
const graphType = this.parentElement.getAttribute('chart-type');
const dataFields = this.dataField.replace(/ /g, '').split(',');
switch (graphType) {
case 'donut':
case 'radialBar':
case 'polarArea':
case 'pie':
this.repeater.repeats.forEach(row => {
if (this.categoryField && this._pathGet(row, this.categoryField)) {
this.categories.push(this._pathGet(row, this.categoryField)._value);
} else {
this.categories.push('');
}
if (this._pathGet(row, dataFields[0])) {
this.dataSeries.data.push(this._pathGet(row, dataFields[0])._value);
} else {
this.dataSeries.data.push(null);
}
});
break;
case 'bubble':
/**
* bubble series expects following format:
* series = [{
* data: [
* [3, 3, 3],
* [4, 4, 4],
* [1, 1, 1],
* ],
* }];
*
*/
this.repeater.repeats.forEach(row => {
const v = [];
// build multidimensional data
if (dataFields.length === 3) {
v.y = [];
dataFields.forEach(field => {
if (this._pathGet(row, field)) {
v.push(this._pathGet(row, field)._value);
} else {
v.push(null);
}
});
} else {
// eslint-disable-next-line no-console
console.warn('You must give exact 3 fields for bubble charts');
}
this.dataSeries.data.push(v);
});
break;
case 'line':
default:
this.repeater.repeats.forEach(row => {
const v = { x: '', y: null };
if (this.categoryField && this._pathGet(row, this.categoryField)) {
const fieldNode = this._pathGet(row, this.categoryField);
if (fieldNode._spec.type === 'google.type.Date') {
const date = new Date(
Date.UTC(
fieldNode.year._value,
fieldNode.month._value - 1,
fieldNode.day._value,
0,
0,
0,
0
)
);
v.x = date.getTime();
} else {
v.x = fieldNode._value;
}
}
if (dataFields.length === 1) {
if (this._pathGet(row, dataFields[0])) {
const fieldNode = this._pathGet(row, this.dataField);
if (fieldNode._spec.type === 'google.type.Date') {
const date = new Date(
Date.UTC(
fieldNode.year._value,
fieldNode.month._value - 1,
fieldNode.day._value,
0,
0,
0,
0
)
);
v.y = date.getTime();
} else {
v.y = fieldNode._value;
}
} else {
v.y = null;
}
}
// build multidimensional data
if (dataFields.length > 1) {
v.y = [];
dataFields.forEach(field => {
if (this._pathGet(row, field)) {
const fieldNode = this._pathGet(row, field);
if (fieldNode._spec.type === 'google.type.Date') {
const date = new Date(
Date.UTC(
fieldNode.year._value,
fieldNode.month._value - 1,
fieldNode.day._value,
0,
0,
0,
0
)
);
v.y.push(date.getTime());
} else {
v.y.push(fieldNode._value);
}
} else {
v.y.push(null);
}
});
}
this.dataSeries.data.push(v);
});
}
const customEvent = new Event('data-updated', {
composed: true,
bubbles: true,
});
customEvent.detail = this;
this.dispatchEvent(customEvent);
}
/**
*
* @private
*/
_initEmptySeries() {
// data series
this.dataSeries = { name: this.options.seriesName, data: [] };
this.categories = [];
// set type if given
if (this.chartType) {
this.dataSeries.type = this.chartType;
}
// set color if given
if (this.chartColor) {
this.dataSeries.color = this.chartColor;
}
}
/**
* Reads a value from a path. If any sub-property in the path is `undefined`,
* this method returns `undefined` (will never throw.
*
* @param {Object} root Object from which to dereference path from
* @param {string | !Array<string|number>} path Path to read
* @return {*} Value at path, or `undefined` if the path could not be fully dereferenced.
* @private
*/
_pathGet(root, path) {
let prop = root;
const parts = this._split(path);
// Loop over path parts[0..n-1] and dereference
for (let i = 0; i < parts.length; i += 1) {
if (!prop) {
return false;
}
const part = parts[i];
prop = prop[part];
}
return prop;
}
/**
*
* @param path
* @return {string[]}
* @private
*/
// eslint-disable-next-line class-methods-use-this
_split(path) {
return path.toString().split('.');
}
/**
* Themable Styles
* @private
* @return {CSSResult}
*/
static get styles() {
// language=CSS
return css`
:host {
display: none;
}
`;
}
}
window.customElements.define('furo-ui5-chart', FuroUi5Chart);