Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/components/chart/chart.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ $default-item-color: var(--chart-item-color, rgb(var(--contrast-1100), 0.8));
isolation: isolate;

display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
height: 100%;
min-width: 0;
Expand Down
167 changes: 121 additions & 46 deletions src/components/chart/chart.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
import { Component, Event, EventEmitter, h, Prop, Watch } from '@stencil/core';
import {
Component,
Event,
EventEmitter,
h,
Host,
Prop,
State,
Watch,
} from '@stencil/core';
import { Languages } from '../date-picker/date.types';
import translate from '../../global/translations';
import { createRandomString } from '../../util/random-string';
Expand Down Expand Up @@ -117,6 +126,9 @@ export class Chart {
totalRange: number;
};

@State()
private hiddenItems: Set<string> = new Set();

/**
* Fired when a chart item with `clickable` set to `true` is clicked
*/
Expand All @@ -133,19 +145,26 @@ export class Chart {
}

return (
<table
aria-busy={this.loading ? 'true' : 'false'}
aria-live="polite"
style={{
'--limel-chart-number-of-items':
this.items.length.toString(),
}}
<Host
// class={{
// 'has-orientation-portrait': this.orientation === 'portrait',
// }}
>
{this.renderCaption()}
{this.renderTableHeader()}
{this.renderAxises()}
<tbody class="chart">{this.renderItems()}</tbody>
</table>
<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()}
<tbody class="chart">{this.renderItems()}</tbody>
</table>
{this.renderLegend(this.items)}
</Host>
);
}

Expand Down Expand Up @@ -212,35 +231,78 @@ export class Chart {

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 (
<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}
>
<th>{this.getItemText(item)}</th>
<td>{this.getFormattedValue(item)}</td>
{this.renderTooltip(item, itemId, size)}
</tr>
);
});
return this.items
.filter((item) => !this.hiddenItems.has(item.text))
.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 (
<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}
>
<th>{this.getItemText(item)}</th>
<td>{this.getFormattedValue(item)}</td>
{this.renderTooltip(item, itemId, size)}
</tr>
);
});
}

private renderLegend(items: ChartItem[]) {
// Only render legend if at least one item has a color
const hasColoredItems = items.some((item) => item.color);

// Check if all colors are the same (no need for legend if they are)
const allColorsSame =
items.length > 1 &&
items.every((item) => item.color === items[0].color);

if (
!hasColoredItems ||
this.orientation === 'portrait' ||
allColorsSame
) {
return;
}

return (
<limel-legend
items={items}
hiddenItems={this.hiddenItems}
getItemText={this.getItemText}
onLegendClick={(event: CustomEvent<ChartItem>) =>
this.handleLegendClick(event.detail)
}
/>
);
}

private handleLegendClick(item: ChartItem) {
const updatedHiddenItems = new Set(this.hiddenItems);
if (updatedHiddenItems.has(item.text)) {
updatedHiddenItems.delete(item.text);
} else {
updatedHiddenItems.add(item.text);
}
this.hiddenItems = updatedHiddenItems;
this.range = null;
this.recalculateRangeData();
}

