Skip to content

Commit

Permalink
feat(line): Finalize TinyLineChart
Browse files Browse the repository at this point in the history
  • Loading branch information
ajitzero committed Feb 12, 2025
1 parent 0225fa5 commit 0f82f4d
Show file tree
Hide file tree
Showing 14 changed files with 197 additions and 38 deletions.
33 changes: 25 additions & 8 deletions apps/demo/src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterModule } from '@angular/router';
import { Line, LineChart } from 'ng-vz';
import { Line, LineChart, ResponsiveContainer } from 'ng-vz';
import { MOCK_DATA } from './mocks';

@Component({
selector: 'app-root',
imports: [RouterModule, LineChart, Line],
imports: [RouterModule, LineChart, Line, ResponsiveContainer],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<vz-line-chart class="m-1" [height]="250" [width]="600" [data]="data">
<ng-container vzTitle>Simple Line Chart 1</ng-container>
<ng-container vzDesc>A sample chart for demonstrating the usage of the ng-vz library.</ng-container>
<div class="p-1">
<h1>TinyLineChart</h1>
<vz-line-chart class="outline" [height]="250" [width]="600" [data]="data">
<ng-container vzTitle>Simple Line Chart 1</ng-container>
<ng-container vzDesc>A sample chart for demonstrating the usage of the ng-vz library.</ng-container>
<g vzLine dataKey="uv" stroke="blue" stroke-width="2"></g>
<g vzLine dataKey="pv" stroke="darkblue" stroke-width="3"></g>
</vz-line-chart>
<g vzLine dataKey="uv" stroke="blue" stroke-width="2"></g>
<g vzLine dataKey="pv" stroke="darkblue" stroke-width="3"></g>
</vz-line-chart>
<h1>Responsive Container: TinyLineChart</h1>
<div style="position: relative; width: 100%; height: 250px">
<vz-responsive-container class="outline">
<vz-line-chart [height]="250" [width]="600" [data]="data">
<ng-container vzTitle>Simple Line Chart 2</ng-container>
<ng-container vzDesc>A sample chart for demonstrating the usage of the ng-vz library.</ng-container>
<g vzLine dataKey="pv" stroke="darkblue" stroke-width="2"></g>
<g vzLine dataKey="uv" stroke="blue" stroke-width="3"></g>
</vz-line-chart>
</vz-responsive-container>
</div>
<a href="https://recharts.org/en-US/examples/TinyLineChart">TinyLineChart | Recharts</a>
</div>
<router-outlet></router-outlet>
`,
})
Expand Down
6 changes: 6 additions & 0 deletions apps/demo/src/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,9 @@
.m-1 {
margin: 1rem;
}
.p-1 {
padding: 1rem;
}
.outline {
outline: 1px solid #aaa;
}
2 changes: 2 additions & 0 deletions apps/ng-vz/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
export * from './lib/cartesian';
export * from './lib/charts';
export * from './lib/layout';
export * from './lib/types';
10 changes: 3 additions & 7 deletions apps/ng-vz/src/lib/cartesian/line.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ import { InnerBounds } from '../types';
></svg:path>
@let _points = points();
@if (_points.length) {
<svg:g class="recharts-layer recharts-line-dots">
<svg:g>
@for (point of _points; track $index) {
<circle
class="recharts-dot recharts-line-dot"
[attr.cx]="point.cx"
[attr.cy]="point.cy"
[attr.height]="height()"
Expand Down Expand Up @@ -57,7 +56,7 @@ export class Line {
private readonly base = computed(() => {
const { innerWidth, innerHeight } = this.innerBounds();
const data = this.data().map(item => item[this.dataKey()] as number);
const pointRadiusDelta = 10;
const pointRadiusDelta = 5;

const xScale = scaleLinear()
.domain([0, data.length - 1])
Expand Down Expand Up @@ -86,9 +85,6 @@ export class Line {
protected readonly points = computed(() => {
const { data, xScale, yScale } = this.base();

return data.map((d, i) => ({
cx: xScale(i),
cy: yScale(d),
}));
return data.map((d, i) => ({ cx: xScale(i), cy: yScale(d) }));
});
}
37 changes: 35 additions & 2 deletions apps/ng-vz/src/lib/charts/cartesian-chart.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,44 @@
import type { Signal } from '@angular/core';
import type { Signal, WritableSignal } from '@angular/core';
import type { Primitive } from 'd3-array';
import type { GapInput, InnerBounds } from '../types';

/**
* Base cartesian chart requirements
*/
export abstract class CartesianChart {
/**
* Data Source
*/
public readonly data!: Signal<Record<string, Primitive>[]>;

/**
* Initial height of the chart
*/
public readonly height!: Signal<number>;
/**
* Initial width of the chart
*/
public readonly width!: Signal<number>;
public readonly data!: Signal<Record<string, Primitive>[]>;
/**
* These are used to allow the chart to be resized dynamically.
*
* @internal
*/
public readonly _height!: WritableSignal<number>;
/**
* These are used to allow the chart to be resized dynamically.
*
* @internal
*/
public readonly _width!: WritableSignal<number>;
/**
* Optional. Gap between the chart and the edges of the container
*
* @default 10
*/
public readonly gap!: Signal<GapInput>;
/**
* @internal
*/
public readonly innerBounds!: Signal<InnerBounds>;
}
60 changes: 42 additions & 18 deletions apps/ng-vz/src/lib/charts/line-chart.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { ChangeDetectionStrategy, Component, computed, contentChildren, effect, input } from '@angular/core';
import {
ChangeDetectionStrategy,
Component,
computed,
contentChildren,
effect,
input,
linkedSignal,
} from '@angular/core';
import type { Primitive } from 'd3-array';
import { Line } from '../cartesian';
import { DEFAULT_GAPS } from '../constants';
Expand All @@ -8,7 +16,7 @@ import { CartesianChart } from './cartesian-chart';
@Component({
selector: 'vz-line-chart',
template: `
<svg [attr.viewBox]="viewBox()" [attr.width]="width()" [attr.height]="height()">
<svg [attr.viewBox]="viewBox()" [attr.width]="__width()" [attr.height]="__height()">
<svg:title><ng-content select="[vzTitle]"></ng-content></svg:title>
<desc><ng-content select="[vzDesc]"></ng-content></desc>
Expand All @@ -23,6 +31,7 @@ import { CartesianChart } from './cartesian-chart';
})
export class LineChart implements CartesianChart {
public readonly data = input.required<Record<string, Primitive>[]>();

public readonly height = input.required<number>();
public readonly width = input.required<number>();
public readonly gap = input<GapInput, Partial<GapInput> | number>(DEFAULT_GAPS, {
Expand All @@ -34,36 +43,51 @@ export class LineChart implements CartesianChart {
},
});

public readonly _height = linkedSignal(() => this.height());
public readonly _width = linkedSignal(() => this.width());

public readonly __height = computed(() => {
const height = this._height();
const gap = this.gap();

return height - gap.top - gap.bottom;
});
public readonly __width = computed(() => {
const width = this._width();
const gap = this.gap();

return width - gap.left - gap.right;
});

private lines = contentChildren(Line);

protected readonly hostStyle = computed(() => {
const height = this.height();
const width = this.width();
const height = this._height();
const width = this._width();
const gap = this.gap();

const padding = [gap.top, gap.right, gap.bottom, gap.left].map(value => `${value}px`).join(' ');

return {
display: 'block',
boxSizing: 'content-box',
padding,
padding: [gap.top, gap.right, gap.bottom, gap.left].map(value => `${value}px`).join(' '),
width: '100%',
height: '100%',
maxHeight: `${height}px`,
maxWidth: `${width}px`,
outline: '1px solid #aaa',
};
});

protected readonly viewBox = computed(() => `0 0 ${this.width()} ${this.height()}`);
protected readonly viewBox = computed(() => {
const height = this.__height();
const width = this.__width();

return `0 0 ${width} ${height}`;
});

public readonly innerBounds = computed(() => {
const width = this.width();
const height = this.height();
const width = this.__width();
const height = this.__height();
const gap = this.gap();
// const lines = this.lines();

// const pointRadius = Math.max(...lines.map(line => +line.strokeWidth()));
const innerWidth = Math.max(0, width - gap.left - gap.right);
const innerHeight = Math.max(0, height - gap.top - gap.bottom);

Expand All @@ -78,10 +102,10 @@ export class LineChart implements CartesianChart {
}

for (const line of lines) {
line.width.set(this.width());
line.height.set(this.height());
line.innerBounds.set(this.innerBounds());
line.data.set(this.data());
line?.width?.set(this.__width());
line?.height?.set(this.__height());
line?.innerBounds?.set(this.innerBounds());
line?.data?.set(this.data());
}
});
}
Expand Down
2 changes: 1 addition & 1 deletion apps/ng-vz/src/lib/constants/defaults.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { GapInput } from '../types';

