fixed some alignment

This commit is contained in:
CodingPhoenixx
2026-02-13 17:42:20 +01:00
parent e6198508e7
commit 3477e74a83
+155 -126
View File
@@ -2,16 +2,17 @@ import { LitElement, html, css, svg } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
export interface LineSeries { export interface LineSeries {
label: string; label: string;
color?: string; color?: string;
points: { x: number; y: number }[]; points: { x: number; y: number }[];
} }
@customElement('line-chart') @customElement('line-chart')
export class LineChart extends LitElement { export class LineChart extends LitElement {
static styles = css` static styles = css`
:host { :host {
display: inline-flex; display: block;
width: 100%;
} }
.chart-wrapper { .chart-wrapper {
@@ -23,6 +24,8 @@ export class LineChart extends LitElement {
flex-direction: column; flex-direction: column;
gap: 1.25rem; gap: 1.25rem;
transition: border-color 0.25s ease; transition: border-color 0.25s ease;
height: 100%;
box-sizing: border-box;
} }
.chart-wrapper:hover { .chart-wrapper:hover {
@@ -37,6 +40,7 @@ export class LineChart extends LitElement {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: baseline; align-items: baseline;
flex-shrink: 0;
} }
.title { .title {
@@ -52,10 +56,16 @@ export class LineChart extends LitElement {
margin: 0; margin: 0;
} }
.svg-container { .svg-container {
width: 100%; width: 100%;
overflow: visible; min-height: 0;
flex: 1 1 auto;
aspect-ratio: 500 / 300; aspect-ratio: 500 / 300;
overflow: visible;
}
:host([style*='height']) .svg-container {
aspect-ratio: unset;
} }
svg { svg {
@@ -64,6 +74,7 @@ export class LineChart extends LitElement {
overflow: visible; overflow: visible;
display: block; display: block;
} }
.grid-line { .grid-line {
stroke: var(--color-border); stroke: var(--color-border);
stroke-width: 0.5; stroke-width: 0.5;
@@ -97,6 +108,7 @@ export class LineChart extends LitElement {
display: flex; display: flex;
gap: 1rem; gap: 1rem;
flex-wrap: wrap; flex-wrap: wrap;
flex-shrink: 0;
} }
.legend-item { .legend-item {
@@ -119,154 +131,171 @@ export class LineChart extends LitElement {
} }
`; `;
@property() heading = ''; @property() heading = '';
@property() subtitle = ''; @property() subtitle = '';
@property() xLabel = 'X'; @property() xLabel = 'X';
@property() yLabel = 'Y'; @property() yLabel = 'Y';
@property({ type: Array }) series: LineSeries[] = []; @property({ type: Array }) series: LineSeries[] = [];
@property({ type: Boolean }) showArea = false; @property({ type: Boolean }) showArea = false;
private defaultColors = [ private defaultColors = [
'var(--color-accent)', 'var(--color-accent)',
'#30a46c', '#30a46c',
'#e79d13', '#e79d13',
'#e5484d', '#e5484d',
'#6e56cf', '#6e56cf',
'#0091ff', '#0091ff',
]; ];
private padding = { top: 20, right: 25, bottom: 40, left: 45 }; private padding = { top: 20, right: 25, bottom: 40, left: 45 };
private width = 500; private width = 500;
private height = 300; private height = 300;
private niceScale(min: number, max: number, ticks: number) { private niceScale(min: number, max: number, ticks: number) {
const range = max - min || 1; const range = max - min || 1;
const rough = range / ticks; const rough = range / ticks;
const mag = Math.pow(10, Math.floor(Math.log10(rough))); const mag = Math.pow(10, Math.floor(Math.log10(rough)));
const norm = rough / mag; const norm = rough / mag;
let step: number; let step: number;
if (norm <= 1.5) step = 1 * mag; if (norm <= 1.5) step = 1 * mag;
else if (norm <= 3) step = 2 * mag; else if (norm <= 3) step = 2 * mag;
else if (norm <= 7) step = 5 * mag; else if (norm <= 7) step = 5 * mag;
else step = 10 * mag; else step = 10 * mag;
const nMin = Math.floor(min / step) * step; const nMin = Math.floor(min / step) * step;
const nMax = Math.ceil(max / step) * step; const nMax = Math.ceil(max / step) * step;
return { min: nMin, max: nMax, step }; return { min: nMin, max: nMax, step };
} }
render() { render() {
const p = this.padding; const p = this.padding;
const w = this.width - p.left - p.right; const w = this.width - p.left - p.right;
const h = this.height - p.top - p.bottom; const h = this.height - p.top - p.bottom;
const allPts = this.series.flatMap((s) => s.points); const allPts = this.series.flatMap((s) => s.points);
const xs = allPts.map((d) => d.x); const xs = allPts.map((d) => d.x);
const ys = allPts.map((d) => d.y); const ys = allPts.map((d) => d.y);
const rawXMin = Math.min(...xs, 0); const rawXMin = Math.min(...xs, 0);
const rawXMax = Math.max(...xs, 1); const rawXMax = Math.max(...xs, 1);
const rawYMin = Math.min(...ys, 0); const rawYMin = Math.min(...ys, 0);
const rawYMax = Math.max(...ys, 1); const rawYMax = Math.max(...ys, 1);
const xScale = this.niceScale(rawXMin, rawXMax, 5); const xScale = this.niceScale(rawXMin, rawXMax, 5);
const yScale = this.niceScale(rawYMin, rawYMax, 5); const yScale = this.niceScale(rawYMin, rawYMax, 5);
const sx = (v: number) => const sx = (v: number) =>
p.left + ((v - xScale.min) / (xScale.max - xScale.min)) * w; p.left + ((v - xScale.min) / (xScale.max - xScale.min)) * w;
const sy = (v: number) => const sy = (v: number) =>
p.top + p.top + h - ((v - yScale.min) / (yScale.max - yScale.min)) * h;
h -
((v - yScale.min) / (yScale.max - yScale.min)) * h;
const gridLines = []; const gridLines = [];
for ( for (
let val = yScale.min; let val = yScale.min;
val <= yScale.max + yScale.step * 0.01; val <= yScale.max + yScale.step * 0.01;
val += yScale.step val += yScale.step
) { ) {
const y = sy(val); const y = sy(val);
gridLines.push(svg` gridLines.push(svg`
<line class="grid-line" x1="${p.left}" y1="${y}" x2="${this.width - p.right}" y2="${y}" /> <line class="grid-line" x1="${p.left}" y1="${y}" x2="${this.width - p.right}" y2="${y}" />
<text class="axis-label" x="${p.left - 8}" y="${y + 3}" text-anchor="end">${Math.round(val)}</text> <text class="axis-label" x="${p.left - 8}" y="${y + 3}" text-anchor="end">${Math.round(val)}</text>
`); `);
} }
for ( for (
let val = xScale.min; let val = xScale.min;
val <= xScale.max + xScale.step * 0.01; val <= xScale.max + xScale.step * 0.01;
val += xScale.step val += xScale.step
) { ) {
const x = sx(val); const x = sx(val);
gridLines.push(svg` gridLines.push(svg`
<text class="axis-label" x="${x}" y="${this.height - p.bottom + 18}" text-anchor="middle">${Math.round(val)}</text> <text class="axis-label" x="${x}" y="${this.height - p.bottom + 18}" text-anchor="middle">${Math.round(val)}</text>
`); `);
} }
const baseLine = sy(yScale.min); const baseLine = sy(yScale.min);
const lines = this.series.map((s, i) => { const lines = this.series.map((s, i) => {
const color = const color =
s.color || this.defaultColors[i % this.defaultColors.length]; s.color || this.defaultColors[i % this.defaultColors.length];
const sorted = [...s.points].sort((a, b) => a.x - b.x); const sorted = [...s.points].sort((a, b) => a.x - b.x);
if (!sorted.length) return svg``; if (!sorted.length) return svg``;
const d = sorted const d = sorted
.map((pt, j) => `${j === 0 ? 'M' : 'L'}${sx(pt.x)},${sy(pt.y)}`) .map(
.join(' '); (pt, j) =>
`${j === 0 ? 'M' : 'L'}${sx(pt.x)},${sy(pt.y)}`
)
.join(' ');
const areaD = const areaD =
`M${sx(sorted[0].x)},${baseLine} ` + `M${sx(sorted[0].x)},${baseLine} ` +
sorted.map((pt) => `L${sx(pt.x)},${sy(pt.y)}`).join(' ') + sorted.map((pt) => `L${sx(pt.x)},${sy(pt.y)}`).join(' ') +
` L${sx(sorted[sorted.length - 1].x)},${baseLine} Z`; ` L${sx(sorted[sorted.length - 1].x)},${baseLine} Z`;
return svg` return svg`
${this.showArea ? svg`<path class="area-path" d="${areaD}" fill="${color}" />` : null} ${this.showArea ? svg`<path class="area-path" d="${areaD}" fill="${color}" />` : null}
<path class="line-path" d="${d}" stroke="${color}" /> <path class="line-path" d="${d}" stroke="${color}" />
`; `;
}); });
return html` return html`
<div class="chart-wrapper"> <div class="chart-wrapper">
${this.heading || this.subtitle ${this.heading || this.subtitle
? html` ? html`
<div class="header"> <div class="header">
${this.heading ${this.heading
? html`<h3 class="title">${this.heading}</h3>` ? html`<h3 class="title">${this.heading}</h3>`
: null} : null}
${this.subtitle ${this.subtitle
? html`<p class="subtitle">${this.subtitle}</p>` ? html`<p class="subtitle">${this.subtitle}</p>`
: null} : null}
</div> </div>
` `
: null} : null}
<div class="svg-container"> <div class="svg-container">
<svg viewBox="0 0 ${this.width} ${this.height}"> <svg
${gridLines} ${lines} viewBox="0 0 ${this.width} ${this.height}"
<text class="axis-label" x="${this.width / 2}" y="${this.height - 4}" text-anchor="middle"> preserveAspectRatio="none"
${this.xLabel} >
</text> ${gridLines} ${lines}
<text class="axis-label" x="12" y="${this.height / 2}" text-anchor="middle" <text
transform="rotate(-90, 12, ${this.height / 2})"> class="axis-label"
${this.yLabel} x="${this.width / 2}"
</text> y="${this.height - 4}"
</svg> text-anchor="middle"
>
${this.xLabel}
</text>
<text
class="axis-label"
x="12"
y="${this.height / 2}"
text-anchor="middle"
transform="rotate(-90, 12, ${this.height / 2})"
>
${this.yLabel}
</text>
</svg>
</div> </div>
${this.series.length > 1 ${this.series.length > 1
? html` ? html`
<div class="legend"> <div class="legend">
${this.series.map( ${this.series.map(
(s, i) => html` (s, i) => html`
<div class="legend-item"> <div class="legend-item">
<span class="legend-line" style="background: ${s.color || <span
this.defaultColors[ class="legend-line"
i % this.defaultColors.length style="background: ${s.color ||
]}"></span> this.defaultColors[
<span class="legend-label">${s.label}</span> i % this.defaultColors.length
</div> ]}"
></span>
<span class="legend-label">${s.label}</span>
</div>
`
)}
</div>
` `
)} : null}
</div> </div>
`
: null}
</div>
`; `;
} }
} }