UNPKG

@tanstack/charts

Version:

A chart grammar for TypeScript and JavaScript. Marks consume your data directly, channels describe visual encodings, and the engine compiles them into a renderer-neutral keyed scene. TanStack's compact scales cover common numeric and categorical mappings.

351 lines (346 loc) 14.9 kB
import * as i0 from '@angular/core'; import { inject, TemplateRef, Input, Directive, APP_ID, Injectable, afterNextRender, ViewContainerRef, ContentChild, ViewChild, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; import { resolveChartAdapterLayout } from '@tanstack/charts/adapter'; import { createChartRendererAdapter } from '@tanstack/charts/adapter/renderer'; import { renderChartSvg } from '@tanstack/charts/svg'; import { createSvgChartRenderer } from '@tanstack/charts/svg/renderer'; class ChartTooltipBodyDirective { constructor() { this.templateRef = inject(TemplateRef); } static ngTemplateContextGuard(_directive, context) { return true; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ChartTooltipBodyDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.8", type: ChartTooltipBodyDirective, isStandalone: true, selector: "ng-template[tanstackChartTooltipBody]", inputs: { definition: ["tanstackChartTooltipBody", "definition"] }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ChartTooltipBodyDirective, decorators: [{ type: Directive, args: [{ selector: 'ng-template[tanstackChartTooltipBody]', standalone: true, }] }], propDecorators: { definition: [{ type: Input, args: [{ required: true, alias: 'tanstackChartTooltipBody' }] }] } }); class ChartIdGenerator { constructor() { this.appId = inject(APP_ID); this.nextId = 0; } next() { return `ts-chart-${this.appId}-${++this.nextId}`.replaceAll(/[^a-zA-Z0-9_-]/g, ''); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ChartIdGenerator, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ChartIdGenerator, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: ChartIdGenerator, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class Chart { set tooltipBodyDirective(directive) { if (directive === this.tooltipBody) return; this.destroyTooltipBodyView(); this.tooltipBody = directive; this.updateAdapter(); if (directive && this.tooltipBodyTarget) { this.renderTooltipBody(this.tooltipBodyTarget); } } constructor() { this.initialMarkup = ''; this.hostClass = 'ts-chart-host'; this.hostStyle = 'position:relative;width:100%;height:320px'; this.sanitizer = inject(DomSanitizer); this.generatedId = inject(ChartIdGenerator).next(); this.handleTooltipBodyChange = (target) => { if (!target) { this.tooltipBodyTarget = undefined; this.destroyTooltipBodyView(); return; } this.tooltipBodyTarget = target; this.renderTooltipBody(target); }; afterNextRender(() => { this.adapter?.mount(this.surface.nativeElement); }); } ngOnChanges(_changes) { if (!this.options) return; const layout = resolveChartAdapterLayout(this.options); this.hostClass = this.options.class ? `ts-chart-host ${this.options.class}` : 'ts-chart-host'; this.hostStyle = [ 'position:relative', `width:${this.options.width === undefined ? '100%' : `${this.options.width}px`}`, this.options.height !== undefined ? `height:${this.options.height}px` : layout.aspectRatio === undefined ? 'height:320px' : `aspect-ratio:${layout.aspectRatio}`, this.options.style, ] .filter(Boolean) .join(';'); this.updateAdapter(); } ngOnDestroy() { this.adapter?.destroy(); this.destroyTooltipBodyView(); } updateAdapter() { if (!this.options) return; const hostOptions = toHostOptions(this.options, this.options.idPrefix ?? this.generatedId, this.resolveRenderer(this.options.renderSvg ?? renderChartSvg), this.tooltipBody ? this.handleTooltipBodyChange : undefined); if (!this.adapter) { this.adapter = createChartRendererAdapter(hostOptions); this.initialMarkup = this.sanitizer.bypassSecurityTrustHtml(this.adapter.prerender()); } else { this.adapter.update(hostOptions); } } resolveRenderer(renderSvg) { if (!this.renderer || renderSvg !== this.activeRenderSvg) { this.activeRenderSvg = renderSvg; this.renderer = createSvgChartRenderer(renderSvg); } return this.renderer; } renderTooltipBody(target) { const directive = this.tooltipBody; if (!directive) return; this.defaultTooltipText = typeof target.content === 'string' ? target.content : undefined; this.defaultTooltipContent = typeof target.content === 'string' ? undefined : target.content; const context = this.tooltipBodyView?.context; if (context) { context.points = target.points; context.content = target.content; context.defaultBody = this.defaultTooltipBody; context.pinned = target.pinned; context.dismiss = target.dismiss; this.moveTooltipBodyView(target.element); this.tooltipBodyView?.detectChanges(); return; } const nextContext = { points: target.points, content: target.content, defaultBody: this.defaultTooltipBody, pinned: target.pinned, dismiss: target.dismiss, }; nextContext.$implicit = nextContext; this.tooltipBodyView = this.tooltipOutlet.createEmbeddedView(directive.templateRef, nextContext); this.moveTooltipBodyView(target.element); this.tooltipBodyView.detectChanges(); } moveTooltipBodyView(target) { for (const node of this.tooltipBodyView?.rootNodes ?? []) { target.append(node); } } destroyTooltipBodyView() { const view = this.tooltipBodyView; if (!view) return; const nodes = [...view.rootNodes]; const index = this.tooltipOutlet.indexOf(view); if (index === -1) view.destroy(); else this.tooltipOutlet.remove(index); for (const node of nodes) node.parentNode?.removeChild(node); this.tooltipBodyView = undefined; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: Chart, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.8", type: Chart, isStandalone: true, selector: "tanstack-chart", inputs: { options: "options" }, queries: [{ propertyName: "tooltipBodyDirective", first: true, predicate: ChartTooltipBodyDirective, descendants: true }], viewQueries: [{ propertyName: "surface", first: true, predicate: ["surface"], descendants: true, static: true }, { propertyName: "tooltipOutlet", first: true, predicate: ["tooltipOutlet"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "defaultTooltipBody", first: true, predicate: ["defaultTooltipBody"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: ` <div [class]="hostClass" [style]="hostStyle"> <div #surface class="ts-chart-surface" style="width: 100%; height: 100%" [innerHTML]="initialMarkup" ></div> </div> <ng-container #tooltipOutlet></ng-container> <ng-content select="ng-template[tanstackChartTooltipBody]" /> <ng-template #defaultTooltipBody> @if (defaultTooltipText !== undefined) { {{ defaultTooltipText }} } @else if (defaultTooltipContent; as content) { @if (content.title) { <div class="ts-chart-tooltip__title" style="display:flex;align-items:center;gap:0.4rem;font-weight:650" [style.margin-bottom]="content.rows.length ? '0.3rem' : '0'" > @if (content.color) { <span class="ts-chart-tooltip__swatch" aria-hidden="true" style="display:block;width:0.55rem;height:0.55rem;border-radius:0.15rem;box-shadow:inset 0 0 0 1px rgb(0 0 0/.12)" [style.background]="content.color" ></span> } {{ content.title }} </div> } @if (content.rows.length) { <div class="ts-chart-tooltip__rows" aria-hidden="true"> @for (row of content.rows; track $index) { <div class="ts-chart-tooltip__row" style="display:grid;grid-template-columns:0.55rem minmax(0,1fr) auto;align-items:center;column-gap:0.4rem" > @if (row.color) { <span class="ts-chart-tooltip__swatch" aria-hidden="true" style="display:block;width:0.55rem;height:0.55rem;border-radius:0.15rem;box-shadow:inset 0 0 0 1px rgb(0 0 0/.12)" [style.background]="row.color" ></span> } @else { <span></span> } <span>{{ row.label }}</span> <span style="text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap" >{{ row.value }}</span > </div> } </div> } } </ng-template> `, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: Chart, decorators: [{ type: Component, args: [{ selector: 'tanstack-chart', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: ` <div [class]="hostClass" [style]="hostStyle"> <div #surface class="ts-chart-surface" style="width: 100%; height: 100%" [innerHTML]="initialMarkup" ></div> </div> <ng-container #tooltipOutlet></ng-container> <ng-content select="ng-template[tanstackChartTooltipBody]" /> <ng-template #defaultTooltipBody> @if (defaultTooltipText !== undefined) { {{ defaultTooltipText }} } @else if (defaultTooltipContent; as content) { @if (content.title) { <div class="ts-chart-tooltip__title" style="display:flex;align-items:center;gap:0.4rem;font-weight:650" [style.margin-bottom]="content.rows.length ? '0.3rem' : '0'" > @if (content.color) { <span class="ts-chart-tooltip__swatch" aria-hidden="true" style="display:block;width:0.55rem;height:0.55rem;border-radius:0.15rem;box-shadow:inset 0 0 0 1px rgb(0 0 0/.12)" [style.background]="content.color" ></span> } {{ content.title }} </div> } @if (content.rows.length) { <div class="ts-chart-tooltip__rows" aria-hidden="true"> @for (row of content.rows; track $index) { <div class="ts-chart-tooltip__row" style="display:grid;grid-template-columns:0.55rem minmax(0,1fr) auto;align-items:center;column-gap:0.4rem" > @if (row.color) { <span class="ts-chart-tooltip__swatch" aria-hidden="true" style="display:block;width:0.55rem;height:0.55rem;border-radius:0.15rem;box-shadow:inset 0 0 0 1px rgb(0 0 0/.12)" [style.background]="row.color" ></span> } @else { <span></span> } <span>{{ row.label }}</span> <span style="text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap" >{{ row.value }}</span > </div> } </div> } } </ng-template> `, }] }], ctorParameters: () => [], propDecorators: { options: [{ type: Input, args: [{ required: true }] }], surface: [{ type: ViewChild, args: ['surface', { static: true }] }], tooltipOutlet: [{ type: ViewChild, args: ['tooltipOutlet', { read: ViewContainerRef, static: true }] }], defaultTooltipBody: [{ type: ViewChild, args: ['defaultTooltipBody', { static: true }] }], tooltipBodyDirective: [{ type: ContentChild, args: [ChartTooltipBodyDirective] }] } }); function toHostOptions(options, idPrefix, renderer, onTooltipBodyChange) { const { class: _class, style: _style, renderSvg: _renderSvg, onRender, ...hostOptions } = options; return { ...hostOptions, idPrefix, renderer, onRender: adaptOnRender(onRender), onTooltipBodyChange, }; } function adaptOnRender(onRender) { if (!onRender) return undefined; return (context) => { const svg = context.surface.element; const SvgElement = context.container.ownerDocument.defaultView?.SVGSVGElement; if (!SvgElement || !(svg instanceof SvgElement)) { throw new TypeError('Expected the SVG chart surface.'); } onRender({ container: context.container, scene: context.scene, svg, interaction: context.interaction, }); }; } /** * Generated bundle index. Do not edit. */ export { Chart, ChartTooltipBodyDirective }; //# sourceMappingURL=tanstack-angular-charts.mjs.map