private getItemStyle(
Expand Down Expand Up @@ -352,9 +414,21 @@ export class Chart {
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(
// Use only visible items for range calculation
const visibleItems = this.items.filter(
(item) => !this.hiddenItems.has(item.text)
);
const itemsForCalculation =
visibleItems.length > 0 ? visibleItems : this.items;

const minRange = Math.min(
0,
...itemsForCalculation.map(this.getMinimumValue)
);
const maxRange = Math.max(
...itemsForCalculation.map(this.getMaximumValue)
);
const totalSum = itemsForCalculation.reduce(
(sum, item) => sum + this.getMaximumValue(item),
0
);
Expand All @@ -368,7 +442,8 @@ export class Chart {
}

if (!this.axisIncrement) {
this.axisIncrement = this.calculateAxisIncrement(this.items);
this.axisIncrement =
this.calculateAxisIncrement(itemsForCalculation);
}

const visualMaxValue =
Expand Down
2 changes: 1 addition & 1 deletion src/components/chart/examples/chart-type-nps.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import { chartItems } from './chart-items-nps';
export class ChartTypeNpsExample {
public render() {
return (
<Host class="large">
<Host class="large" style={{ flexDirection: 'row' }}>
<h4>
Our Net Promoter Score Development During the Past 5
Quarters
Expand Down
71 changes: 71 additions & 0 deletions src/components/chart/legend.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
@use '../../style/mixins';

.legend {
display: flex;
align-self: center;
max-width: 80%;
font-size: small;

background-color: rgb(var(--contrast-500));
border-radius: 0.5rem;
padding: 0.5rem;

.legend-content {
display: flex;
width: 100%;
overflow-x: auto;
gap: 1rem;

@include mixins.fade-out-overflowed-content-on-edges(horizontally);
}
}

.legend-item {
display: flex;
align-items: center;
gap: 0.25rem;
cursor: pointer;
user-select: none;
white-space: nowrap; // Prevent text wrapping
flex-shrink: 0; // Prevent items from shrinking

&--hidden {
opacity: 0.5;
}

limel-badge {
transition:
opacity 0.2s,
filter 0.2s;
}

&:hover limel-badge {
filter: brightness(1.2);
}
}

// // Change to column layout when chart has portrait orientation
// :host(.has-orientation-portrait) & {
// flex-direction: column;
// overflow-x: visible;
// overflow-y: auto;
// height: 100%;
// max-height: 200px; // Limit height for vertical scrolling

// // Remove horizontal fade and add vertical fade
// -webkit-mask-image: none;
// mask-image: none;
// padding-left: 0;
// padding-right: 0;

// @include mixins.fade-out-overflowed-content-on-edges(vertically);
// }
// }

// // Change to row layout when chart has portrait orientation
// :host(.has-orientation-portrait) & {
// flex-direction: row;
// max-width: 100%;
// max-height: 250px; // Limit height in portrait mode
// align-self: stretch; // Take full width in portrait
// }
66 changes: 66 additions & 0 deletions src/components/chart/legend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { Component, h, Prop, Event, EventEmitter } from '@stencil/core';
import type { ChartItem } from './chart.types';

@Component({
tag: 'limel-legend',
shadow: true,
styleUrl: 'legend.scss',
})
export class Legend {
@Prop() items: ChartItem[];
@Prop() hiddenItems: Set<string>;
@Prop() getItemText: (item: ChartItem) => string = (item) => item.text;

@Event() legendClick: EventEmitter<ChartItem>;

private handleLegendClick = (item: ChartItem) => {

Check warning on line 16 in src/components/chart/legend.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'handleLegendClick' is never reassigned; mark it as `readonly`.

See more on https://sonarcloud.io/project/issues?id=Lundalogik_lime-elements&issues=AZrAQMjNrAnsyRD75eO2&open=AZrAQMjNrAnsyRD75eO2&pullRequest=3724
this.legendClick.emit(item);
};

render() {
return (
<div class="legend">
<div class="legend-content">
{this.items.map((item) => {
const isHidden = this.hiddenItems?.has(item.text);
return (
<div
class={{
'legend-item': true,
'legend-item--hidden': isHidden,
}}
key={item.text}
onClick={() => this.handleLegendClick(item)}
>

Check warning on line 34 in src/components/chart/legend.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Avoid non-native interactive elements. If using native HTML is not possible, add an appropriate role and support for tabbing, mouse, keyboard, and touch inputs to an interactive content element.

See more on https://sonarcloud.io/project/issues?id=Lundalogik_lime-elements&issues=AZrAQMjNrAnsyRD75eO3&open=AZrAQMjNrAnsyRD75eO3&pullRequest=3724

Check warning on line 34 in src/components/chart/legend.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Visible, non-interactive elements with click handlers must have at least one keyboard listener.

See more on https://sonarcloud.io/project/issues?id=Lundalogik_lime-elements&issues=AZrAQMjNrAnsyRD75eO4&open=AZrAQMjNrAnsyRD75eO4&pullRequest=3724
<limel-badge
style={{
'--badge-background-color': item.color,
cursor: 'pointer',
opacity: isHidden ? '0.6' : '1',
}}
/>
<span
style={{
opacity: isHidden ? '0.6' : '1',
marginRight: '0.5em',
}}
>
{this.getItemText(item)}
</span>
<span
style={{
opacity: isHidden ? '0.6' : '1',
}}
>
{Array.isArray(item.value)
? `${item.value[0]} — ${item.value[1]}`
: item.value}
</span>
</div>
);
})}
</div>
</div>
);
}
}
Loading