diff --git a/etc/lime-elements.api.md b/etc/lime-elements.api.md index c2749ea935..79133525cd 100644 --- a/etc/lime-elements.api.md +++ b/etc/lime-elements.api.md @@ -61,6 +61,14 @@ export interface Button { // @public export type CalloutType = 'note' | 'important' | 'tip' | 'caution' | 'warning'; +// @beta +export interface ChartItem { + color?: string; + formattedValue?: string; + text: string; + value: T; +} + // @public (undocumented) export interface Chip { badge?: number; @@ -189,6 +197,26 @@ export namespace Components { "language": Languages; "type"?: CalloutType; } + // @beta + export interface LimelChart { + "accessibleItemsLabel"?: string; + "accessibleLabel"?: string; + "axisIncrement"?: number; + "items": ChartItem[]; + "language": Languages; + "loading": boolean; + "maxValue"?: number; + "orientation"?: 'landscape' | 'portrait'; + "type"?: | 'area' + | 'bar' + | 'doughnut' + | 'line' + | 'nps' + | 'pie' + | 'ring' + | 'dot' + | 'stacked-bar'; + } export interface LimelCheckbox { "checked": boolean; "disabled": boolean; @@ -957,6 +985,10 @@ namespace JSX_2 { "limel-button-group": LimelButtonGroup; // (undocumented) "limel-callout": LimelCallout; + // Warning: (ae-incompatible-release-tags) The symbol ""limel-chart"" is marked as @public, but its signature references "JSX_2" which is marked as @beta + // + // (undocumented) + "limel-chart": LimelChart; // (undocumented) "limel-checkbox": LimelCheckbox; // (undocumented) @@ -1134,6 +1166,26 @@ namespace JSX_2 { "language"?: Languages; "type"?: CalloutType; } + // @beta + interface LimelChart { + "accessibleItemsLabel"?: string; + "accessibleLabel"?: string; + "axisIncrement"?: number; + "items": ChartItem[]; + "language"?: Languages; + "loading"?: boolean; + "maxValue"?: number; + "orientation"?: 'landscape' | 'portrait'; + "type"?: | 'area' + | 'bar' + | 'doughnut' + | 'line' + | 'nps' + | 'pie' + | 'ring' + | 'dot' + | 'stacked-bar'; + } interface LimelCheckbox { "checked"?: boolean; "disabled"?: boolean; diff --git a/src/components/chart/chart.scss b/src/components/chart/chart.scss new file mode 100644 index 0000000000..73d765c69a --- /dev/null +++ b/src/components/chart/chart.scss @@ -0,0 +1,127 @@ +@use '../../style/mixins'; +$min-item-size: 0.5rem; +$default-item-color: var(--chart-item-color, rgb(var(--contrast-1100), 0.8)); + +/** +* @prop --chart-background-color: Defines the background color of the chart. Defaults to `transparent` for _most_ chart types. +* @prop --chart-item-color: If no color is defined for chart items, this color will be use. Defaults to `rgb(var(--contrast-1100), 0.8)`. +* @prop --chart-item-divider-color: Defines the color that visually separates items in some charts, such as `stacked-bar` chart items. Defaults to `rgb(var(--color-white), 0.6)`. +* @prop --chart-axis-line-color: Defines color of the axis lines. Defaults to `--contrast-900`. Note that lines have opacity as well, and get opaque on hover. +* @prop --chart-item-border-radius: Defines the roundness of corners of items in a chart. Defaults to different values depending on the chart type. Does not have any effect on `pie` and `doughnut` types. +*/ + +:host(limel-chart) { + --chart-axis-line-color: var( + --limel-chart-axis-line-color, + rgb(var(--contrast-900)) + ); + box-sizing: border-box; + isolation: isolate; + + display: flex; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + padding: var(--limel-chart-padding); +} + +table { + // Since these are mainly "resets", no styles should be before them. + all: unset; + border-collapse: collapse; + border-spacing: 0; + empty-cells: show; + + position: relative; + display: flex; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; + + colgroup, + thead, + tbody, + tr, + th, + td { + all: unset; + } + + caption, + colgroup, + thead, + tfoot, + th, + td { + @include mixins.visually-hidden; + } +} + +*, +*:before, +*:after { + box-sizing: border-box; +} + +.chart { + position: relative; + flex-grow: 1; + width: 100%; + height: 100%; + min-height: 0; + min-width: 0; + + &:has(.item:hover), + &:has(.item:focus-visible) { + .item { + opacity: 0.4; + } + } +} + +.item { + transition: + background-color 0.2s ease, + filter 0.2s ease, + opacity 0.4s ease; + cursor: help; + + &:focus-visible, + &:hover { + opacity: 1 !important; + } +} + +limel-spinner { + margin: auto; +} + +@mixin line( + $direction: vertical, + $color: rgb(var(--contrast-800), 0.4), + $position: center +) { + @if $direction == vertical { + background: linear-gradient(to bottom, $color 0%, $color 100%) + $position/1px + 100% + no-repeat; + } @else if $direction == horizontal { + background: linear-gradient(to right, $color 0%, $color 100%) + $position/100% + 1px + no-repeat; + } +} + +@import './partial-styles/_layout-for-charts-with-x-y-axises'; +@import './partial-styles/_layout-for-charts-with-circular-shape'; +@import './partial-styles/_bar-gantt-dot'; +@import './partial-styles/_area_line'; +@import './partial-styles/_pie-doughnut'; +@import './partial-styles/_ring'; +@import './partial-styles/_stacked-bar'; +@import './partial-styles/_nps'; +@import './partial-styles/_axises'; diff --git a/src/components/chart/chart.tsx b/src/components/chart/chart.tsx new file mode 100644 index 0000000000..355f2f76cd --- /dev/null +++ b/src/components/chart/chart.tsx @@ -0,0 +1,425 @@ +import { Component, h, Prop, Watch } from '@stencil/core'; +import { Languages } from '../date-picker/date.types'; +import translate from '../../global/translations'; +import { createRandomString } from '../../util/random-string'; +import { ChartItem } from './chart.types'; + +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-accessibility + * @exampleComponent limel-example-chart-styling + * @exampleComponent limel-example-chart-creative-styling + * @beta + */ + +@Component({ + tag: 'limel-chart', + shadow: true, + styleUrl: 'chart.scss', +}) +export class Chart { + /** + * Defines the language for translations. + * Will translate the translatable strings on the components. + */ + @Prop({ reflect: true }) + public language: Languages = 'en'; + + /** + * Helps users of assistive technologies to understand + * the context of the chart, and what is being displayed. + */ + @Prop({ reflect: true }) + public accessibleLabel?: string; + + /** + * Helps users of assistive technologies to understand + * what the items in the chart represent. + */ + @Prop({ reflect: true }) + public accessibleItemsLabel?: string; + + /** + * List of items in the chart, + * each representing a data point. + */ + @Prop() + public items!: ChartItem[]; + + /** + * Defines how items are visualized in the chart. + */ + @Prop({ reflect: true }) + public type?: + | 'area' + | 'bar' + | 'doughnut' + | 'line' + | 'nps' + | 'pie' + | 'ring' + | 'dot' + | 'stacked-bar' = '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. + */ + @Prop({ reflect: true }) + public orientation?: 'landscape' | 'portrait' = 'landscape'; + + /** + * Specifies the range that items' values could be in. + * This is used in calculation of the size of the items in the chart. + * When not provided, the sum of all values in the items will be considered as the range. + */ + @Prop({ reflect: true }) + public maxValue?: number; + + /** + * Specifies the increment for the axis lines. + */ + @Prop({ reflect: true }) + public axisIncrement?: number; + + /** + * Indicates whether the chart is in a loading state. + */ + @Prop({ reflect: true }) + public loading: boolean = false; + + private range: { + minValue: number; + maxValue: number; + totalRange: number; + }; + + public componentWillLoad() { + this.recalculateRangeData(); + } + + public render() { + if (this.loading) { + return ; + } + + return ( + + {this.renderCaption()} + {this.renderTableHeader()} + {this.renderAxises()} + {this.renderItems()} +
+ ); + } + + private renderCaption() { + if (!this.accessibleLabel) { + return; + } + + return {this.accessibleLabel}; + } + + private renderTableHeader() { + return ( + + + {this.accessibleItemsLabel} + {translate.get('value', this.language)} + + + ); + } + + private 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( + , + ); + } + + return ( + + ); + } + + private renderItems() { + if (!this.items?.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 ( + + {this.getItemText(item)} + {this.getFormattedValue(item)} + {this.renderTooltip(item, itemId, size)} + + ); + }); + } + + private getItemStyle( + item: ChartItem, + index: number, + size: number, + offset: number, + ): Record { + const style: Record = { + '--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; + } + + private getItemClass(item: ChartItem) { + return { + item: true, + 'has-start-value': Array.isArray(item.value), + 'has-negative-value-only': + this.getMaximumValue(item) < 0 && !this.isRangeItem(item), + }; + } + + private calculateSizeAndOffset(item?: ChartItem) { + 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, + }; + } + + private getFormattedValue(item: ChartItem): string { + const { value, formattedValue } = item; + + if (formattedValue) { + return formattedValue; + } + + if (Array.isArray(value)) { + return `${value[0]} — ${value[1]}`; + } + + return `${value}`; + } + + private getItemText(item: ChartItem): string { + return item.text; + } + + private renderTooltip(item: ChartItem, itemId: string, size: number) { + const text = this.getItemText(item); + const PERCENT_DECIMAL = 2; + const formattedValue = this.getFormattedValue(item); + + const tooltipProps: any = { + label: text, + helperLabel: formattedValue, + elementId: itemId, + }; + + if (this.type !== 'bar' && this.type !== 'dot' && this.type !== 'nps') { + tooltipProps.label = `${text} (${size.toFixed(PERCENT_DECIMAL)}%)`; + } + + return ( + + ); + } + + private calculateRange() { + 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 = this.maxValue ?? 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, + }; + } + + private calculateAxisIncrement( + items: ChartItem[], + steps: number = 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; + // eslint-disable-next-line no-magic-numbers + const magnitude = 10 ** Math.floor(Math.log10(roughStep)); + + return Math.ceil(roughStep / magnitude) * magnitude; + } + + private getMinimumValue(item: ChartItem): number { + const value = item.value; + + return Array.isArray(value) ? Math.min(...value) : value; + } + + private getMaximumValue(item: ChartItem): number { + const value = item.value; + + return Array.isArray(value) ? Math.max(...value) : value; + } + + private isRangeItem(item: ChartItem): item is ChartItem<[number, number]> { + return Array.isArray(item.value); + } + + @Watch('items') + @Watch('axisIncrement') + @Watch('maxValue') + handleChange() { + this.range = null; + this.recalculateRangeData(); + } + + private recalculateRangeData() { + this.range = this.calculateRange(); + } +} diff --git a/src/components/chart/chart.types.ts b/src/components/chart/chart.types.ts new file mode 100644 index 0000000000..1e9bcb5619 --- /dev/null +++ b/src/components/chart/chart.types.ts @@ -0,0 +1,29 @@ +/** + * Chart component props. + * @beta + */ +export interface ChartItem< + T extends number | [number, number] = number | [number, number], +> { + /** + * Label displayed for the item. + */ + text: string; + + /** + * Value of the item. + */ + value: T; + + /** + * Formatted value of the item + */ + formattedValue?: string; + + /** + * Color of the item in the chart. Defaults to a shade of grey. + * It is recommended to use distinct colors for each item, + * and make sure there is enough contrast between colors of adjacent items. + */ + color?: string; +} diff --git a/src/components/chart/examples/chart-accessibility.tsx b/src/components/chart/examples/chart-accessibility.tsx new file mode 100644 index 0000000000..56c95311f1 --- /dev/null +++ b/src/components/chart/examples/chart-accessibility.tsx @@ -0,0 +1,82 @@ +import { Component, h, Host, State } from '@stencil/core'; +import { chartItems } from './chart-items-multi-axis-negative-start-values'; + +/** + * Accessibility + * Under the hoods, our charts are simply HTML tables. + * This helps screen readers to interpret the data and present it to their users. + * However, to make this semantic more accessible and more understandable, + * there are some optional props that we highly recommend you to use. + * + * - `accessibleLabel`: Will be used as a `caption` for the table, and + * describes what the chart is about. Depending on the context, + * It might also be a good idea to include the accessible label for sighted + * users as well, for instance, as a heading. + * - `accessibleItemsLabel`: Will be used as a `th` for the first column of the table, + * describing what all items in this column have in common. In this example, + * all items are cities. + * + * Note that these props won't be visually rendered for sighted users, but + * they will be presented to assistive technologies, such as screen readers + * as well as search engines. + * + * Another way to improve the accessibility of the chart is to use + * the `prefix` and `suffix` props to provide additional context to the `value` + * of each item. + * + * ##### Using the `loading` prop + * Sometimes the data set needs to be calculated, or fetched + * through a process that requires some time. In such cases, it is + * a great idea to let the users know that the data is being updated. + * + * To do so, set the `loading` property to `true`. The component will then + * show a spinner, indicating the data is being updated. This not only improves + * the user experience, but also the accessibility of the chart both for sighted users, + * and behind the scenes, for users of assistive technologies. + * + * @sourceFile chart-items-gantt-negative-values.ts + */ +@Component({ + tag: 'limel-example-chart-accessibility', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartAccessibilityExample { + @State() + public loading = false; + + public render() { + const heading = 'Temperature fluctuations past 24 hours'; + const subHeading = 'in cities we have our offices'; + + return ( + + + + + + + + ); + } + + private setLoading = (event: CustomEvent) => { + event.stopPropagation(); + this.loading = event.detail; + }; +} diff --git a/src/components/chart/examples/chart-axis-increment.tsx b/src/components/chart/examples/chart-axis-increment.tsx new file mode 100644 index 0000000000..c26476aebb --- /dev/null +++ b/src/components/chart/examples/chart-axis-increment.tsx @@ -0,0 +1,82 @@ +import { Component, h, Host, State } from '@stencil/core'; +import { LimelSelectCustomEvent, Option } from '@limetech/lime-elements'; +import { chartItems } from './chart-items-with-negative-values'; + +/** + * Using the `axisIncrement` prop + * The `axisIncrement` prop specifies the incremental + * values of the axis lines. By default the component tries to + * smartly calculate a proper axis increment, to render meaningful + * axis lines, based on the maximum value provided in the dataset. + * + * However, you can set the `axisIncrement` to a + * different custom value if needed. + * + * @sourceFile chart-items-with-negative-values.ts + */ +@Component({ + tag: 'limel-example-chart-axis-increment', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartAxisIncrementExample { + @State() + private axisIncrement = 5; + + @State() + private orientation: 'landscape' | 'portrait' = 'landscape'; + + private orientations: Option[] = [ + { text: 'landscape', value: 'landscape' }, + { text: 'portrait', value: 'portrait' }, + ]; + + public render() { + const defaultAxisIncrement = `${this.axisIncrement}`; + + return ( + +

Subscriptions per month

+ + + + + +
+ ); + } + + private getSelectedOrientation() { + return this.orientations.find( + (option) => option.value === this.orientation, + ); + } + + private handleOrientationChange = ( + event: LimelSelectCustomEvent>, + ) => { + this.orientation = event.detail.value as 'landscape' | 'portrait'; + }; + + private handleAxisIncrementChange = (event) => { + this.axisIncrement = +event.detail; + }; +} diff --git a/src/components/chart/examples/chart-creative-styling.tsx b/src/components/chart/examples/chart-creative-styling.tsx new file mode 100644 index 0000000000..65c42ac01e --- /dev/null +++ b/src/components/chart/examples/chart-creative-styling.tsx @@ -0,0 +1,35 @@ +import { Component, h, Host } from '@stencil/core'; +import { + stackedBarChartItems, + ganttChartItems, + areaChartItems, +} from './chart-items-creative-styling'; + +/** + * Creative styling + * + * Behind the scenes for some chart types, + * the `color` property of the `item` is used as a `background` style, + * not a `background-color` style. + * This, together with some CSS knowledge can empower some creative visualizations; + * specially when a solid color is not enough to communicate a certain meaning. + * Here are some examples for inspiration. + * + * @sourceFile chart-items-creative-styling.ts + */ +@Component({ + tag: 'limel-example-chart-creative-styling', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartTypeCreativeStylingExample { + public render() { + return ( + + + + + + ); + } +} diff --git a/src/components/chart/examples/chart-examples.scss b/src/components/chart/examples/chart-examples.scss new file mode 100644 index 0000000000..95c9b20bec --- /dev/null +++ b/src/components/chart/examples/chart-examples.scss @@ -0,0 +1 @@ +@import './chart-resizable-container'; diff --git a/src/components/chart/examples/chart-items-area.ts b/src/components/chart/examples/chart-items-area.ts new file mode 100644 index 0000000000..c92be3298d --- /dev/null +++ b/src/components/chart/examples/chart-items-area.ts @@ -0,0 +1,64 @@ +import { ChartItem } from '@limetech/lime-elements'; + +export const chartItems: ChartItem[] = [ + { + text: 'January', + value: 10, + color: 'rgb(var(--color-pink-dark))', + }, + { + text: 'February', + value: 3, + color: 'rgb(var(--color-pink-dark))', + }, + { + text: 'March', + value: 13, + color: 'rgb(var(--color-pink-dark))', + }, + { + text: 'April', + value: 55, + color: 'rgb(var(--color-pink-dark))', + }, + { + text: 'May', + value: 67, + color: 'rgb(var(--color-pink-dark))', + }, + { + text: 'June', + value: 40, + color: 'rgb(var(--color-pink-dark))', + }, + { + text: 'July', + value: 7, + color: 'rgb(var(--color-pink-dark))', + }, + { + text: 'August', + value: 0, + color: 'rgb(var(--color-pink-dark))', + }, + { + text: 'September', + value: 90, + color: 'rgb(var(--color-pink-dark))', + }, + { + text: 'October', + value: 70, + color: 'rgb(var(--color-pink-dark))', + }, + { + text: 'November', + value: 20, + color: 'rgb(var(--color-pink-dark))', + }, + { + text: 'December', + value: 36, + color: 'rgb(var(--color-pink-dark))', + }, +]; diff --git a/src/components/chart/examples/chart-items-bar.ts b/src/components/chart/examples/chart-items-bar.ts new file mode 100644 index 0000000000..cafc384bb5 --- /dev/null +++ b/src/components/chart/examples/chart-items-bar.ts @@ -0,0 +1,64 @@ +import { ChartItem } from '@limetech/lime-elements'; + +export const chartItems: ChartItem[] = [ + { + text: 'January', + value: 10, + color: 'var(--color-percent--0to10)', + }, + { + text: 'February', + value: 3, + color: 'var(--color-percent--0to10)', + }, + { + text: 'March', + value: 13, + color: 'var(--color-percent--10to20)', + }, + { + text: 'April', + value: 55, + color: 'var(--color-percent--50to60)', + }, + { + text: 'May', + value: 67, + color: 'var(--color-percent--60to70)', + }, + { + text: 'June', + value: 40, + color: 'var(--color-percent--40to50)', + }, + { + text: 'July', + value: 7, + color: 'var(--color-percent--0to10)', + }, + { + text: 'August', + value: 0, + color: 'var(--color-percent--0)', + }, + { + text: 'September', + value: 90, + color: 'var(--color-percent--90to100)', + }, + { + text: 'October', + value: 70, + color: 'var(--color-percent--70to80)', + }, + { + text: 'November', + value: 20, + color: 'var(--color-percent--20to30)', + }, + { + text: 'December', + value: 36, + color: 'var(--color-percent--30to40)', + }, +]; diff --git a/src/components/chart/examples/chart-items-creative-styling.ts b/src/components/chart/examples/chart-items-creative-styling.ts new file mode 100644 index 0000000000..e8cae67131 --- /dev/null +++ b/src/components/chart/examples/chart-items-creative-styling.ts @@ -0,0 +1,81 @@ +import { ChartItem } from '@limetech/lime-elements'; +export const stackedBarChartItems: ChartItem[] = [ + { + text: 'Applications', + value: 40, + formattedValue: '40 gb', + color: 'rgb(var(--color-coral-default))', + }, + { + text: 'Application cache', + value: 18, + formattedValue: '18 gb', + color: "rgb(var(--color-coral-default)) url(\"data:image/svg+xml;charset=utf-8,\")", + }, + { + text: 'Temporary files', + value: 23, + formattedValue: '23 gb', + color: 'rgb(var(--color-cyan-default))', + }, + { + text: 'OS data', + value: 16, + formattedValue: '16 gb', + color: 'rgb(var(--color-blue-default))', + }, +]; + +export const ganttChartItems: Array> = [ + { + text: 'Week 1', + formattedValue: '-7% — 10%', + value: [-7, 10], + color: 'rgb(var(--color-blue-default))', + }, + { + text: 'Week 2', + formattedValue: '4% — 32%', + value: [4, 32], + color: "rgb(var(--color-orange-light)) url(\"data:image/svg+xml;charset=utf-8,\")", + }, + { + text: 'Week 3', + formattedValue: '14% — 44%', + value: [14, 44], + color: "rgb(var(--color-cyan-default)) url(\"data:image/svg+xml;charset=utf-8,\")", + }, +]; + +export const areaChartItems: ChartItem[] = [ + { + text: '10 to 20', + value: 6, + color: 'linear-gradient(0deg, rgb(var(--color-cyan-default)) 0%, rgb(var(--color-red-default)) 80%)', + }, + { + text: '20 to 30', + value: 12, + color: 'linear-gradient(0deg, rgb(var(--color-cyan-default)) 0%, rgb(var(--color-red-default)) 80%)', + }, + { + text: '30 to 40', + value: 18, + color: 'linear-gradient(0deg, rgb(var(--color-cyan-default)) 0%, rgb(var(--color-red-default)) 80%)', + }, + { + text: '40 to 50', + value: 23, + color: 'linear-gradient(0deg, rgb(var(--color-cyan-default)) 0%, rgb(var(--color-red-default)) 80%)', + }, + { + text: '50 to 60', + value: 30, + color: 'linear-gradient(0deg, rgb(var(--color-cyan-default)) 0%, rgb(var(--color-red-default)) 80%)', + }, + { + text: '60 to 70', + value: 18, + color: 'linear-gradient(0deg, rgb(var(--color-cyan-default)) 0%, rgb(var(--color-red-default)) 80%)', + }, +]; diff --git a/src/components/chart/examples/chart-items-gantt.ts b/src/components/chart/examples/chart-items-gantt.ts new file mode 100644 index 0000000000..a097540ab9 --- /dev/null +++ b/src/components/chart/examples/chart-items-gantt.ts @@ -0,0 +1,51 @@ +import { ChartItem } from '@limetech/lime-elements'; +export const chartItems: Array> = [ + { + text: 'Market Research', + formattedValue: 'day 1 — day 10', + value: [1, 10], + color: 'rgb(var(--color-blue-default))', + }, + { + text: 'Business Plan Development', + formattedValue: 'day 1 — day 20', + value: [1, 20], + color: 'rgb(var(--color-green-default))', + }, + { + text: 'Prototyping', + formattedValue: 'day 10 — day 40', + value: [10, 40], + color: 'rgb(var(--color-cyan-default))', + }, + { + text: 'User Testing', + formattedValue: 'day 15 — day 70', + value: [15, 70], + color: 'rgb(var(--color-purple-default))', + }, + { + text: 'MVP Development', + formattedValue: 'day 70 — day 120', + value: [70, 120], + color: 'rgb(var(--color-pink-default))', + }, + { + text: 'Marketing & PR', + formattedValue: 'day 80 — day 130', + value: [80, 130], + color: 'rgb(var(--color-violet-default))', + }, + { + text: 'Launch Preparation', + formattedValue: 'day 110 — day 140', + value: [110, 140], + color: 'rgb(var(--color-orange-default))', + }, + { + text: 'Product Launch', + formattedValue: 'day 140 — day 155', + value: [140, 155], + color: 'rgb(var(--color-teal-default))', + }, +]; diff --git a/src/components/chart/examples/chart-items-multi-axis-negative-start-values-area.ts b/src/components/chart/examples/chart-items-multi-axis-negative-start-values-area.ts new file mode 100644 index 0000000000..a758615ab5 --- /dev/null +++ b/src/components/chart/examples/chart-items-multi-axis-negative-start-values-area.ts @@ -0,0 +1,104 @@ +import { ChartItem } from '@limetech/lime-elements'; + +export const chartItems: Array> = [ + { + text: 'Monday 01', + value: [7, 27.3], + formattedValue: '7¢ — 27.3¢', + }, + { + text: 'Tuesday 02', + value: [4, 27.2], + formattedValue: '4¢ — 27.2¢', + }, + { + text: 'Wednesday 03', + value: [0, 28], + formattedValue: '0¢ — 28¢', + }, + { + text: 'Thursday 04', + value: [-2, 17], + formattedValue: '-2¢ — 17¢', + }, + { + text: 'Friday 05', + value: [-1, 16], + formattedValue: '-1¢ — 16¢', + }, + { + text: 'Saturday 06', + value: [-4, 12], + formattedValue: '-4¢ — 12¢', + }, + { + text: 'Sunday 07', + value: [-3.4, 8], + formattedValue: '-3.4¢ — 8¢', + }, + { + text: 'Monday 08', + value: [12, 32], + formattedValue: '12¢ — 32¢', + }, + { + text: 'Tuesday 09', + value: [7, 27], + formattedValue: '7¢ — 27¢', + }, + { + text: 'Wednesday 10', + value: [8, 20], + formattedValue: '8¢ — 20¢', + }, + { + text: 'Thursday 11', + value: [9.9, 21], + formattedValue: '9.9¢ — 21¢', + }, + { + text: 'Friday 12', + value: [5, 15], + formattedValue: '5¢ — 15¢', + }, + { + text: 'Saturday 13', + value: [1, 10], + formattedValue: '1¢ — 10¢', + }, + { + text: 'Sunday 14', + value: [-5.2, 3], + formattedValue: '-5.2¢ — 3¢', + }, + { + text: 'Monday 15', + value: [1, 4], + formattedValue: '1¢ — 4¢', + }, + { + text: 'Tuesday 16', + value: [-0.5, 4.7], + formattedValue: '-0.5¢ — 4.7¢', + }, + { + text: 'Wednesday 17', + value: [4.2, 14], + formattedValue: '4.2¢ — 14¢', + }, + { + text: 'Thursday 18', + value: [5, 25], + formattedValue: '5¢ — 25¢', + }, + { + text: 'Friday 19', + value: [1, 23], + formattedValue: '1¢ — 23¢', + }, + { + text: 'Saturday 20', + value: [-2, 13], + formattedValue: '-2¢ — 13¢', + }, +]; diff --git a/src/components/chart/examples/chart-items-multi-axis-negative-start-values.ts b/src/components/chart/examples/chart-items-multi-axis-negative-start-values.ts new file mode 100644 index 0000000000..14d69acb6c --- /dev/null +++ b/src/components/chart/examples/chart-items-multi-axis-negative-start-values.ts @@ -0,0 +1,52 @@ +import { ChartItem } from '@limetech/lime-elements'; + +export const chartItems: Array> = [ + { + text: 'New York', + value: [0, 10], + color: 'rgb(var(--color-yellow-dark))', + formattedValue: '0° — 10°', + }, + { + text: 'Kiruna', + value: [-17, -5], + color: 'rgb(var(--color-sky-lighter))', + formattedValue: '-17° — -5°', + }, + { + text: 'Dubai', + value: [20, 35], + color: 'rgb(var(--color-red-default))', + formattedValue: '20° — 35°', + }, + { + text: 'Sydney', + value: [10, 25], + color: 'rgb(var(--color-orange-default))', + formattedValue: '10° — 25°', + }, + { + text: 'Reykjavik', + value: [-10, 0], + color: 'rgb(var(--color-sky-default))', + formattedValue: '-10° — 0°', + }, + { + text: 'Helsinki', + value: [-12, -2], + color: 'rgb(var(--color-sky-light))', + formattedValue: '-12° — -2°', + }, + { + text: 'Buenos Aires', + value: [5, 22], + color: 'rgb(var(--color-orange-light))', + formattedValue: '5° — 22°', + }, + { + text: 'Tokyo', + value: [6, 17], + color: 'rgb(var(--color-orange-lighter))', + formattedValue: '6° — 17°', + }, +]; diff --git a/src/components/chart/examples/chart-items-nps.ts b/src/components/chart/examples/chart-items-nps.ts new file mode 100644 index 0000000000..b4a7908c4b --- /dev/null +++ b/src/components/chart/examples/chart-items-nps.ts @@ -0,0 +1,29 @@ +import { ChartItem } from '@limetech/lime-elements'; + +export const chartItems: ChartItem[] = [ + { + text: 'Q1 2024', + value: 75, + color: 'rgb(var(--color-violet-light))', + }, + { + text: 'Q4 2023', + value: 18, + color: 'rgb(var(--color-glaucous-dark))', + }, + { + text: 'Q3 2023', + value: 30, + color: 'rgb(var(--color-glaucous-default))', + }, + { + text: 'Q2 2023', + value: -6, + color: 'rgb(var(--color-glaucous-light))', + }, + { + text: 'Q1 2023', + value: -46, + color: 'rgb(var(--color-glaucous-lighter))', + }, +]; diff --git a/src/components/chart/examples/chart-items-pie.ts b/src/components/chart/examples/chart-items-pie.ts new file mode 100644 index 0000000000..26158b8f5f --- /dev/null +++ b/src/components/chart/examples/chart-items-pie.ts @@ -0,0 +1,19 @@ +import { ChartItem } from '@limetech/lime-elements'; + +export const chartItems: ChartItem[] = [ + { + text: 'Chrome', + value: 60, + color: 'rgb(var(--color-red-default))', + }, + { + text: 'Safari', + value: 30, + color: 'rgb(var(--color-blue-default))', + }, + { + text: 'Edge', + value: 10, + color: 'rgb(var(--color-cyan-default))', + }, +]; diff --git a/src/components/chart/examples/chart-items-ring.ts b/src/components/chart/examples/chart-items-ring.ts new file mode 100644 index 0000000000..7146ba7aa9 --- /dev/null +++ b/src/components/chart/examples/chart-items-ring.ts @@ -0,0 +1,33 @@ +import { ChartItem } from '@limetech/lime-elements'; +export const chartItems: ChartItem[] = [ + { + text: 'Walking', + value: 2, + formattedValue: '2h', + color: 'rgb(var(--color-coral-light))', + }, + { + text: 'Running', + value: 1, + formattedValue: '1h', + color: 'rgb(var(--color-pink-default))', + }, + { + text: 'Standing', + value: 5, + formattedValue: '5h', + color: 'rgb(var(--color-grass-default))', + }, + { + text: 'Sitting', + value: 8, + formattedValue: '8h', + color: 'rgb(var(--color-sky-default))', + }, + { + text: 'Resting', + value: 8, + formattedValue: '8h', + color: 'rgb(var(--color-glaucous-darker))', + }, +]; diff --git a/src/components/chart/examples/chart-items-stack.ts b/src/components/chart/examples/chart-items-stack.ts new file mode 100644 index 0000000000..0b9fb1f760 --- /dev/null +++ b/src/components/chart/examples/chart-items-stack.ts @@ -0,0 +1,52 @@ +import { ChartItem } from '@limetech/lime-elements'; + +export const chartItems: ChartItem[] = [ + { + text: 'Applications', + value: 10, + formattedValue: '10 gb', + color: 'rgb(var(--color-red-default))', + }, + { + text: 'Photos', + value: 52, + formattedValue: '52 gb', + color: 'rgb(var(--color-magenta-default))', + }, + { + text: 'Messages', + value: 48, + formattedValue: '48 gb', + color: 'rgb(var(--color-violet-default))', + }, + { + text: 'Podcasts', + value: 11, + formattedValue: '11 gb', + color: 'rgb(var(--color-blue-default))', + }, + { + text: 'Mail', + value: 25, + formattedValue: '25 gb', + color: 'rgb(var(--color-cyan-default))', + }, + { + text: 'Videos', + value: 80, + formattedValue: '80 gb', + color: 'rgb(var(--color-green-default))', + }, + { + text: 'OS', + value: 30, + formattedValue: '30 gb', + color: 'rgb(var(--color-glaucous-default))', + }, + { + text: 'System data', + value: 13, + formattedValue: '13 gb', + color: 'rgb(var(--color-glaucous-dark))', + }, +]; diff --git a/src/components/chart/examples/chart-items-with-negative-values.ts b/src/components/chart/examples/chart-items-with-negative-values.ts new file mode 100644 index 0000000000..2c188e4c08 --- /dev/null +++ b/src/components/chart/examples/chart-items-with-negative-values.ts @@ -0,0 +1,52 @@ +import { ChartItem } from '@limetech/lime-elements'; + +export const chartItems: ChartItem[] = [ + { + text: 'New York', + value: 11.5, + color: 'rgb(var(--color-yellow-dark))', + formattedValue: '11.5°', + }, + { + text: 'Kiruna', + value: -15, + color: 'rgb(var(--color-sky-lighter))', + formattedValue: '-15°', + }, + { + text: 'Dubai', + value: 38, + color: 'rgb(var(--color-red-default))', + formattedValue: '38°', + }, + { + text: 'Sydney', + value: 23.5, + color: 'rgb(var(--color-orange-default))', + formattedValue: '23.5°', + }, + { + text: 'Reykjavik', + value: 0, + color: 'rgb(var(--color-sky-default))', + formattedValue: '0°', + }, + { + text: 'Helsinki', + value: -7, + color: 'rgb(var(--color-sky-light))', + formattedValue: '-7°', + }, + { + text: 'Buenos Aires', + value: 22, + color: 'rgb(var(--color-orange-light))', + formattedValue: '22°', + }, + { + text: 'Tokyo', + value: 14, + color: 'rgb(var(--color-orange-lighter))', + formattedValue: '14°', + }, +]; diff --git a/src/components/chart/examples/chart-max-value.tsx b/src/components/chart/examples/chart-max-value.tsx new file mode 100644 index 0000000000..15e0f1f546 --- /dev/null +++ b/src/components/chart/examples/chart-max-value.tsx @@ -0,0 +1,35 @@ +import { Component, h } from '@stencil/core'; +import { chartItems } from './chart-items-stack'; + +/** + * Using the `maxValue` prop + * + * The `maxValue` prop defines the upper limit of the visual range for the chart. + * It determines the maximum value represented on the axis and is used to + * calculate the size of each item in the chart relative to this value. + * + * For example, if `maxValue` is set to `100`, an item with a value of `10` + * will occupy 10% of the chart, while an item with a value of `50` will occupy 50%. + * If `maxValue` is set to `200`, an item with a value of `50` will occupy 25% of the chart. + * + * If `maxValue` is not provided, the chart will calculate the maximum value based on + * the actual data points, and the size of each item will be calculated relative to + * the total range of the data. + * + * :::note + * The `maxValue` only affects the upper limit of the chart's range. + * The chart will always start from the smallest value present in the dataset, + * which could be a negative number. + * ::: + * + * @sourceFile chart-items-stack.ts + */ +@Component({ + tag: 'limel-example-chart-max-value', + shadow: true, +}) +export class ChartMaxValueExample { + public render() { + return ; + } +} diff --git a/src/components/chart/examples/chart-multi-axis-area.tsx b/src/components/chart/examples/chart-multi-axis-area.tsx new file mode 100644 index 0000000000..4fe5dccadd --- /dev/null +++ b/src/components/chart/examples/chart-multi-axis-area.tsx @@ -0,0 +1,82 @@ +import { Component, h, Host, State } from '@stencil/core'; +import { LimelSelectCustomEvent, Option } from '@limetech/lime-elements'; +import { chartItems } from './chart-items-multi-axis-negative-start-values-area'; + +/** + * Multi-axis Area Chart with Negative Start Values + * You can also get a multi-axis Area chart, by making sure that + * each item has a start value, and some of them are negative. + * + * @sourceFile chart-items-multi-axis-negative-start-values-area.ts + */ +@Component({ + tag: 'limel-example-chart-multi-axis-area-with-negative-start-values', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartMultiAxisAreaWithNegativeStartValuesExample { + @State() + private orientation: 'landscape' | 'portrait' = 'landscape'; + + @State() + private type: 'area' | 'dot' = 'area'; + + private orientations: Option[] = [ + { text: 'landscape', value: 'landscape' }, + { text: 'portrait', value: 'portrait' }, + ]; + + private types: Option[] = [ + { text: 'Area', value: 'area' }, + { text: 'Dot', value: 'dot' }, + ]; + + public render() { + return ( + +

Electricity price fluctuations, past 20 days

+ + + + + +
+ ); + } + + private getSelectedOrientation() { + return this.orientations.find( + (option) => option.value === this.orientation, + ); + } + + private handleOrientationChange = ( + event: LimelSelectCustomEvent>, + ) => { + this.orientation = event.detail.value as 'landscape' | 'portrait'; + }; + + private getSelectedType() { + return this.types.find((option) => option.value === this.type); + } + + private handleTypeChange = ( + event: LimelSelectCustomEvent>, + ) => { + this.type = event.detail.value as 'area' | 'dot'; + }; +} diff --git a/src/components/chart/examples/chart-multi-axis-gantt.tsx b/src/components/chart/examples/chart-multi-axis-gantt.tsx new file mode 100644 index 0000000000..e3398bba61 --- /dev/null +++ b/src/components/chart/examples/chart-multi-axis-gantt.tsx @@ -0,0 +1,82 @@ +import { Component, h, Host, State } from '@stencil/core'; +import { LimelSelectCustomEvent, Option } from '@limetech/lime-elements'; +import { chartItems } from './chart-items-multi-axis-negative-start-values'; + +/** + * Multi-axis with Negative Start Values + * You can also get a multi-axis chart with items in your dataset + * that have both start and end values, e.g. `value: [10, 20]`. + * + * @sourceFile chart-items-multi-axis-negative-start-values.ts + */ +@Component({ + tag: 'limel-example-chart-multi-axis-with-negative-start-values', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartMultiAxisWithNegativeStartValuesExample { + @State() + private orientation: 'landscape' | 'portrait' = 'landscape'; + + @State() + private type: 'bar' | 'dot' = 'bar'; + + private orientations: Option[] = [ + { text: 'landscape', value: 'landscape' }, + { text: 'portrait', value: 'portrait' }, + ]; + + private types: Option[] = [ + { text: 'Bar', value: 'bar' }, + { text: 'Dot', value: 'dot' }, + ]; + + public render() { + return ( + +

Temperature fluctuations past 24 hours

+ + + + + +
+ ); + } + + private getSelectedOrientation() { + return this.orientations.find( + (option) => option.value === this.orientation, + ); + } + + private handleOrientationChange = ( + event: LimelSelectCustomEvent>, + ) => { + this.orientation = event.detail.value as 'landscape' | 'portrait'; + }; + + private getSelectedType() { + return this.types.find((option) => option.value === this.type); + } + + private handleTypeChange = ( + event: LimelSelectCustomEvent>, + ) => { + this.type = event.detail.value as 'bar' | 'dot'; + }; +} diff --git a/src/components/chart/examples/chart-multi-axis.tsx b/src/components/chart/examples/chart-multi-axis.tsx new file mode 100644 index 0000000000..50ac5b80b6 --- /dev/null +++ b/src/components/chart/examples/chart-multi-axis.tsx @@ -0,0 +1,84 @@ +import { Component, h, Host, State } from '@stencil/core'; +import { LimelSelectCustomEvent, Option } from '@limetech/lime-elements'; +import { chartItems } from './chart-items-with-negative-values'; + +/** + * Multi-axis Charts + * Normally, charts visualize items in a positive range. + * However, there are cases where you want to visualize items that have both + * positive and negative `value`s. + * + * @sourceFile chart-items-with-negative-values.ts + */ +@Component({ + tag: 'limel-example-chart-multi-axis', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartTypeMultiAxisExample { + @State() + private orientation: 'landscape' | 'portrait' = 'landscape'; + + @State() + private type: 'bar' | 'dot' | 'line' = 'dot'; + + private orientations: Option[] = [ + { text: 'landscape', value: 'landscape' }, + { text: 'portrait', value: 'portrait' }, + ]; + + private types: Option[] = [ + { text: 'Bar', value: 'bar' }, + { text: 'Dot', value: 'dot' }, + { text: 'Line', value: 'line' }, + ]; + + public render() { + return ( + +

Temperature right now

+ + + + + +
+ ); + } + + private getSelectedOrientation() { + return this.orientations.find( + (option) => option.value === this.orientation, + ); + } + + private handleOrientationChange = ( + event: LimelSelectCustomEvent>, + ) => { + this.orientation = event.detail.value as 'landscape' | 'portrait'; + }; + + private getSelectedType() { + return this.types.find((option) => option.value === this.type); + } + + private handleTypeChange = ( + event: LimelSelectCustomEvent>, + ) => { + this.type = event.detail.value as 'bar' | 'dot' | 'line'; + }; +} diff --git a/src/components/chart/examples/chart-orientation.tsx b/src/components/chart/examples/chart-orientation.tsx new file mode 100644 index 0000000000..0c5aa54fdf --- /dev/null +++ b/src/components/chart/examples/chart-orientation.tsx @@ -0,0 +1,31 @@ +import { Component, h } from '@stencil/core'; +import { chartItems } from './chart-items-stack'; + +/** + * The `orientation` prop + * Using the `orientation` prop, you can change the direction of + * the the chart. Note that the `orientation` prop + * does not have any effect on those `type`s of visualization that + * do not have the common `X` and `Y` axises, such as `pie` or `doughnut`. + * + * :::note + * Charts are responsive and stretch inside their containers. + * You need to set ensure that there space for them to get rendered in. + * ::: + * @sourceFile chart-items-stack.ts + */ +@Component({ + tag: 'limel-example-chart-orientation', + shadow: true, +}) +export class ChartOrientationExample { + public render() { + return ( + + ); + } +} diff --git a/src/components/chart/examples/chart-resizable-container.scss b/src/components/chart/examples/chart-resizable-container.scss new file mode 100644 index 0000000000..596b8bc113 --- /dev/null +++ b/src/components/chart/examples/chart-resizable-container.scss @@ -0,0 +1,63 @@ +* { + box-sizing: border-box; +} + +:host { + position: relative; + display: flex; + flex-direction: column; + gap: 1rem; + + resize: both; + overflow: auto; + + box-sizing: border-box; + + min-width: 10rem; + width: 100%; + max-width: 100%; + + min-height: 5rem; + height: 100%; + max-height: 50rem; + + padding: 1rem 1rem 3rem 1rem; + border: 0.125rem dashed rgb(var(--contrast-500)); + + border-radius: 0.5rem; + + &::after { + content: 'Resize me ⤵'; + font-size: 0.75rem; + position: absolute; + right: 0.25rem; + bottom: 0.25rem; + } +} + +:host(.large) { + height: 25rem; +} + +:host(.tall) { + height: 35rem; +} + +:host(.row-layout) { + flex-direction: row; +} + +limel-example-controls { + --example-controls-column-layout: auto-fit; + padding: 1rem 0.5rem; + height: fit-content; + min-width: 40%; +} + +:host(.creative-styling) { + gap: 4rem; + + limel-chart[type='stacked-bar'] { + height: 6rem; + } +} diff --git a/src/components/chart/examples/chart-styling.scss b/src/components/chart/examples/chart-styling.scss new file mode 100644 index 0000000000..e7201b5ec4 --- /dev/null +++ b/src/components/chart/examples/chart-styling.scss @@ -0,0 +1,27 @@ +:host(limel-example-chart-styling) { + display: flex; + flex-direction: column; + gap: 2rem; +} + +limel-chart { + &[type='bar'] { + --chart-item-border-radius: 0.5rem; + height: 20rem; + } + + &[type='stacked-bar'] { + --chart-item-divider-color: transparent; + --chart-item-border-radius: 1.5rem; + --chart-background-color: transparent; + + height: var(--chart-item-border-radius); + border-radius: var(--chart-item-border-radius); + overflow: hidden; + } +} + +div[role='separator'] { + width: 100%; + border-top: 1px dashed rgb(var(--color-green-light)); +} diff --git a/src/components/chart/examples/chart-styling.tsx b/src/components/chart/examples/chart-styling.tsx new file mode 100644 index 0000000000..41a4381bc3 --- /dev/null +++ b/src/components/chart/examples/chart-styling.tsx @@ -0,0 +1,25 @@ +import { Component, h } from '@stencil/core'; +import { chartItems } from './chart-items-ring'; + +/** + * Styling + * The component provides a few styling options, using CSS custom properties. + * + * @sourceFile chart-items-gantt-negative-values.ts + */ +@Component({ + tag: 'limel-example-chart-styling', + shadow: true, + styleUrl: 'chart-styling.scss', +}) +export class ChartStackedBarExample { + public render() { + return [ +

Stacked-bar Chart

, + , +
, +

Bar Chart

, + , + ]; + } +} diff --git a/src/components/chart/examples/chart-type-area.tsx b/src/components/chart/examples/chart-type-area.tsx new file mode 100644 index 0000000000..e2aec0324c --- /dev/null +++ b/src/components/chart/examples/chart-type-area.tsx @@ -0,0 +1,87 @@ +import { Component, h, Host, State } from '@stencil/core'; +import { LimelSelectCustomEvent, Option } from '@limetech/lime-elements'; +import { chartItems } from './chart-items-area'; + +/** + * Area chart + * An area chart is like a line chart but with the area below the line filled in, + * representing cumulative data. + * + * It's good for: + * - Showing cumulative totals over time. + * - Emphasizing data changes while highlighting volume or totals. + * + * :::tip + * **Use:** + * - For showing cumulative data trends where total volume over time is meaningful. + * - When visualizing stacked data in a cumulative format. + * + * **Avoid:** + * - If individual values need precise comparison (stacked bar charts are more suitable). + * - For datasets with highly fluctuating values, as overlapping areas can obscure details. + * ::: + * @sourceFile chart-items-area.ts + */ +@Component({ + tag: 'limel-example-chart-type-area', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartTypeAreaExample { + @State() + private maxValue = 100; + + @State() + private orientation: 'landscape' | 'portrait' = 'landscape'; + + private orientations: Option[] = [ + { text: 'landscape', value: 'landscape' }, + { text: 'portrait', value: 'portrait' }, + ]; + + public render() { + const defaultMaxValue = `${this.maxValue}`; + + return ( + +

Subscriptions per month

+ + + + + +
+ ); + } + + private getSelectedOrientation() { + return this.orientations.find( + (option) => option.value === this.orientation, + ); + } + + private handleOrientationChange = ( + event: LimelSelectCustomEvent>, + ) => { + this.orientation = event.detail.value as 'landscape' | 'portrait'; + }; + + private handleMaxValueChange = (event) => { + this.maxValue = +event.detail; + }; +} diff --git a/src/components/chart/examples/chart-type-bar.tsx b/src/components/chart/examples/chart-type-bar.tsx new file mode 100644 index 0000000000..9ba5304929 --- /dev/null +++ b/src/components/chart/examples/chart-type-bar.tsx @@ -0,0 +1,87 @@ +import { Component, h, Host, State } from '@stencil/core'; +import { LimelSelectCustomEvent, Option } from '@limetech/lime-elements'; +import { chartItems } from './chart-items-bar'; + +/** + * Bar chart + * A bar chart displays data with rectangular bars, where the length of each bar represents the value of a data point. + * + * It's good for: + * - Comparing discrete categories or groups. + * - Visualizing changes in data over time when categories are limited. + * + * :::tip + * **Use:** + * - When you have categorical data that needs clear comparisons. + * - For datasets with fewer than 20 categories, as too many bars can make it hard to read. + * + * **Avoid:** + * - When showing continuous data trends over time (a line chart might be better). + * - When you have many categories, which could make the chart crowded. + * ::: + * + * @sourceFile chart-items-bar.ts + */ +@Component({ + tag: 'limel-example-chart-type-bar', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartTypeBarExample { + @State() + private maxValue = 100; + + @State() + private orientation: 'landscape' | 'portrait' = 'landscape'; + + private orientations: Option[] = [ + { text: 'landscape', value: 'landscape' }, + { text: 'portrait', value: 'portrait' }, + ]; + + public render() { + const defaultMaxValue = `${this.maxValue}`; + + return ( + +

Subscriptions per month

+ + + + + +
+ ); + } + + private getSelectedOrientation() { + return this.orientations.find( + (option) => option.value === this.orientation, + ); + } + + private handleOrientationChange = ( + event: LimelSelectCustomEvent>, + ) => { + this.orientation = event.detail.value as 'landscape' | 'portrait'; + }; + + private handleMaxValueChange = (event) => { + this.maxValue = +event.detail; + }; +} diff --git a/src/components/chart/examples/chart-type-dot.tsx b/src/components/chart/examples/chart-type-dot.tsx new file mode 100644 index 0000000000..f0f16fcc17 --- /dev/null +++ b/src/components/chart/examples/chart-type-dot.tsx @@ -0,0 +1,89 @@ +import { Component, h, Host, State } from '@stencil/core'; +import { LimelSelectCustomEvent, Option } from '@limetech/lime-elements'; +import { chartItems } from './chart-items-bar'; + +/** + * Dot chart + * A dot chart displays values for each category using dots along an axis, similar to a bar chart but with dots + * at the value associated with each category instead of bars. It’s often used as an alternative to bar charts, + * especially when focusing on individual data points or reducing visual clutter. + * + * It's good for: + * - Comparing values across categories in a clean and uncluttered way. + * - Visualizing discrete data points without the visual weight of bars. + * - Allowing readers to focus on precise values and distribution. + * + * :::tip + * **Use:** + * - When comparing values across categories in a straightforward way. + * - For datasets where you do not want to emphasize on or compare "volumes" or "sizes", + * but rather compare the points that the data represents. + * + * **Avoid:** + * - For datasets with very few or very densely packed points, which could make the chart difficult to read. + * - When representing complex relationships or multiple variables (scatter plots or line charts may be more effective). + * ::: + * @sourceFile chart-items-bar.ts + */ +@Component({ + tag: 'limel-example-chart-type-dot', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartTypeDotExample { + @State() + private maxValue = 100; + + @State() + private orientation: 'landscape' | 'portrait' = 'landscape'; + + private orientations: Option[] = [ + { text: 'landscape', value: 'landscape' }, + { text: 'portrait', value: 'portrait' }, + ]; + + public render() { + const defaultMaxValue = `${this.maxValue}`; + + return ( + +

Subscriptions per month

+ + + + + +
+ ); + } + + private getSelectedOrientation() { + return this.orientations.find( + (option) => option.value === this.orientation, + ); + } + + private handleOrientationChange = ( + event: LimelSelectCustomEvent>, + ) => { + this.orientation = event.detail.value as 'landscape' | 'portrait'; + }; + private handleMaxValueChange = (event) => { + this.maxValue = +event.detail; + }; +} diff --git a/src/components/chart/examples/chart-type-doughnut.tsx b/src/components/chart/examples/chart-type-doughnut.tsx new file mode 100644 index 0000000000..c19734ebec --- /dev/null +++ b/src/components/chart/examples/chart-type-doughnut.tsx @@ -0,0 +1,58 @@ +import { Component, h, Host, State } from '@stencil/core'; +import { chartItems } from './chart-items-pie'; + +/** + * Doughnut chart + * A doughnut chart is a variation of the pie chart, with a center space, + * often used to show multiple concentric data series. + * + * It's good for: + * - Showing proportions with a visually balanced layout. + * - Allowing room in the center for additional information (e.g., displaying totals). + * + * :::tip + * **Use:** + * - When visual space is limited, and a pie chart may look crowded. + * - When you have a limited number of categories (at least 3, and maximum ~10). + * - For static data composition, not suitable for showing time trends. + * + * **Avoid:** + * - When precise comparisons are needed, as bars provide clearer detail. + * - With complex or large datasets where slices become too small to read. + * ::: + * @sourceFile chart-items-pie.ts + */ +@Component({ + tag: 'limel-example-chart-type-doughnut', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartTypeDoughnutExample { + @State() + private maxValue = 140; + + public render() { + const defaultMaxValue = `${this.maxValue}`; + + return ( + + + + + + + ); + } + private handleMaxValueChange = (event) => { + this.maxValue = +event.detail; + }; +} diff --git a/src/components/chart/examples/chart-type-gannt.tsx b/src/components/chart/examples/chart-type-gannt.tsx new file mode 100644 index 0000000000..b1979210d0 --- /dev/null +++ b/src/components/chart/examples/chart-type-gannt.tsx @@ -0,0 +1,94 @@ +import { Component, h, Host, State } from '@stencil/core'; +import { LimelSelectCustomEvent, Option } from '@limetech/lime-elements'; +import { chartItems } from './chart-items-gantt'; + +/** + * Gantt chart + * Gantt charts are used to visualize items that have a defined start and end value, making them ideal + * for displaying timelines or project phases. Each item typically represents a phase or task, with its length + * indicating duration. + * + * It's good for: + * - Visualizing project schedules, with tasks and milestones over time. + * - Showing task dependencies, start and end dates, and overlaps between phases. + * - Providing an easy-to-understand timeline for project planning and tracking. + * + * :::tip + * **Use:** + * - When you need to show the progression of tasks or stages over time. + * - When items have start points which are not simply zero. + * + * **Avoid:** + * - For datasets that don't involve time or sequential phases (bar charts or line charts may be better). + * + * **Note:** + * In Gantt charts, items have a start value to indicate when they begin. Unlike other charts, + * where items default to a start value of `0`, each Gantt chart item should specify a start value + * and an end value (e.g., `value: [10, 20]`), which determines the duration and position of the item. + * ::: + * + * @sourceFile chart-items-gantt.ts + */ +@Component({ + tag: 'limel-example-chart-type-gantt', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartTypeGanttExample { + @State() + private maxValue = 170; + + @State() + private orientation: 'landscape' | 'portrait' = 'portrait'; + + private orientations: Option[] = [ + { text: 'landscape', value: 'landscape' }, + { text: 'portrait', value: 'portrait' }, + ]; + + public render() { + const defaultMaxValue = `${this.maxValue}`; + + return ( + +

Project Timeline: Key Phases from Concept to Launch

+ + + + + +
+ ); + } + + private getSelectedOrientation() { + return this.orientations.find( + (option) => option.value === this.orientation, + ); + } + + private handleOrientationChange = ( + event: LimelSelectCustomEvent>, + ) => { + this.orientation = event.detail.value as 'landscape' | 'portrait'; + }; + + private handleMaxValueChange = (event) => { + this.maxValue = +event.detail; + }; +} diff --git a/src/components/chart/examples/chart-type-line.tsx b/src/components/chart/examples/chart-type-line.tsx new file mode 100644 index 0000000000..45faab7a23 --- /dev/null +++ b/src/components/chart/examples/chart-type-line.tsx @@ -0,0 +1,88 @@ +import { Component, h, Host, State } from '@stencil/core'; +import { LimelSelectCustomEvent, Option } from '@limetech/lime-elements'; +import { chartItems } from './chart-items-area'; + +/** + * Line chart + * * A line chart connects data points with a continuous line, + * often used for displaying trends over intervals. + * + * It's good for: + * - Showing trends over time in a simple, readable format. + * - Highlighting increases, decreases, or patterns in a dataset. + * + * :::tip + * **Use:** + * - For tracking data changes over time, especially with multiple data points. + * - When visualizing time-series data to show overall trends. + * + * **Avoid:** + * - For large fluctuations, which may make data misinterpretations likely. + * - When individual point comparison is critical (consider a bar or dot chart). + * ::: + * + * @sourceFile chart-items-area.ts + */ +@Component({ + tag: 'limel-example-chart-type-line', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartTypeLineExample { + @State() + private maxValue = 100; + + @State() + private orientation: 'landscape' | 'portrait' = 'landscape'; + + private orientations: Option[] = [ + { text: 'landscape', value: 'landscape' }, + { text: 'portrait', value: 'portrait' }, + ]; + + public render() { + const defaultMaxValue = `${this.maxValue}`; + + return ( + +

Subscriptions per month

+ + + + + +
+ ); + } + + private getSelectedOrientation() { + return this.orientations.find( + (option) => option.value === this.orientation, + ); + } + + private handleOrientationChange = ( + event: LimelSelectCustomEvent>, + ) => { + this.orientation = event.detail.value as 'landscape' | 'portrait'; + }; + + private handleMaxValueChange = (event) => { + this.maxValue = +event.detail; + }; +} diff --git a/src/components/chart/examples/chart-type-nps.tsx b/src/components/chart/examples/chart-type-nps.tsx new file mode 100644 index 0000000000..95477cccc7 --- /dev/null +++ b/src/components/chart/examples/chart-type-nps.tsx @@ -0,0 +1,60 @@ +import { Component, h, Host } from '@stencil/core'; +import { chartItems } from './chart-items-nps'; + +/** + * NPS® chart + * The NPS chart visually represents customer loyalty by plotting scores that + * range from -100 to +100. NPS is based on customer responses to a simple question: + * "_How likely are you to recommend us?_" + * + * Respondents score from 0 to 10, which is then transformed into the NPS scale which starts from -100 and + * goes up to +100. The NPS chart groups scores into three categories of: + * detractors, + * passives, + * or promoters. + * + * An NPS score above 30 is considered + * good, + * while a score above 70 is considered + * excellent. + * + * This chart is good for: + * - Summarizing customer satisfaction or loyalty on a single scale. + * - Quickly identifying the distribution of detractors, passives, and promoters. + * + * :::tip + * **Use:** + * - Visualizing a single score that summarizes customer loyalty. + * - When tracking customer loyalty over time. + * - When tracking customer loyalty of different companies. + * - In dashboards or reporting tools to visualize changes in customer sentiment. + * + * **Avoid:** + * - For in-depth customer feedback analysis (consider pairing with more detailed survey insights). + * - Any other data visualization than NPS scores. + * ::: + * + * :::important + * For the `nps` chart type to visualize properly, the `value` property of the `ChartItem` + * should be a number between `-100` and `100`! + * ::: + * @sourceFile chart-items-nps.ts + */ +@Component({ + tag: 'limel-example-chart-type-nps', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartTypeNpsExample { + public render() { + return ( + +

+ Our Net Promoter Score Development During the Past 5 + Quarters +

+ +
+ ); + } +} diff --git a/src/components/chart/examples/chart-type-pie.tsx b/src/components/chart/examples/chart-type-pie.tsx new file mode 100644 index 0000000000..fb2c90af50 --- /dev/null +++ b/src/components/chart/examples/chart-type-pie.tsx @@ -0,0 +1,57 @@ +import { Component, h, Host, State } from '@stencil/core'; +import { chartItems } from './chart-items-pie'; + +/** + * Pie chart + * A pie chart represents data as slices of a circle, with each slice’s size proportional to its value. + * * + * It's good for: + * - Showing the proportions of a whole. + * - Visualizing data composition for easy understanding. + * + * :::tip + * **Use:** + * - When you have a limited number of categories (at least 3, and maximum ~10). + * - For static data composition, not suitable for showing time trends. + * + * **Avoid:** + * - When precise comparisons are needed, as bars provide clearer detail. + * - With complex or large datasets where slices become too small to read. + * ::: + * + * @sourceFile chart-items-pie.ts + */ +@Component({ + tag: 'limel-example-chart-type-pie', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartTypePieExample { + @State() + private maxValue = 300; + + public render() { + const defaultMaxValue = `${this.maxValue}`; + + return ( + + + + + + + ); + } + private handleMaxValueChange = (event) => { + this.maxValue = +event.detail; + }; +} diff --git a/src/components/chart/examples/chart-type-ring.tsx b/src/components/chart/examples/chart-type-ring.tsx new file mode 100644 index 0000000000..72cc2cb0b8 --- /dev/null +++ b/src/components/chart/examples/chart-type-ring.tsx @@ -0,0 +1,57 @@ +import { Component, h, Host, State } from '@stencil/core'; +import { chartItems } from './chart-items-ring'; + +/** + * Ring chart + * A ring chart is similar to a doughnut chart but used in concentric layers, + * ideal for comparison of hierarchical data. + * + * It's good for: + * - Comparing multiple parts of a whole in a layered visual layout. + * - Displaying hierarchical data or showing nested relationships. + * + * :::tip + * **Use:** + * - When you need to show multiple data series in a single, visually appealing chart. + * - For data with a clear hierarchy or grouping. + * + * **Avoid:** + * - With too many rings, as it can become visually overwhelming. + * - For data that needs precise comparison across series. + * ::: + * @sourceFile chart-items-ring.ts + */ +@Component({ + tag: 'limel-example-chart-type-ring', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartTypeRingExample { + @State() + private maxValue = 10; + + public render() { + const defaultMaxValue = `${this.maxValue}`; + + return ( + + + + + + + ); + } + private handleMaxValueChange = (event) => { + this.maxValue = +event.detail; + }; +} diff --git a/src/components/chart/examples/chart-type-stacked-bar.tsx b/src/components/chart/examples/chart-type-stacked-bar.tsx new file mode 100644 index 0000000000..ac83a78865 --- /dev/null +++ b/src/components/chart/examples/chart-type-stacked-bar.tsx @@ -0,0 +1,43 @@ +import { Component, h } from '@stencil/core'; +import { chartItems } from './chart-items-stack'; + +/** + * Stacked bar (default) + * + * You can simply provide a list of items to the chart component, + * and it will visualize them the way you want. For the default + * visualization, the component uses the `stacked-bar` `type`, + * as this is the most compact form of date visualization. + * + * The only thing each item needs is a `text`, a `value`, and a your + * choice of `color`. + * + * * A stacked bar chart builds creates a data visualization + * by stacking multiple data series in each bar. + * + * It's good for: + * - Showing the composition of categories across multiple groups. + * - Highlighting cumulative values while breaking down individual contributions. + * + * :::tip + * **Use:** + * - When you want to show both the total and individual values in each category. + * - For data with sub-categories or components that need visualization. + * + * **Avoid:** + * - If precise individual comparisons between subcategories are necessary. + * - When there are too many categories, making the chart crowded. + * ::: + * + * @sourceFile chart-items-stack.ts + */ +@Component({ + tag: 'limel-example-chart-stacked-bar', + shadow: true, + styleUrl: 'chart-examples.scss', +}) +export class ChartStackedBarExample { + public render() { + return ; + } +} diff --git a/src/components/chart/partial-styles/_area_line.scss b/src/components/chart/partial-styles/_area_line.scss new file mode 100644 index 0000000000..318f9ba0de --- /dev/null +++ b/src/components/chart/partial-styles/_area_line.scss @@ -0,0 +1,174 @@ +:host(limel-chart[type='area']), +:host(limel-chart[type='line']) { + .item { + position: relative; + + &:after { + // the dot + margin: auto; + width: $min-item-size; + height: $min-item-size; + border-radius: 50%; + border: 1px solid rgb(var(--contrast-100)); + } + + &:before { + // the line & area + inset: 0; + } + + &:after, + &:before { + transition: + border-color 0.2s ease, + opacity 0.4s ease; + content: ''; + position: absolute; + background: var(--limel-chart-item-color, $default-item-color); + } + + &:hover, + &:focus-visible { + &:after { + border-color: transparent; + } + } + } +} + +:host(limel-chart[type='line'][orientation='landscape']), +:host(limel-chart[type='area'][orientation='landscape']) { + .item { + height: 100%; + + &:after { + transform: translateX(-50%) translateY(50%); + left: 0; + bottom: calc( + (var(--limel-chart-item-size) + var(--limel-chart-item-offset)) * + 1% + ); + } + } +} + +:host(limel-chart[type='line'][orientation='portrait']), +:host(limel-chart[type='area'][orientation='portrait']) { + .item { + width: 100%; + + &:after { + transform: translateX(-50%) translateY(-50%); + left: calc( + (var(--limel-chart-item-size) + var(--limel-chart-item-offset)) * + 1% + ); + } + } +} + +// Area chart + +:host(limel-chart[type='area']) { + .item { + &:after { + opacity: 0; + } + + &:hover, + &:focus-visible { + &:after { + opacity: 1; + } + &:before { + opacity: 0.7; + } + } + } +} + +:host(limel-chart[type='area'][orientation='landscape']) { + $start: 0 calc((100 - var(--limel-chart-item-offset)) * 1%); + /* prettier-ignore */ + $first: 0 calc((100 - (var(--limel-chart-item-size) + var(--limel-chart-item-offset))) * 1%); + /* prettier-ignore */ + $second: 100% calc((100 - (var(--limel-chart-next-item-size) + var(--limel-chart-next-item-offset))) * 1%); + $end: 100% calc((100 - var(--limel-chart-next-item-offset)) * 1%); + + .item { + &:before { + clip-path: polygon($start, $first, $second, $end); + } + } +} + +:host(limel-chart[type='area'][orientation='portrait']) { + $start: calc(var(--limel-chart-item-offset) * 1%) 0; + /* prettier-ignore */ + $first: calc((var(--limel-chart-item-size) + var(--limel-chart-item-offset)) * 1%) 0; + /* prettier-ignore */ + $second: calc((var(--limel-chart-next-item-size) + var(--limel-chart-next-item-offset)) * 1%) 100%; + $end: calc(var(--limel-chart-next-item-offset) * 1%) 100%; + + .item { + &:before { + clip-path: polygon($start, $first, $second, $end); + } + } +} + +// Line chart + +:host(limel-chart[type='line']) { + --limel-chart-line-thickness: 0.125rem; + + .item { + &:hover { + &:before { + opacity: 0.4; + } + } + } +} + +:host(limel-chart[type='line'][orientation='landscape']) { + /* prettier-ignore */ + $start: 0 calc((100 - (var(--limel-chart-item-size) + var(--limel-chart-item-offset))) * 1%); + /* prettier-ignore */ + $first: 0 calc(((100 - (var(--limel-chart-item-size) + var(--limel-chart-item-offset))) * 1%) + var(--limel-chart-line-thickness)); + /* prettier-ignore */ + $second: 100% calc(((100 - (var(--limel-chart-next-item-size) + var(--limel-chart-next-item-offset))) * 1%) + var(--limel-chart-line-thickness)); + /* prettier-ignore */ + $end: 100% calc((100 - (var(--limel-chart-next-item-size) + var(--limel-chart-next-item-offset))) * 1%); + + .item { + &:hover { + @include line(vertical, $position: left); + } + + &:before { + clip-path: polygon($start, $first, $second, $end); + } + } +} + +:host(limel-chart[type='line'][orientation='portrait']) { + /* prettier-ignore */ + $start: calc(((var(--limel-chart-item-size) + var(--limel-chart-item-offset))) * 1%) 0; + /* prettier-ignore */ + $first: calc((((var(--limel-chart-item-size) + var(--limel-chart-item-offset)) * 1%) + var(--limel-chart-line-thickness))) 0; + /* prettier-ignore */ + $second: calc(((var(--limel-chart-next-item-size) + var(--limel-chart-next-item-offset)) * 1%) + var(--limel-chart-line-thickness)) 100%; + /* prettier-ignore */ + $end: calc(((var(--limel-chart-next-item-size) + var(--limel-chart-next-item-offset)) * 1%)) 100%; + + .item { + &:hover { + @include line(horizontal, $position: top); + } + + &:before { + clip-path: polygon($start, $first, $second, $end); + } + } +} diff --git a/src/components/chart/partial-styles/_axises.scss b/src/components/chart/partial-styles/_axises.scss new file mode 100644 index 0000000000..5ffadc44e4 --- /dev/null +++ b/src/components/chart/partial-styles/_axises.scss @@ -0,0 +1,67 @@ +.axises { + position: absolute; + display: flex; + justify-content: space-between; + min-height: 100%; + min-width: 100%; + height: 100%; + width: 100%; +} + +.axis-line { + transition: opacity 0.4s ease; + position: relative; + opacity: 0.2; + + font-size: 0.625rem; + border-color: var(--limel-chart-axis-line-color); + + &:hover { + opacity: 0.6; + transition-duration: 0.2s; + } + + &.zero-line { + opacity: 0.6; + z-index: 1; + } + + limel-badge { + --badge-background-color: transparent; + --badge-text-color: currentColor; + position: absolute; + text-align: right; + min-width: 2rem; + } +} + +:host(limel-chart[orientation='landscape']) { + .axises { + flex-direction: column-reverse; + } + .axis-line { + border-bottom: 1px solid; + transform: translateY(50%); + + limel-badge { + bottom: 0; + left: -2rem; + transform: translateY(50%); + } + } +} + +:host(limel-chart[orientation='portrait']) { + .axises { + flex-direction: row; + } + .axis-line { + border-left: 1px solid; + transform: translateX(-50%); + + limel-badge { + bottom: -1rem; + right: -1rem; + } + } +} diff --git a/src/components/chart/partial-styles/_bar-gantt-dot.scss b/src/components/chart/partial-styles/_bar-gantt-dot.scss new file mode 100644 index 0000000000..3577502fc8 --- /dev/null +++ b/src/components/chart/partial-styles/_bar-gantt-dot.scss @@ -0,0 +1,119 @@ +:host(limel-chart[type='bar']), +:host(limel-chart[type='dot']) { + .chart { + gap: 0.5rem; + } + + .item { + display: flex; + align-items: center; + justify-content: center; + + border-radius: var(--chart-item-border-radius, 0.125rem); + } +} + +:host(limel-chart[type='bar']) { + .item { + background: var(--limel-chart-item-color, $default-item-color); + } +} + +:host(limel-chart[type='dot']) { + .item { + &:before, + &:after { + content: ''; + position: absolute; + margin: auto; + + width: $min-item-size; + height: $min-item-size; + border-radius: 50%; + } + &::after { + background-color: var( + --limel-chart-item-color, + $default-item-color + ); + } + + &.has-start-value { + &:before { + background-color: var( + --limel-chart-item-color, + $default-item-color + ); + } + } + } +} + +:host(limel-chart[type='bar'][orientation='landscape']), +:host(limel-chart[type='dot'][orientation='landscape']) { + .item { + height: calc(var(--limel-chart-item-size) * 1%); + bottom: calc(var(--limel-chart-item-offset) * 1%); + + &.has-negative-value-only { + height: calc(var(--limel-chart-item-size) * -1%); + + transform-origin: bottom; + transform: rotateX(180deg); + } + } +} + +:host(limel-chart[type='dot'][orientation='landscape']) { + .item { + &.has-start-value, + &:hover, + &:focus-visible { + @include line(vertical); + } + + &:before { + inset: auto 0 0 0; + transform: translateY(50%); + } + &::after { + inset: 0 0 auto 0; + transform: translateY(-50%); + } + } +} + +:host(limel-chart[type='bar'][orientation='portrait']), +:host(limel-chart[type='dot'][orientation='portrait']) { + .item { + width: calc(var(--limel-chart-item-size) * 1%); + left: calc(var(--limel-chart-item-offset) * 1%); + + &.has-negative-value-only { + width: calc(var(--limel-chart-item-size) * -1%); + + transform-origin: left; + transform: rotateY(180deg); + } + } +} + +:host(limel-chart[type='dot'][orientation='portrait']) { + .item { + &.has-start-value, + &:hover, + &:focus-visible { + @include line(horizontal); + } + + &:before { + inset: 0 auto 0 0; + transform: translateX(-50%); + } + + &:after { + inset: 0 0 0 auto; + transform: translateX(50%); + } + } +} diff --git a/src/components/chart/partial-styles/_layout-for-charts-with-circular-shape.scss b/src/components/chart/partial-styles/_layout-for-charts-with-circular-shape.scss new file mode 100644 index 0000000000..b8fe2530ef --- /dev/null +++ b/src/components/chart/partial-styles/_layout-for-charts-with-circular-shape.scss @@ -0,0 +1,46 @@ +:host(limel-chart[type='pie']), +:host(limel-chart[type='doughnut']), +:host(limel-chart[type='ring']) { + table { + min-height: 2rem; + min-width: 2rem; + } + + .chart, + .item { + aspect-ratio: 1; + display: flex; + margin: auto; + } + + .chart { + justify-content: center; + align-items: center; + + &:before { + aspect-ratio: 1; + content: ''; + position: absolute; + z-index: 0; + inset: 0; + margin: auto; + + border-radius: 50%; + max-width: 100%; + max-height: 100%; + background-color: var( + --chart-background-color, + rgb(var(--contrast-200)) + ); + } + } + + .item { + max-width: 100%; + max-height: 100%; + border-radius: 50%; + + position: absolute; + inset: 0; + } +} diff --git a/src/components/chart/partial-styles/_layout-for-charts-with-x-y-axises.scss b/src/components/chart/partial-styles/_layout-for-charts-with-x-y-axises.scss new file mode 100644 index 0000000000..bbfeb3df25 --- /dev/null +++ b/src/components/chart/partial-styles/_layout-for-charts-with-x-y-axises.scss @@ -0,0 +1,52 @@ +:host(limel-chart[type='bar']), +:host(limel-chart[type='dot']), +:host(limel-chart[type='line']), +:host(limel-chart[type='area']) { + .chart { + display: flex; + background-color: var(--chart-background-color, transparent); + } + + .item { + @include mixins.visualize-keyboard-focus; + position: relative; + mix-blend-mode: hard-light; + } +} + +:host(limel-chart[type='bar'][orientation='landscape']), +:host(limel-chart[type='dot'][orientation='landscape']), +:host(limel-chart[type='line'][orientation='landscape']), +:host(limel-chart[type='area'][orientation='landscape']) { + --limel-chart-padding: 0.5rem 0.5rem 0.5rem 2rem; + + .chart { + flex-direction: row; + align-items: flex-end; + overflow: auto hidden; + padding: 0 0.125rem; + } + + .item { + min-width: $min-item-size; + width: inherit; + } +} + +:host(limel-chart[type='bar'][orientation='portrait']), +:host(limel-chart[type='dot'][orientation='portrait']), +:host(limel-chart[type='line'][orientation='portrait']), +:host(limel-chart[type='area'][orientation='portrait']) { + --limel-chart-padding: 0.5rem 0.5rem 1rem 0.5rem; + + .chart { + flex-direction: column; + overflow: hidden auto; + padding: 0.125rem 0; + } + + .item { + min-height: $min-item-size; + height: inherit; + } +} diff --git a/src/components/chart/partial-styles/_nps.scss b/src/components/chart/partial-styles/_nps.scss new file mode 100644 index 0000000000..d2e05eaa52 --- /dev/null +++ b/src/components/chart/partial-styles/_nps.scss @@ -0,0 +1,121 @@ +:host(limel-chart[type='nps']) { + --limel-chart-nps-gauge-angel: 220deg; + $detractors: rgb(var(--color-coral-default)) 0deg + calc(var(--limel-chart-nps-gauge-angel) / 2); + $passives: rgb(var(--color-amber-light)) + calc(var(--limel-chart-nps-gauge-angel) / 2) + calc(var(--limel-chart-nps-gauge-angel) * 0.65); + $promoters-good: rgb(var(--color-lime-light)) + calc(var(--limel-chart-nps-gauge-angel) * 0.65) + calc(var(--limel-chart-nps-gauge-angel) * 0.85); + $promoters-excellent: rgb(var(--color-lime-default)) + calc(var(--limel-chart-nps-gauge-angel) * 0.85) + var(--limel-chart-nps-gauge-angel); + $thickness-of-colorful-area: min(3rem, 20%); + + table { + min-height: 4rem; + min-width: 4rem; + } + + .chart { + position: relative; + display: flex; + justify-content: center; + align-items: center; + + aspect-ratio: 1; + margin: auto; + + width: unset; + height: unset; + max-width: 100%; + max-height: 100%; + rotate: calc(var(--limel-chart-nps-gauge-angel) / 2 * -1); + transform: translate(-15%, -5%); // values depend on `rotate` above + + &:before, + &:after { + content: ''; + aspect-ratio: 1; + position: absolute; + border-radius: 50%; + z-index: -1; + min-height: 0; + min-width: 0; + } + &:before { + height: 100%; + max-height: 100%; + background: conic-gradient( + $detractors, + $passives, + $promoters-good, + $promoters-excellent, + transparent var(--limel-chart-nps-gauge-angel) + ); + } + &:after { + height: calc(100% - ($thickness-of-colorful-area * 2)); + max-height: calc(100% - ($thickness-of-colorful-area * 2)); + background: conic-gradient( + var(--chart-background-color, rgb(var(--contrast-100))) 0deg + var(--limel-chart-nps-gauge-angel), + transparent var(--limel-chart-nps-gauge-angel) + ); + } + } + + .item { + $item-rotation-angel: calc( + (var(--limel-chart-item-value) + 100) / 200 * + var(--limel-chart-nps-gauge-angel) + ); + + @include mixins.visualize-keyboard-focus; + display: flex; + align-items: flex-start; + justify-content: center; + + border-radius: 0.5rem; + position: absolute; + height: calc(50% - $thickness-of-colorful-area + 0.5rem); + width: 0.5rem; + transform: translateY(-50%) rotate($item-rotation-angel); + transform-origin: bottom; + + &:hover, + &:focus-visible { + @include line(vertical); + } + + &:before, + &:after { + content: ''; + position: absolute; + } + &:before { + transform: translateY(-60%); + width: 0.4rem; + border-radius: 1rem; + border-color: var(--limel-chart-item-color, $default-item-color); + border-style: solid; + border-bottom-width: 1.75rem; + border-right-color: transparent; + border-left-color: transparent; + border-top-color: transparent; + } + &:after { + border-radius: 50%; + background-color: var( + --limel-chart-item-color, + $default-item-color + ); + aspect-ratio: 1; + height: clamp(0.75rem, 10%, 1.25rem); + + border: 0.125rem solid rgb(var(--contrast-100), 0.8); + box-shadow: var(--shadow-depth-8); + } + } +} diff --git a/src/components/chart/partial-styles/_pie-doughnut.scss b/src/components/chart/partial-styles/_pie-doughnut.scss new file mode 100644 index 0000000000..561487361b --- /dev/null +++ b/src/components/chart/partial-styles/_pie-doughnut.scss @@ -0,0 +1,48 @@ +:host(limel-chart[type='pie']), +:host(limel-chart[type='doughnut']) { + .item { + background: conic-gradient( + transparent 0 calc(var(--limel-chart-item-offset) * 1%), + var(--limel-chart-item-color, $default-item-color) + calc(var(--limel-chart-item-offset) * 1%) + calc( + var(--limel-chart-item-offset) * 1% + + var(--limel-chart-item-size) * 1% + ), + transparent + calc( + var(--limel-chart-item-offset) * 1% + + var(--limel-chart-item-size) * 1% + ) + ); + &:focus, + &:focus-visible { + outline: none; + } + } + + .item { + // A hack that disabled the tooltips. + // this is because items are absolutely positioned + // and stacked on top of each other. + // This makes means the tooltip of the top-most layer is + // the one that is shown. Layers below it are + // not hoverable, and I don't know how to fix that yer. + pointer-events: none; + } +} + +:host(limel-chart[type='doughnut']) { + .chart:after { + aspect-ratio: 1; + content: ''; + position: absolute; + inset: 0; + margin: auto; + + max-width: 60%; + max-height: 60%; + border-radius: 50%; + background-color: rgb(var(--contrast-100)); + } +} diff --git a/src/components/chart/partial-styles/_ring.scss b/src/components/chart/partial-styles/_ring.scss new file mode 100644 index 0000000000..929581261c --- /dev/null +++ b/src/components/chart/partial-styles/_ring.scss @@ -0,0 +1,72 @@ +:host(limel-chart[type='ring']) { + .chart { + &:after { + content: ''; + position: absolute; + inset: 0; + aspect-ratio: 1; + border-radius: 50%; + + max-height: calc( + 100% - var(--limel-chart-number-of-items) * + (100% / (var(--limel-chart-number-of-items) + 1)) + ); + max-width: calc( + 100% - var(--limel-chart-number-of-items) * + (100% / (var(--limel-chart-number-of-items) + 1)) + ); + + background-color: var( + --limel-chart-background-color, + rgb(var(--contrast-200)) + ); + } + + &:has(.item:hover), + &:has(.item:focus-visible) { + .item { + opacity: 1; + filter: grayscale(1); + } + } + } + + .chart:after, + .item { + margin: auto; + border: 1px solid + var(--limel-chart-background-color, rgb(var(--contrast-400))); + } + + .item { + @include mixins.visualize-keyboard-focus; + + background: conic-gradient( + var(--limel-chart-item-color, $default-item-color) 0 + calc( + var(--limel-chart-item-offset) * 1% + + var(--limel-chart-item-size) * 1% + ), + var(--chart-background-color, rgb(var(--contrast-200))) + calc( + var(--limel-chart-item-offset) * 1% + + var(--limel-chart-item-size) * 1% + ) + ); + + max-width: calc( + 100% - var(--limel-chart-item-index) * + (100% / (var(--limel-chart-number-of-items) + 1)) + ); + + max-height: calc( + 100% - var(--limel-chart-item-index) * + (100% / (var(--limel-chart-number-of-items) + 1)) + ); + + &:focus-visible, + &:hover { + filter: grayscale(0) !important; + } + } +} diff --git a/src/components/chart/partial-styles/_stacked-bar.scss b/src/components/chart/partial-styles/_stacked-bar.scss new file mode 100644 index 0000000000..1698019786 --- /dev/null +++ b/src/components/chart/partial-styles/_stacked-bar.scss @@ -0,0 +1,49 @@ +@use '../../../style/mixins'; + +:host(limel-chart[type='stacked-bar']) { + .chart { + display: flex; + border-radius: 0.25rem; + overflow: hidden; + background-color: var( + --chart-background-color, + rgb(var(--contrast-800), 0.2) + ); + } + + .item { + @include mixins.visualize-keyboard-focus; + display: flex; + border-radius: var(--chart-item-border-radius, 0); + background: var(--limel-chart-item-color, $default-item-color); + + &:last-of-type { + box-shadow: none !important; + } + } +} + +:host(limel-chart[type='stacked-bar'][orientation='landscape']) { + .chart { + flex-direction: row; + } + + .item { + min-height: 0.5rem; + width: calc(var(--limel-chart-item-size) * 1%); + box-shadow: -1px 0 0 0 + var(--chart-item-divider-color, rgb(var(--color-white), 0.6)) inset; + } +} + +:host(limel-chart[type='stacked-bar'][orientation='portrait']) { + .chart { + flex-direction: column-reverse; + } + + .item { + min-width: 0.5rem; + height: calc(var(--limel-chart-item-size) * 1%); + box-shadow: 0 -1px 0 0 rgb(var(--color-white), 0.6) inset; + } +} diff --git a/src/style/mixins.scss b/src/style/mixins.scss index ab76c1e7ee..1890f316cb 100644 --- a/src/style/mixins.scss +++ b/src/style/mixins.scss @@ -440,3 +440,19 @@ $clickable-normal-state-transitions: ( -webkit-box-orient: vertical; -webkit-line-clamp: $max-lines; } + +// Hide element visually while keeping it accessible to assistive technologies +// See: https://www.a11yproject.com/posts/2013-01-11-how-to-hide-content/ +// See: https://hugogiraudel.com/2016/10/13/css-hide-and-seek/ +@mixin visually-hidden { + position: absolute; + width: 0; + height: 0; + margin: -1px; + padding: 0; + border: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + clip-path: inset(50%); + white-space: nowrap; +} diff --git a/src/translations/da.ts b/src/translations/da.ts index f238ec9358..96900d6589 100644 --- a/src/translations/da.ts +++ b/src/translations/da.ts @@ -3,6 +3,7 @@ export default { save: 'Gem', cancel: 'Annullér', loading: 'Indlæser…', + value: 'Værdi', 'callout.note': 'Bemærk', 'callout.important': 'Vigtig', 'callout.tip': 'Tip', diff --git a/src/translations/de.ts b/src/translations/de.ts index 4dd8e6361b..ea5bbc89d7 100644 --- a/src/translations/de.ts +++ b/src/translations/de.ts @@ -3,6 +3,7 @@ export default { save: 'Speichern', cancel: 'Abbrechen', loading: 'Laden…', + value: 'Wert', 'callout.note': 'Hinweis', 'callout.important': 'Wichtig', 'callout.tip': 'Tipp', diff --git a/src/translations/en.ts b/src/translations/en.ts index a576efeeda..8549c0a2ce 100644 --- a/src/translations/en.ts +++ b/src/translations/en.ts @@ -3,6 +3,7 @@ export default { save: 'Save', cancel: 'Cancel', loading: 'Loading…', + value: 'Value', 'callout.note': 'Note', 'callout.important': 'Important', 'callout.tip': 'Tip', diff --git a/src/translations/fi.ts b/src/translations/fi.ts index 1cec9c61b8..a27e0b364d 100644 --- a/src/translations/fi.ts +++ b/src/translations/fi.ts @@ -3,6 +3,7 @@ export default { save: 'Tallenna', canceL: 'Peruuta', loading: 'Ladataan…', + value: 'Arvo', 'callout.note': 'Huomio', 'callout.important': 'Tärkeää', 'callout.tip': 'Vinkki', diff --git a/src/translations/fr.ts b/src/translations/fr.ts index 5c2218b6cb..efb03bb866 100644 --- a/src/translations/fr.ts +++ b/src/translations/fr.ts @@ -3,6 +3,7 @@ export default { save: 'Enregistrer', cancel: 'Annuler', loading: 'Chargement…', + value: 'Valeur', 'callout.note': 'Note', 'callout.important': 'Important', 'callout.tip': 'Conseil', diff --git a/src/translations/nl.ts b/src/translations/nl.ts index f9784ed63c..9c5cc8c67e 100644 --- a/src/translations/nl.ts +++ b/src/translations/nl.ts @@ -3,6 +3,7 @@ export default { save: 'Opslaan', cancel: 'Annuleren', loading: 'Laden…', + value: 'Waarde', 'callout.note': 'Opmerking', 'callout.important': 'Belangrijk', 'callout.tip': 'Tip', diff --git a/src/translations/no.ts b/src/translations/no.ts index 96180fbe5f..63f4f453ca 100644 --- a/src/translations/no.ts +++ b/src/translations/no.ts @@ -3,6 +3,7 @@ export default { save: 'Lagre', cancel: 'Avbryt', loading: 'Laster…', + value: 'Verdi', 'callout.note': 'Note', 'callout.important': 'Viktig', 'callout.tip': 'Tip', diff --git a/src/translations/sv.ts b/src/translations/sv.ts index 5f5536587b..6ec6bd349b 100644 --- a/src/translations/sv.ts +++ b/src/translations/sv.ts @@ -3,6 +3,7 @@ export default { save: 'Spara', cancel: 'Avbryt', loading: 'Laddar…', + value: 'Värde', 'callout.note': 'Obs', 'callout.important': 'Viktigt', 'callout.tip': 'Tips',