export const DEFAULT_GAPS: GapInput = { top: 10, right: 10, bottom: 10, left: 10 };
export const DEFAULT_GAPS: GapInput = { top: 12, right: 12, bottom: 12, left: 12 };
1 change: 1 addition & 0 deletions apps/ng-vz/src/lib/layout/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './responsive-container.component';
71 changes: 71 additions & 0 deletions apps/ng-vz/src/lib/layout/responsive-container.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { DOCUMENT } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
computed,
contentChild,
effect,
ElementRef,
inject,
input,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { debounceTime, fromEvent } from 'rxjs';
import { LineChart } from '../charts';
import { CssNumberValue } from '../types/css';

@Component({
selector: 'vz-responsive-container',
template: `
<ng-content></ng-content>
`,
host: {
'[style]': 'hostStyle()',
},
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ResponsiveContainer {
private readonly element = inject(ElementRef);
private readonly document = inject(DOCUMENT);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion, @typescript-eslint/no-non-null-asserted-optional-chain
private readonly window = this.document?.defaultView!;

private readonly chart = contentChild(LineChart);

public readonly height = input<CssNumberValue>('auto');
public readonly width = input<CssNumberValue>('auto');
public readonly debounceTime = input(0);

private readonly resizingEvent = toSignal(fromEvent(this.window, 'resize').pipe(debounceTime(this.debounceTime())));

hostStyle = computed(() => {
const height = this.height();
const width = this.width();

return {
display: 'inline-block',
position: 'absolute',
boxSizing: 'content-box !important', // TODO: Remove this once we have a better way to handle this
height,
width,
};
});

constructor() {
effect(() => {
const chart = this.chart();
const _height = this.height();
const _width = this.width();
const _resizingEvent = this.resizingEvent();

if (chart) {
const element = this.element.nativeElement.parentElement;
const style = this.window?.getComputedStyle(element);
if (!style) return;

chart._height.set(element.clientHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom));
chart._width.set(element.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight));
}
});
}
}
2 changes: 2 additions & 0 deletions apps/ng-vz/src/lib/types/chart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export type GapInput = Record<'top' | 'right' | 'bottom' | 'left', number>;
export type InnerBounds = { innerWidth: number; innerHeight: number };
2 changes: 2 additions & 0 deletions apps/ng-vz/src/lib/types/css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
type CssNumberUnit = 'px' | 'em' | 'rem' | 'ex' | 'ch' | 'vw' | 'vh' | 'vmin' | 'vmax' | '%';
export type CssNumberValue = `${number}${CssNumberUnit}` | 'auto';
4 changes: 2 additions & 2 deletions apps/ng-vz/src/lib/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type GapInput = Record<'top' | 'right' | 'bottom' | 'left', number>;
export type InnerBounds = { innerWidth: number; innerHeight: number };
export * from './chart';
export * from './css';
1 change: 1 addition & 0 deletions apps/ng-vz/src/lib/utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './strip-css-units.util';
4 changes: 4 additions & 0 deletions apps/ng-vz/src/lib/utils/strip-css-units.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const stripCssUnits = (cssValue: string) => {
const match = cssValue.match(/^(-?\d*\.?\d+)([a-z%]*)$/i);
return match ? parseFloat(match[1]) : NaN;
};

0 comments on commit 0f82f4d

Please sign in to comment.