@limetech/lime-elements
Version:
605 lines (604 loc) • 24.2 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-axis-labels
* @exampleComponent limel-example-chart-styling
* @exampleComponent limel-example-chart-creative-styling
* @beta
*/
export class Chart {
constructor() {
/**
* Defines the language for translations.
* Will translate the translatable strings on the components.
*/
this.language = 'en';
/**
* When set to true, renders visible labels for X and Y axes.
* Only affects chart types with X and Y axes, such as area, bar, and line charts.
*/
this.displayAxisLabels = false;
/**
* Makes the `text` of chart items constantly visible,
* By default, item texts are displayed in a tooltip,
* when the item is hovered or focused.
* Only affects chart types with X and Y axes, such as area, bar, and line charts.
*/
this.displayItemText = false;
/**
* Makes the `value` (or `formattedValue`) of chart items constantly visible,
* By default, item values are displayed in a tooltip,
* when the item is hovered or focused.
* Only affects chart types with X and Y axes, such as area, bar, and line charts.
*/
this.displayItemValue = false;
/**
* Defines how items are visualized in the chart.
*/
this.type = 'stacked-bar';
/**
* Defines whether the chart is intended to be displayed wide or tall.
* Does not have any effect on chart types which generate circular forms.
*/
this.orientation = 'landscape';
/**
* Indicates whether the chart is in a loading state.
*/
this.loading = false;
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);
};
}
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() {
var _a, _b;
return (h("thead", null, h("tr", null, h("th", { scope: "col" }, (_a = this.accessibleItemsLabel) !== null && _a !== void 0 ? _a : translate.get('items', this.language)), h("th", { scope: "col" }, (_b = this.accessibleValuesLabel) !== null && _b !== void 0 ? _b : 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("td", { class: "text" }, this.getItemText(item)), h("td", { class: "value" }, 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),
'has-value-zero': this.getMaximumValue(item) === 0,
};
}
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",
"id": "src/components/date-picker/date.types.ts::Languages",
"referenceLocation": "Languages"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Defines the language for translations.\nWill translate the translatable strings on the components."
},
"getter": false,
"setter": false,
"reflect": true,
"attribute": "language",
"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."
},
"getter": false,
"setter": false,
"reflect": true,
"attribute": "accessible-label"
},
"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.\nDefaults to the translation for \"items\" in the current language."
},
"getter": false,
"setter": false,
"reflect": true,
"attribute": "accessible-items-label"
},
"accessibleValuesLabel": {
"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 values in the chart represent.\nDefaults to the translation for \"value\" in the current language."
},
"getter": false,
"setter": false,
"reflect": true,
"attribute": "accessible-values-label"
},
"displayAxisLabels": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "When set to true, renders visible labels for X and Y axes.\nOnly affects chart types with X and Y axes, such as area, bar, and line charts."
},
"getter": false,
"setter": false,
"reflect": true,
"attribute": "display-axis-labels",
"defaultValue": "false"
},
"displayItemText": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Makes the `text` of chart items constantly visible,\nBy default, item texts are displayed in a tooltip,\nwhen the item is hovered or focused.\nOnly affects chart types with X and Y axes, such as area, bar, and line charts."
},
"getter": false,
"setter": false,
"reflect": true,
"attribute": "display-item-text",
"defaultValue": "false"
},
"displayItemValue": {
"type": "boolean",
"mutable": false,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Makes the `value` (or `formattedValue`) of chart items constantly visible,\nBy default, item values are displayed in a tooltip,\nwhen the item is hovered or focused.\nOnly affects chart types with X and Y axes, such as area, bar, and line charts."
},
"getter": false,
"setter": false,
"reflect": true,
"attribute": "display-item-value",
"defaultValue": "false"
},
"items": {
"type": "unknown",
"mutable": false,
"complexType": {
"original": "ChartItem[]",
"resolved": "ChartItem<number | [number, number]>[]",
"references": {
"ChartItem": {
"location": "import",
"path": "./chart.types",
"id": "src/components/chart/chart.types.ts::ChartItem",
"referenceLocation": "ChartItem"
}
}
},
"required": true,
"optional": false,
"docs": {
"tags": [],
"text": "List of items in the chart,\neach representing a data point."
},
"getter": false,
"setter": false
},
"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."
},
"getter": false,
"setter": false,
"reflect": true,
"attribute": "type",
"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."
},
"getter": false,
"setter": false,
"reflect": true,
"attribute": "orientation",
"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."
},
"getter": false,
"setter": false,
"reflect": true,
"attribute": "max-value"
},
"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."
},
"getter": false,
"setter": false,
"reflect": true,
"attribute": "axis-increment"
},
"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."
},
"getter": false,
"setter": false,
"reflect": true,
"attribute": "loading",
"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",
"id": "src/components/chart/chart.types.ts::ChartItem",
"referenceLocation": "ChartItem"
}
}
}
}];
}
static get watchers() {
return [{
"propName": "items",
"methodName": "handleChange"
}, {
"propName": "axisIncrement",
"methodName": "handleChange"
}, {
"propName": "maxValue",
"methodName": "handleChange"
}];
}
}