@limetech/lime-elements
Version:
472 lines (471 loc) • 15.6 kB
JavaScript
import { h } from '@stencil/core';
import translate from '../../global/translations';
import { createRandomString } from '../../util/random-string';
const PERCENT = 100;
const DEFAULT_INCREMENT_SIZE = 10;
/**
* A chart is a graphical representation of data, in which
* visual symbols such as such bars, dots, lines, or slices, represent
* each data point, in comparison to others.
*
* @exampleComponent limel-example-chart-stacked-bar
* @exampleComponent limel-example-chart-orientation
* @exampleComponent limel-example-chart-max-value
* @exampleComponent limel-example-chart-type-bar
* @exampleComponent limel-example-chart-type-dot
* @exampleComponent limel-example-chart-type-area
* @exampleComponent limel-example-chart-type-line
* @exampleComponent limel-example-chart-type-pie
* @exampleComponent limel-example-chart-type-doughnut
* @exampleComponent limel-example-chart-type-ring
* @exampleComponent limel-example-chart-type-gantt
* @exampleComponent limel-example-chart-type-nps
* @exampleComponent limel-example-chart-multi-axis
* @exampleComponent limel-example-chart-multi-axis-with-negative-start-values
* @exampleComponent limel-example-chart-multi-axis-area-with-negative-start-values
* @exampleComponent limel-example-chart-axis-increment
* @exampleComponent limel-example-chart-clickable-items
* @exampleComponent limel-example-chart-accessibility
* @exampleComponent limel-example-chart-styling
* @exampleComponent limel-example-chart-creative-styling
* @beta
*/
export class Chart {
constructor() {
this.handleClick = (event) => {
const item = this.getClickableItem(event.currentTarget);
if (!item) {
return;
}
event.stopPropagation();
this.interact.emit(item);
};
this.handleKeyDown = (event) => {
if (event.key !== 'Enter' && event.key !== ' ') {
return;
}
const item = this.getClickableItem(event.currentTarget);
if (!item) {
return;
}
event.preventDefault();
this.interact.emit(item);
};
this.language = 'en';
this.accessibleLabel = undefined;
this.accessibleItemsLabel = undefined;
this.items = undefined;
this.type = 'stacked-bar';
this.orientation = 'landscape';
this.maxValue = undefined;
this.axisIncrement = undefined;
this.loading = false;
}
componentWillLoad() {
this.recalculateRangeData();
}
render() {
if (this.loading) {
return h("limel-spinner", { limeBranded: false });
}
return (h("table", { "aria-busy": this.loading ? 'true' : 'false', "aria-live": "polite", style: {
'--limel-chart-number-of-items': this.items.length.toString(),
} }, this.renderCaption(), this.renderTableHeader(), this.renderAxises(), h("tbody", { class: "chart" }, this.renderItems())));
}
renderCaption() {
if (!this.accessibleLabel) {
return;
}
return h("caption", null, this.accessibleLabel);
}
renderTableHeader() {
return (h("thead", null, h("tr", null, h("th", { scope: "col" }, this.accessibleItemsLabel), h("th", { scope: "col" }, translate.get('value', this.language)))));
}
renderAxises() {
if (!['bar', 'dot', 'area', 'line'].includes(this.type)) {
return;
}
const { minValue, maxValue } = this.range;
const lines = [];
const adjustedMinRange = Math.floor(minValue / this.axisIncrement) * this.axisIncrement;
const adjustedMaxRange = Math.ceil(maxValue / this.axisIncrement) * this.axisIncrement;
for (let value = adjustedMinRange; value <= adjustedMaxRange; value += this.axisIncrement) {
lines.push(h("div", { class: {
'axis-line': true,
'zero-line': value === 0,
}, role: "presentation" }, h("limel-badge", { label: value })));
}
return (h("div", { class: "axises", role: "presentation" }, lines));
}
renderItems() {
var _a;
if (!((_a = this.items) === null || _a === void 0 ? void 0 : _a.length)) {
return;
}
let cumulativeOffset = 0;
return this.items.map((item, index) => {
const itemId = createRandomString();
const sizeAndOffset = this.calculateSizeAndOffset(item);
const size = sizeAndOffset.size;
let offset = sizeAndOffset.offset;
if (this.type === 'pie' || this.type === 'doughnut') {
offset = cumulativeOffset;
cumulativeOffset += size;
}
return (h("tr", { style: this.getItemStyle(item, index, size, offset), class: this.getItemClass(item), key: itemId, id: itemId, "data-index": index, tabIndex: 0, role: item.clickable ? 'button' : null, onClick: this.handleClick, onKeyDown: this.handleKeyDown }, h("th", null, this.getItemText(item)), h("td", null, this.getFormattedValue(item)), this.renderTooltip(item, itemId, size)));
});
}
getItemStyle(item, index, size, offset) {
const style = {
'--limel-chart-item-offset': `${offset}`,
'--limel-chart-item-size': `${size}`,
'--limel-chart-item-index': `${index}`,
'--limel-chart-item-value': `${item.value}`,
};
if (item.color) {
style['--limel-chart-item-color'] = item.color;
}
if (this.type === 'line' || this.type === 'area') {
const nextItem = this.calculateSizeAndOffset(this.items[index + 1]);
style['--limel-chart-next-item-size'] = `${nextItem.size}`;
style['--limel-chart-next-item-offset'] = `${nextItem.offset}`;
}
return style;
}
getItemClass(item) {
return {
item: true,
'has-start-value': Array.isArray(item.value),
'has-negative-value-only': this.getMaximumValue(item) < 0 && !this.isRangeItem(item),
};
}
calculateSizeAndOffset(item) {
const { minValue, totalRange } = this.range;
if (!item) {
return {
size: 0,
offset: 0,
};
}
let startValue = 0;
if (this.isRangeItem(item)) {
startValue = this.getMinimumValue(item);
}
const normalizedStart = ((startValue - minValue) / totalRange) * PERCENT;
const normalizedEnd = ((this.getMaximumValue(item) - minValue) / totalRange) * PERCENT;
return {
size: normalizedEnd - normalizedStart,
offset: normalizedStart,
};
}
getFormattedValue(item) {
const { value, formattedValue } = item;
if (formattedValue) {
return formattedValue;
}
if (Array.isArray(value)) {
return `${value[0]} — ${value[1]}`;
}
return `${value}`;
}
getItemText(item) {
return item.text;
}
renderTooltip(item, itemId, size) {
const text = this.getItemText(item);
const PERCENT_DECIMAL = 2;
const formattedValue = this.getFormattedValue(item);
const tooltipProps = {
label: text,
helperLabel: formattedValue,
elementId: itemId,
};
if (this.type !== 'bar' && this.type !== 'dot' && this.type !== 'nps') {
tooltipProps.label = `${text} (${size.toFixed(PERCENT_DECIMAL)}%)`;
}
return (h("limel-tooltip", Object.assign({}, tooltipProps, { openDirection: this.orientation === 'portrait' ? 'right' : 'top' })));
}
calculateRange() {
var _a;
if (this.range) {
return this.range;
}
const minRange = Math.min(0, ...this.items.map(this.getMinimumValue));
const maxRange = Math.max(...this.items.map(this.getMaximumValue));
const totalSum = this.items.reduce((sum, item) => sum + this.getMaximumValue(item), 0);
let finalMaxRange = (_a = this.maxValue) !== null && _a !== void 0 ? _a : maxRange;
if ((this.type === 'pie' || this.type === 'doughnut') &&
!this.maxValue) {
finalMaxRange = totalSum;
}
if (!this.axisIncrement) {
this.axisIncrement = this.calculateAxisIncrement(this.items);
}
const visualMaxValue = Math.ceil(finalMaxRange / this.axisIncrement) * this.axisIncrement;
const visualMinValue = Math.floor(minRange / this.axisIncrement) * this.axisIncrement;
const totalRange = visualMaxValue - visualMinValue;
return {
minValue: visualMinValue,
maxValue: visualMaxValue,
totalRange: totalRange,
};
}
calculateAxisIncrement(items, steps = DEFAULT_INCREMENT_SIZE) {
const maxValue = Math.max(...items.map((item) => {
const value = item.value;
if (Array.isArray(value)) {
return Math.max(...value);
}
return value;
}));
const roughStep = maxValue / steps;
const magnitude = 10 ** Math.floor(Math.log10(roughStep));
return Math.ceil(roughStep / magnitude) * magnitude;
}
getMinimumValue(item) {
const value = item.value;
return Array.isArray(value) ? Math.min(...value) : value;
}
getMaximumValue(item) {
const value = item.value;
return Array.isArray(value) ? Math.max(...value) : value;
}
isRangeItem(item) {
return Array.isArray(item.value);
}
handleChange() {
this.range = null;
this.recalculateRangeData();
}
recalculateRangeData() {
this.range = this.calculateRange();
}
getClickableItem(target) {
const index = target.dataset.index;
if (index === undefined) {
return;
}
const item = this.items[Number(index)];
if (!item.clickable) {
return;
}
return item;
}
static get is() { return "limel-chart"; }
static get encapsulation() { return "shadow"; }
static get originalStyleUrls() {
return {
"$": ["chart.scss"]
};
}
static get styleUrls() {
return {
"$": ["chart.css"]
};
}
static get properties() {
return {
"language": {
"type": "string",
"mutable": false,
"complexType": {
"original": "Languages",
"resolved": "\"da\" | \"de\" | \"en\" | \"fi\" | \"fr\" | \"nb\" | \"nl\" | \"no\" | \"sv\"",
"references": {
"Languages": {
"location": "import",
"path": "../date-picker/date.types"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Defines the language for translations.\nWill translate the translatable strings on the components."
},
"attribute": "language",
"reflect": true,
"defaultValue": "'en'"
},
"accessibleLabel": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Helps users of assistive technologies to understand\nthe context of the chart, and what is being displayed."
},
"attribute": "accessible-label",
"reflect": true
},
"accessibleItemsLabel": {
"type": "string",
"mutable": false,
"complexType": {
"original": "string",
"resolved": "string",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Helps users of assistive technologies to understand\nwhat the items in the chart represent."
},
"attribute": "accessible-items-label",
"reflect": true
},
"items": {
"type": "unknown",
"mutable": false,
"complexType": {
"original": "ChartItem[]",
"resolved": "ChartItem<number | [number, number]>[]",
"references": {
"ChartItem": {
"location": "import",
"path": "./chart.types"
}
}
},
"required": true,
"optional": false,
"docs": {
"tags": [],
"text": "List of items in the chart,\neach representing a data point."
}
},
"type": {
"type": "string",
"mutable": false,
"complexType": {
"original": "| 'area'\n | 'bar'\n | 'doughnut'\n | 'line'\n | 'nps'\n | 'pie'\n | 'ring'\n | 'dot'\n | 'stacked-bar'",
"resolved": "\"area\" | \"bar\" | \"dot\" | \"doughnut\" | \"line\" | \"nps\" | \"pie\" | \"ring\" | \"stacked-bar\"",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Defines how items are visualized in the chart."
},
"attribute": "type",
"reflect": true,
"defaultValue": "'stacked-bar'"
},
"orientation": {
"type": "string",
"mutable": false,
"complexType": {
"original": "'landscape' | 'portrait'",
"resolved": "\"landscape\" | \"portrait\"",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Defines whether the chart is intended to be displayed wide or tall.\nDoes not have any effect on chart types which generate circular forms."
},
"attribute": "orientation",
"reflect": true,
"defaultValue": "'landscape'"
},
"maxValue": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Specifies the range that items' values could be in.\nThis is used in calculation of the size of the items in the chart.\nWhen not provided, the sum of all values in the items will be considered as the range."
},
"attribute": "max-value",
"reflect": true
},
"axisIncrement": {
"type": "number",
"mutable": false,
"complexType": {
"original": "number",
"resolved": "number",
"references": {}
},
"required": false,
"optional": true,
"docs": {
"tags": [],
"text": "Specifies the increment for the axis lines."
},
"attribute": "axis-increment",
"reflect": true
},
"loading": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Indicates whether the chart is in a loading state."
},
"attribute": "loading",
"reflect": true,
"defaultValue": "false"
}
};
}
static get events() {
return [{
"method": "interact",
"name": "interact",
"bubbles": true,
"cancelable": true,
"composed": true,
"docs": {
"tags": [],
"text": "Fired when a chart item with `clickable` set to `true` is clicked"
},
"complexType": {
"original": "ChartItem",
"resolved": "ChartItem<number | [number, number]>",
"references": {
"ChartItem": {
"location": "import",
"path": "./chart.types"
}
}
}
}];
}
static get watchers() {
return [{
"propName": "items",
"methodName": "handleChange"
}, {
"propName": "axisIncrement",
"methodName": "handleChange"
}, {
"propName": "maxValue",
"methodName": "handleChange"
}];
}
}
//# sourceMappingURL=chart.js.map