fixed some alignment
This commit is contained in:
@@ -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>
|
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user