@firestitch/report 18.8.0 → 18.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/reports/export/report-export-collector.service.d.ts +2 -0
- package/app/reports/format.d.ts +3 -1
- package/app/reports/interfaces/report.interface.d.ts +1 -1
- package/app/reports/theme/palette.d.ts +2 -0
- package/esm2022/app/reports/export/report-export-collector.service.mjs +24 -2
- package/esm2022/app/reports/export/report-pdf.service.mjs +37 -33
- package/esm2022/app/reports/export/report-pptx.service.mjs +42 -32
- package/esm2022/app/reports/format.mjs +30 -2
- package/esm2022/app/reports/interfaces/report.interface.mjs +1 -1
- package/esm2022/app/reports/kpi-card.mjs +19 -6
- package/esm2022/app/reports/option-builder.mjs +18 -5
- package/esm2022/app/reports/output-notes.mjs +57 -11
- package/esm2022/app/reports/theme/palette.mjs +7 -2
- package/fesm2022/{firestitch-report-echarts-qCBE3LqM.mjs → firestitch-report-echarts-CFnbJx44.mjs} +2 -2
- package/fesm2022/{firestitch-report-echarts-qCBE3LqM.mjs.map → firestitch-report-echarts-CFnbJx44.mjs.map} +1 -1
- package/fesm2022/{firestitch-report-firestitch-report-DbsWbE-j.mjs → firestitch-report-firestitch-report-CSylkyko.mjs} +226 -85
- package/fesm2022/firestitch-report-firestitch-report-CSylkyko.mjs.map +1 -0
- package/fesm2022/firestitch-report.mjs +1 -1
- package/package.json +1 -1
- package/fesm2022/firestitch-report-firestitch-report-DbsWbE-j.mjs.map +0 -1
|
@@ -32,7 +32,7 @@ import { FsApi, StreamEventType } from '@firestitch/api';
|
|
|
32
32
|
import { MatCheckbox } from '@angular/material/checkbox';
|
|
33
33
|
import * as i3 from '@firestitch/dialog';
|
|
34
34
|
import { FsDialogModule } from '@firestitch/dialog';
|
|
35
|
-
import { parseISO, isValid, format, startOfYear, startOfQuarter, startOfMonth, subMonths, subDays, subWeeks, startOfWeek, endOfWeek,
|
|
35
|
+
import { parseISO, isValid, addDays, format, startOfYear, startOfQuarter, startOfMonth, subMonths, subDays, subWeeks, startOfWeek, endOfWeek, min, max, startOfDay, isLastDayOfMonth, endOfMonth, addMonths, addYears, addQuarters, addWeeks, formatISO } from 'date-fns';
|
|
36
36
|
import { formatInTimeZone, toZonedTime } from 'date-fns-tz';
|
|
37
37
|
import { NgTemplateOutlet } from '@angular/common';
|
|
38
38
|
import * as i1 from '@firestitch/list';
|
|
@@ -1338,6 +1338,34 @@ function formatDayWindow(meta, timezone, locale) {
|
|
|
1338
1338
|
const time = formatThroughTime(meta.through, timezone, locale);
|
|
1339
1339
|
return days && time ? `${days}, until ${time}` : days;
|
|
1340
1340
|
}
|
|
1341
|
+
// Whether a comparison is read on one day: a comparison of values read at
|
|
1342
|
+
// the end of each period, with no time axis of its own (a KPI, a one-row
|
|
1343
|
+
// list), whose value is read today and whose prior is read at the end of
|
|
1344
|
+
// the period before. Its two windows are those periods, back to back, and
|
|
1345
|
+
// each value is read on its window's last day, so the windows are written as
|
|
1346
|
+
// those days (formatReadingDay). A spine of several periods sets each period
|
|
1347
|
+
// against an earlier one, and its windows are spans like any other
|
|
1348
|
+
// comparison's.
|
|
1349
|
+
function isReadOnOneDay(comparison) {
|
|
1350
|
+
const current = comparison?.current;
|
|
1351
|
+
const prior = comparison?.priors?.[0];
|
|
1352
|
+
if (comparison?.mode !== 'period_end' || !current || !prior) {
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
const priorEnd = parseDay(prior.end);
|
|
1356
|
+
const currentStart = parseDay(current.start);
|
|
1357
|
+
return !!priorEnd && !!currentStart && addDays(priorEnd, 1).getTime() === currentStart.getTime();
|
|
1358
|
+
}
|
|
1359
|
+
// The day a value is read on: 'on Sep 15, 2026' ('On …' to open a line), as
|
|
1360
|
+
// a chart's tooltip names a prior point read at the end of its period. ''
|
|
1361
|
+
// for a day that is not a real one.
|
|
1362
|
+
function formatReadingDay(window, locale, lead = false) {
|
|
1363
|
+
const day = formatDay(window.end, locale);
|
|
1364
|
+
if (!day) {
|
|
1365
|
+
return '';
|
|
1366
|
+
}
|
|
1367
|
+
return lead ? `On ${day}` : `on ${day}`;
|
|
1368
|
+
}
|
|
1341
1369
|
// One 'yyyy-MM-dd' day in the locale, written with no zone: 'Sep 1, 2026'.
|
|
1342
1370
|
function formatDay(day, locale) {
|
|
1343
1371
|
const date = parseDay(day);
|
|
@@ -1695,23 +1723,36 @@ function deltaTone(trend, increaseIs) {
|
|
|
1695
1723
|
return rise === (polarity === ReportChangePolarity.Good) ? ReportChangePolarity.Good : ReportChangePolarity.Bad;
|
|
1696
1724
|
}
|
|
1697
1725
|
// 'vs $1,139 (Aug 1 – 31, 2026)': the prior value and its window, either
|
|
1698
|
-
// alone when the other is missing.
|
|
1726
|
+
// alone when the other is missing. A value read on one day is compared with
|
|
1727
|
+
// the value on the prior window's last day: 'vs 20 (on Aug 31, 2026)', or
|
|
1728
|
+
// 'vs Aug 31, 2026' with no prior value.
|
|
1699
1729
|
function comparedText(config, data, context) {
|
|
1700
1730
|
const comparison = data.comparison;
|
|
1701
1731
|
const prior = comparison.priors?.[0];
|
|
1702
|
-
const days = prior ? formatDayWindow(prior, comparison.timezone, context.format.locale) : '';
|
|
1703
1732
|
const value = data.kpi.prior === null ? '' : formatNumber(data.kpi.prior, config?.measure, context.format);
|
|
1733
|
+
const days = prior ? priorWindowText$1(comparison, prior, context.format.locale, !!value) : '';
|
|
1704
1734
|
if (value && days) {
|
|
1705
1735
|
return `vs ${value} (${days})`;
|
|
1706
1736
|
}
|
|
1707
1737
|
return value || days ? `vs ${value || days}` : '';
|
|
1708
1738
|
}
|
|
1739
|
+
// The prior window in the compared text: its days, or the day its value is
|
|
1740
|
+
// read on ('on Aug 31, 2026' beside the value, the bare day without one).
|
|
1741
|
+
function priorWindowText$1(comparison, prior, locale, withValue) {
|
|
1742
|
+
if (!isReadOnOneDay(comparison)) {
|
|
1743
|
+
return formatDayWindow(prior, comparison.timezone, locale);
|
|
1744
|
+
}
|
|
1745
|
+
return withValue ? formatReadingDay(prior, locale) : formatDayRange(prior.end, prior.end, locale);
|
|
1746
|
+
}
|
|
1709
1747
|
// The days the value covers: the comparison's current window, else the
|
|
1710
|
-
// timeframe's.
|
|
1748
|
+
// timeframe's. A value read on one day names that day ('On Sep 15, 2026').
|
|
1749
|
+
// '' when the data has neither.
|
|
1711
1750
|
function windowLine(data, context) {
|
|
1712
1751
|
const comparison = data?.comparison;
|
|
1713
1752
|
if (comparison?.current) {
|
|
1714
|
-
return
|
|
1753
|
+
return isReadOnOneDay(comparison)
|
|
1754
|
+
? formatReadingDay(comparison.current, context.format.locale, true)
|
|
1755
|
+
: formatDayWindow(comparison.current, comparison.timezone, context.format.locale);
|
|
1715
1756
|
}
|
|
1716
1757
|
return data?.timeframe ? formatDayWindow(data.timeframe, context.timezone, context.format.locale) : '';
|
|
1717
1758
|
}
|
|
@@ -3435,6 +3476,9 @@ const REPORT_WATERFALL_COLORS = {
|
|
|
3435
3476
|
fall: REPORT_CHART_COLORS[1],
|
|
3436
3477
|
total: REPORT_CHART_COLORS[9],
|
|
3437
3478
|
};
|
|
3479
|
+
// The slate of a tile's small print in both exports: a KPI's label, its
|
|
3480
|
+
// window and target lines, a component's notes.
|
|
3481
|
+
const REPORT_MUTED_TEXT_COLOR = '7b8794';
|
|
3438
3482
|
// A KPI's change is colored by what it means rather than by its sign: green
|
|
3439
3483
|
// when it is good news, red when it is bad news, slate when it is neither (a
|
|
3440
3484
|
// flat change, or a value whose increaseIs is neutral). The same three colors
|
|
@@ -3445,8 +3489,10 @@ const REPORT_WATERFALL_COLORS = {
|
|
|
3445
3489
|
const REPORT_KPI_CHANGE_COLORS = {
|
|
3446
3490
|
[ReportChangePolarity.Good]: REPORT_CHART_COLORS[4],
|
|
3447
3491
|
[ReportChangePolarity.Bad]: REPORT_CHART_COLORS[2],
|
|
3448
|
-
[ReportChangePolarity.Neutral]:
|
|
3492
|
+
[ReportChangePolarity.Neutral]: REPORT_MUTED_TEXT_COLOR,
|
|
3449
3493
|
};
|
|
3494
|
+
// The track a KPI target's progress bar fills, in both exports.
|
|
3495
|
+
const REPORT_KPI_TRACK_COLOR = 'e4e7eb';
|
|
3450
3496
|
// A KPI's sparkline takes the first series color, as a one-measure chart of
|
|
3451
3497
|
// the same values would (--report-kpi-sparkline-color overrides it).
|
|
3452
3498
|
const REPORT_KPI_SPARKLINE_COLOR = REPORT_CHART_COLORS[0];
|
|
@@ -4362,7 +4408,7 @@ function seriesDetail(source, index, dataIndex) {
|
|
|
4362
4408
|
}
|
|
4363
4409
|
// A prior value's days. A range comparison compares every point with the
|
|
4364
4410
|
// same window. A series comparison compares each period with the period its
|
|
4365
|
-
// lane's offset back;
|
|
4411
|
+
// lane's offset back; a period_end comparison reads the value at the end of that
|
|
4366
4412
|
// period ('on Aug 31, 2026').
|
|
4367
4413
|
function priorDetail(source, output, dataIndex) {
|
|
4368
4414
|
const comparison = source.annotations.comparison;
|
|
@@ -4382,9 +4428,22 @@ function priorDetail(source, output, dataIndex) {
|
|
|
4382
4428
|
if (!window) {
|
|
4383
4429
|
return '';
|
|
4384
4430
|
}
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4431
|
+
if (comparison.mode === 'period_end') {
|
|
4432
|
+
return `on ${formatDayRange(window.end, window.end, locale)}`;
|
|
4433
|
+
}
|
|
4434
|
+
return formatDayWindow({ ...withinLaneSpan(window, comparison.priors?.[lane]), through: partial ? comparison.current?.through ?? null : null }, comparison.timezone, locale);
|
|
4435
|
+
}
|
|
4436
|
+
// A series lane reads the span shown moved back by its offset (the server's
|
|
4437
|
+
// comparison.priors[lane]), so the earlier copy of a period the span starts
|
|
4438
|
+
// or ends inside holds only the days within it: a span from Mar 15 sets
|
|
4439
|
+
// March against Feb 15 – 28, not the whole of February.
|
|
4440
|
+
function withinLaneSpan(window, span) {
|
|
4441
|
+
if (!span) {
|
|
4442
|
+
return window;
|
|
4443
|
+
}
|
|
4444
|
+
const start = window.start < span.start ? span.start : window.start;
|
|
4445
|
+
const end = window.end > span.end ? span.end : window.end;
|
|
4446
|
+
return start <= end ? { start, end } : window;
|
|
4388
4447
|
}
|
|
4389
4448
|
// The days one period of the axis is compared with: the period moved back by
|
|
4390
4449
|
// the lane's offset. A period not over yet, compared to date, runs only to
|
|
@@ -5403,8 +5462,8 @@ function outputColumnNote(column, data, locale) {
|
|
|
5403
5462
|
}
|
|
5404
5463
|
return [
|
|
5405
5464
|
comparisonSentence(output, data.comparison, locale),
|
|
5406
|
-
rankSentence(output),
|
|
5407
|
-
shareSentence(output),
|
|
5465
|
+
rankSentence(output, data.outputs),
|
|
5466
|
+
shareSentence(output, data.outputs),
|
|
5408
5467
|
windowSentences(data.windows?.[alias], locale, data),
|
|
5409
5468
|
]
|
|
5410
5469
|
.filter((sentence) => !!sentence)
|
|
@@ -5422,7 +5481,19 @@ function componentWindowsLine(data, context) {
|
|
|
5422
5481
|
}
|
|
5423
5482
|
return data.timeframe ? formatDayWindow(data.timeframe, context.timezone, context.locale) : '';
|
|
5424
5483
|
}
|
|
5484
|
+
// Read on one day, the line names the days the values are read on: 'On Sep
|
|
5485
|
+
// 15, 2026 · vs previous month: Aug 31, 2026'.
|
|
5425
5486
|
function comparisonWindowsLine(comparison, currentWindow, locale) {
|
|
5487
|
+
if (isReadOnOneDay(comparison)) {
|
|
5488
|
+
const priors = (comparison.offsets ?? []).map((offset, index) => {
|
|
5489
|
+
const prior = comparison.priors?.[index];
|
|
5490
|
+
const day = prior ? formatDayRange(prior.end, prior.end, locale) : '';
|
|
5491
|
+
return day ? `${comparisonOffsetLabel(offset)}: ${day}` : comparisonOffsetLabel(offset);
|
|
5492
|
+
});
|
|
5493
|
+
return [formatReadingDay(currentWindow, locale, true), ...priors]
|
|
5494
|
+
.filter((part) => !!part)
|
|
5495
|
+
.join(WINDOWS_SEPARATOR);
|
|
5496
|
+
}
|
|
5426
5497
|
const current = formatDayWindow(currentWindow, comparison.timezone, locale);
|
|
5427
5498
|
const priors = (comparison.offsets ?? []).map((offset, index) => {
|
|
5428
5499
|
return priorWindowText(offset, comparison.priors?.[index], comparison.timezone, locale);
|
|
@@ -5459,8 +5530,9 @@ function windowNotes(data, locale) {
|
|
|
5459
5530
|
.flatMap((meta) => (meta.notes ?? []).map((code) => windowNoteText(code, meta, locale, data.granularity)));
|
|
5460
5531
|
}
|
|
5461
5532
|
// A compared column's windows. In range mode every row compares the same two
|
|
5462
|
-
// windows, so they are named
|
|
5463
|
-
//
|
|
5533
|
+
// windows, so they are named, and read on one day the days the values are
|
|
5534
|
+
// read on are; over a series or a spine of periods each row compares its own
|
|
5535
|
+
// period, so only the lane is.
|
|
5464
5536
|
function comparisonSentence(output, comparison, locale) {
|
|
5465
5537
|
if (!output.role || !comparison) {
|
|
5466
5538
|
return '';
|
|
@@ -5472,6 +5544,9 @@ function comparisonSentence(output, comparison, locale) {
|
|
|
5472
5544
|
if (comparison.mode === 'range' && current && (output.role === 'current' || prior)) {
|
|
5473
5545
|
return rangeSentence(output.role, current, prior, offset);
|
|
5474
5546
|
}
|
|
5547
|
+
if (isReadOnOneDay(comparison) && comparison.current && priorWindow) {
|
|
5548
|
+
return readingDaySentence(output.role, comparison.current, priorWindow, offset, locale);
|
|
5549
|
+
}
|
|
5475
5550
|
return output.role === 'current' || !offset ? '' : `${ROLE_LEADS[output.role]} ${comparisonOffsetLabel(offset)}.`;
|
|
5476
5551
|
}
|
|
5477
5552
|
function rangeSentence(role, current, prior, offset) {
|
|
@@ -5484,9 +5559,25 @@ function rangeSentence(role, current, prior, offset) {
|
|
|
5484
5559
|
return `${ROLE_LEADS[role]} from ${prior} to ${current}.`;
|
|
5485
5560
|
}
|
|
5486
5561
|
}
|
|
5562
|
+
// A column of a comparison read on one day, named by the days its values are
|
|
5563
|
+
// read on: 'Prior value on Aug 31, 2026 (vs previous month).'
|
|
5564
|
+
function readingDaySentence(role, currentWindow, priorWindow, offset, locale) {
|
|
5565
|
+
const current = formatReadingDay(currentWindow, locale);
|
|
5566
|
+
const prior = formatReadingDay(priorWindow, locale);
|
|
5567
|
+
switch (role) {
|
|
5568
|
+
case 'current':
|
|
5569
|
+
return `Value ${current}.`;
|
|
5570
|
+
case 'prior':
|
|
5571
|
+
return offset ? `Prior value ${prior} (${comparisonOffsetLabel(offset)}).` : `Prior value ${prior}.`;
|
|
5572
|
+
default:
|
|
5573
|
+
return `${ROLE_LEADS[role]} from the value ${prior} to the value ${current}.`;
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5487
5576
|
// 'Rank by Revenue, 1 being the largest, within each Region. Tied values share
|
|
5488
|
-
// a rank (1, 1, 3).'
|
|
5489
|
-
|
|
5577
|
+
// a rank (1, 1, 3).' A measure written as its own rank ranks by the value it
|
|
5578
|
+
// would otherwise show, so it names no other measure ('Rank, 1 being the
|
|
5579
|
+
// largest, across all rows.').
|
|
5580
|
+
function rankSentence(output, outputs) {
|
|
5490
5581
|
const rank = output.kind === 'rank' ? output.rank : null;
|
|
5491
5582
|
if (!rank) {
|
|
5492
5583
|
return '';
|
|
@@ -5494,18 +5585,32 @@ function rankSentence(output) {
|
|
|
5494
5585
|
const end = rank.direction === 'asc' ? 'smallest' : 'largest';
|
|
5495
5586
|
const scope = rank.within?.length ? `within each ${joinNames(rank.within)}` : 'across all rows';
|
|
5496
5587
|
const ties = rank.method === 'dense' ? '1, 1, 2' : '1, 1, 3';
|
|
5497
|
-
|
|
5588
|
+
const by = isOwnOutput(output, outputs) ? '' : ` by ${output.measure}`;
|
|
5589
|
+
return `Rank${by}, 1 being the ${end}, ${scope}. Tied values share a rank (${ties}). `
|
|
5498
5590
|
+ FILTERED_ROWS_SENTENCE;
|
|
5499
5591
|
}
|
|
5500
|
-
// 'Share of the total Revenue within each Region.'
|
|
5501
|
-
|
|
5592
|
+
// 'Share of the total Revenue within each Region.' A measure written as its
|
|
5593
|
+
// own share names no other measure ('Share of the total within each
|
|
5594
|
+
// Region.', 'Share of the total across all rows.').
|
|
5595
|
+
function shareSentence(output, outputs) {
|
|
5502
5596
|
const share = output.kind === 'share' ? output.share : null;
|
|
5503
5597
|
if (!share) {
|
|
5504
5598
|
return '';
|
|
5505
5599
|
}
|
|
5600
|
+
if (isOwnOutput(output, outputs)) {
|
|
5601
|
+
const scope = share.within?.length ? `within each ${joinNames(share.within)}` : 'across all rows';
|
|
5602
|
+
return `Share of the total ${scope}. ${FILTERED_ROWS_SENTENCE}`;
|
|
5603
|
+
}
|
|
5506
5604
|
const scope = share.within?.length ? ` within each ${joinNames(share.within)}` : '';
|
|
5507
5605
|
return `Share of the total ${output.measure}${scope}. ${FILTERED_ROWS_SENTENCE}`;
|
|
5508
5606
|
}
|
|
5607
|
+
// Whether a rank or a share is its measure's own output: the server writes a
|
|
5608
|
+
// measure with a rank or a share as that rank or share, so its measure is a
|
|
5609
|
+
// column of the same kind (itself, or for a prior or a change the current
|
|
5610
|
+
// column), never a value of its own to name.
|
|
5611
|
+
function isOwnOutput(output, outputs) {
|
|
5612
|
+
return outputs?.[output.measure]?.kind === output.kind;
|
|
5613
|
+
}
|
|
5509
5614
|
// A windowed column: what it is, then why it reads as it does.
|
|
5510
5615
|
function windowSentences(meta, locale, data) {
|
|
5511
5616
|
if (!meta) {
|
|
@@ -8105,6 +8210,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
8105
8210
|
], template: "<form\n fsForm\n [submit]=\"save\">\n <fs-dialog>\n <h1 mat-dialog-title>\n Report Settings\n </h1>\n <mat-dialog-content>\n <!-- One skeleton over the whole body until the locale list is in. -->\n <ng-container *fsSkeletonForm=\"ready() || null\">\n <mat-tab-group [(selected)]=\"selectedTab\">\n <mat-tab\n label=\"Settings\"\n name=\"settings\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Name\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"name\"\n name=\"name\"\n [fsFormRequired]=\"true\">\n </mat-form-field>\n <!--\n The page, typed in inches. Orientation is not asked for because it\n is not an independent choice - a page wider than it is tall IS\n landscape - so the two numbers say everything there is to say.\n -->\n <div class=\"fs-row.gap-sm.align-start\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Width\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"1\"\n [(ngModel)]=\"pageWidth\"\n name=\"pageWidth\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"1\"\n [(ngModel)]=\"pageHeight\"\n name=\"pageHeight\"\n [fsFormRequired]=\"true\"\n [fsFormMin]=\"pageDimensionMin\"\n [fsFormMax]=\"pageDimensionMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n </div>\n <!--\n How deep the header and footer bands are. Their content is edited\n on the canvas (Edit layout \u2192 Header & footer); these reserve the\n space for it, and 0 means no band.\n -->\n <mat-form-field class=\"full-width\">\n <mat-label>\n Header height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"2\"\n [(ngModel)]=\"headerHeight\"\n name=\"headerHeight\"\n [fsFormMin]=\"0\"\n [fsFormMax]=\"headerFooterMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Footer height\n </mat-label>\n <input\n matInput\n fsMaskNumber\n [scale]=\"2\"\n [(ngModel)]=\"footerHeight\"\n name=\"footerHeight\"\n [fsFormMin]=\"0\"\n [fsFormMax]=\"headerFooterMax\">\n <div matTextSuffix>\n inches\n </div>\n </mat-form-field>\n <fs-ai-report-timezone-select [(timezone)]=\"timezone\"></fs-ai-report-timezone-select>\n <!--\n How numbers and dates are written (formatting only: no text is\n translated) and which currency money is labeled in. Hidden on a\n server too old to resolve a report's locale.\n -->\n @if (locales()) {\n <mat-form-field class=\"full-width\">\n <mat-label>\n Locale\n </mat-label>\n <mat-select\n [ngModel]=\"locale()\"\n (ngModelChange)=\"locale.set($event)\"\n name=\"locale\">\n @for (option of localeOptions(); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n {{ formatExample() }}\n </mat-hint>\n </mat-form-field>\n <mat-form-field class=\"full-width\">\n <mat-label>\n Currency\n </mat-label>\n <mat-select\n [ngModel]=\"currency()\"\n (ngModelChange)=\"currency.set($event)\"\n name=\"currency\">\n @for (option of currencyOptions(); track option.value) {\n <mat-option [value]=\"option.value\">\n {{ option.label }}\n </mat-option>\n }\n </mat-select>\n <mat-hint>\n Names the currency amounts are in; it does not convert them.\n </mat-hint>\n </mat-form-field>\n }\n </div>\n </mat-tab>\n <mat-tab\n label=\"Styles\"\n name=\"styles\">\n <div class=\"fs-column tab-body\">\n <mat-form-field>\n <mat-label>\n Heading Size\n </mat-label>\n <input\n matInput\n type=\"number\"\n step=\"1\"\n min=\"6\"\n max=\"96\"\n [(ngModel)]=\"headingSize\"\n name=\"headingSize\"\n [fsFormMin]=\"6\"\n [fsFormMax]=\"96\">\n <span matTextSuffix>\n pt\n </span>\n <mat-hint>\n Applies to component titles. Points map 1:1 to PDF and PowerPoint.\n </mat-hint>\n </mat-form-field>\n </div>\n </mat-tab>\n <mat-tab\n label=\"Filters\"\n name=\"filters\">\n <div class=\"fs-column tab-body\">\n @if (filterRows().length) {\n @for (row of filterRows(); track row.group.id) {\n @switch (row.group.type) {\n @case ('dateRange') {\n <div class=\"fs-row.gap-sm.align-center fs-flex\">\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default From {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.start\"\n [name]=\"'start' + row.group.id\">\n </mat-form-field>\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default To {{ row.label }}\n </mat-label>\n <input\n matInput\n fsDatePicker\n [(ngModel)]=\"row.end\"\n [name]=\"'end' + row.group.id\">\n </mat-form-field>\n </div>\n <!-- A stored rolling preset cannot be authored here, only\n kept, replaced by picking dates, or cleared. type=\"button\"\n keeps Clear from submitting the form. -->\n @if (row.relative && !row.start && !row.end) {\n <div class=\"fs-row.gap-sm.align-center\">\n <mat-hint class=\"fs-flex\">\n Rolling default: {{ relativeLabel(row.relative) }}. Pick dates to replace it.\n </mat-hint>\n <button\n mat-button\n type=\"button\"\n (click)=\"clearRelative(row)\">\n Clear\n </button>\n </div>\n }\n }\n @case ('select') {\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [label]=\"'Default ' + row.label\"\n [fetch]=\"row.fetch\"\n [(ngModel)]=\"row.values\"\n [name]=\"'values' + row.group.id\"\n [multiple]=\"true\"\n [fetchOnFocus]=\"true\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n </fs-autocomplete-chips>\n }\n @default {\n <mat-form-field class=\"fs-flex\">\n <mat-label>\n Default {{ row.label }}\n </mat-label>\n <input\n matInput\n [(ngModel)]=\"row.value\"\n [name]=\"'value' + row.group.id\">\n </mat-form-field>\n }\n }\n }\n } @else {\n <fs-message-info>\n This report has no report-level filters yet. Add a filter to a\n component (its settings \u2192 Filters) and show it in the report bar,\n then set its default here.\n </fs-message-info>\n }\n </div>\n </mat-tab>\n </mat-tab-group>\n </ng-container>\n </mat-dialog-content>\n <mat-dialog-actions>\n <fs-form-dialog-actions>\n <button\n type=\"button\"\n mat-button\n color=\"warn\"\n (click)=\"delete()\">\n Delete\n </button>\n </fs-form-dialog-actions>\n </mat-dialog-actions>\n </fs-dialog>\n</form>", styles: ["mat-form-field{width:100%}.tab-body{padding-top:16px}\n"] }]
|
|
8106
8211
|
}] });
|
|
8107
8212
|
|
|
8213
|
+
// How far a KPI card's rows shrink to fit a short tile. A tile shorter still
|
|
8214
|
+
// lets the card run past its foot rather than print it unreadably small.
|
|
8215
|
+
const KPI_EXPORT_MIN_SCALE = 0.5;
|
|
8108
8216
|
// How many tile queries an export runs at once.
|
|
8109
8217
|
//
|
|
8110
8218
|
// Not a politeness setting — it is the difference between a deck and a broken
|
|
@@ -8264,7 +8372,9 @@ function kpiCardHasExtras(view, notes) {
|
|
|
8264
8372
|
// sparkline first, then the target line, then the notes; the value, its label
|
|
8265
8373
|
// and the delta row always stay, and the window line stays whenever the card
|
|
8266
8374
|
// shows a comparison (its days are what the delta is measured over). The
|
|
8267
|
-
// sparkline needs values to draw.
|
|
8375
|
+
// sparkline needs values to draw. When what stays is still too tall, every
|
|
8376
|
+
// row shrinks by the same scale, down to KPI_EXPORT_MIN_SCALE; by then the
|
|
8377
|
+
// sparkline, the target and the notes are gone.
|
|
8268
8378
|
function kpiExportParts(view, notes, available, heights) {
|
|
8269
8379
|
const parts = {
|
|
8270
8380
|
delta: !!view.delta,
|
|
@@ -8272,6 +8382,7 @@ function kpiExportParts(view, notes, available, heights) {
|
|
|
8272
8382
|
target: !!view.target,
|
|
8273
8383
|
sparkline: !!view.sparkline?.values.some((value) => value !== null),
|
|
8274
8384
|
notes: [...notes],
|
|
8385
|
+
scale: 1,
|
|
8275
8386
|
};
|
|
8276
8387
|
const height = () => kpiExportHeight(view, parts, heights);
|
|
8277
8388
|
if (height() > available) {
|
|
@@ -8286,8 +8397,24 @@ function kpiExportParts(view, notes, available, heights) {
|
|
|
8286
8397
|
if (height() > available && !parts.delta) {
|
|
8287
8398
|
parts.window = false;
|
|
8288
8399
|
}
|
|
8400
|
+
if (height() > available) {
|
|
8401
|
+
parts.scale = Math.max(KPI_EXPORT_MIN_SCALE, Math.max(0, available) / height());
|
|
8402
|
+
}
|
|
8289
8403
|
return parts;
|
|
8290
8404
|
}
|
|
8405
|
+
// The heights scaled by the parts' scale: what the renderer lays the card
|
|
8406
|
+
// out with, its text sized by the same scale.
|
|
8407
|
+
function scaledKpiHeights(heights, scale) {
|
|
8408
|
+
return {
|
|
8409
|
+
value: heights.value * scale,
|
|
8410
|
+
label: heights.label * scale,
|
|
8411
|
+
delta: heights.delta * scale,
|
|
8412
|
+
window: heights.window * scale,
|
|
8413
|
+
target: heights.target * scale,
|
|
8414
|
+
sparkline: heights.sparkline * scale,
|
|
8415
|
+
note: heights.note * scale,
|
|
8416
|
+
};
|
|
8417
|
+
}
|
|
8291
8418
|
// How tall the card's drawn parts stand together, in the heights' unit.
|
|
8292
8419
|
function kpiExportHeight(view, parts, heights) {
|
|
8293
8420
|
return heights.value
|
|
@@ -8485,7 +8612,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
8485
8612
|
// in the report's format context, as on screen.
|
|
8486
8613
|
async function renderChartImage(component, data, width, height, context, pixelRatio = 3) {
|
|
8487
8614
|
// The same tree-shaken echarts build + 'report' theme the live canvas uses.
|
|
8488
|
-
const echarts = (await import('./firestitch-report-echarts-
|
|
8615
|
+
const echarts = (await import('./firestitch-report-echarts-CFnbJx44.mjs')).default;
|
|
8489
8616
|
const host = document.createElement('div');
|
|
8490
8617
|
host.style.width = `${width}px`;
|
|
8491
8618
|
host.style.height = `${height}px`;
|
|
@@ -8522,7 +8649,7 @@ const SMALL_SIZE = 7.5;
|
|
|
8522
8649
|
const SUBTITLE_H$1 = 11;
|
|
8523
8650
|
const NOTE_LINE_H$1 = 10;
|
|
8524
8651
|
const MIN_BODY_H$1 = 40;
|
|
8525
|
-
const MUTED_COLOR$1 =
|
|
8652
|
+
const MUTED_COLOR$1 = `#${REPORT_MUTED_TEXT_COLOR}`;
|
|
8526
8653
|
// A KPI card's parts, top to bottom, points (kpiExportParts sheds the
|
|
8527
8654
|
// sparkline, then the target, then the notes on a short tile). The value's
|
|
8528
8655
|
// baseline sits KPI_VALUE_BASELINE under its top.
|
|
@@ -8543,7 +8670,7 @@ const KPI_ARROW_GAP$1 = 3;
|
|
|
8543
8670
|
// The target's progress bar, and the track it fills.
|
|
8544
8671
|
const KPI_BAR_W$1 = 120;
|
|
8545
8672
|
const KPI_BAR_H$1 = 3;
|
|
8546
|
-
const KPI_TRACK_COLOR$1 =
|
|
8673
|
+
const KPI_TRACK_COLOR$1 = `#${REPORT_KPI_TRACK_COLOR}`;
|
|
8547
8674
|
// The sparkline's widest box, its top and bottom inset, and its dots' radius.
|
|
8548
8675
|
const KPI_SPARKLINE_W$1 = 140;
|
|
8549
8676
|
const KPI_SPARKLINE_INSET = 3;
|
|
@@ -8835,12 +8962,12 @@ class ReportPdfService {
|
|
|
8835
8962
|
}
|
|
8836
8963
|
// One line of small grey print, cut to the width with an ellipsis: a
|
|
8837
8964
|
// subtitle, a note or a line of a KPI card. y is the line's top; the line is
|
|
8838
|
-
// NOTE_LINE_H tall.
|
|
8839
|
-
_smallLine(doc, text, x, y, w, align, style) {
|
|
8965
|
+
// NOTE_LINE_H tall, times the scale a short KPI tile shrinks its lines by.
|
|
8966
|
+
_smallLine(doc, text, x, y, w, align, style, scale = 1) {
|
|
8840
8967
|
doc.setFont('helvetica', style);
|
|
8841
|
-
doc.setFontSize(SMALL_SIZE);
|
|
8968
|
+
doc.setFontSize(SMALL_SIZE * scale);
|
|
8842
8969
|
doc.setTextColor(MUTED_COLOR$1);
|
|
8843
|
-
doc.text(this._fit(doc, text, w), align === 'center' ? x + w / 2 : x, y + SMALL_SIZE, { align });
|
|
8970
|
+
doc.text(this._fit(doc, text, w), align === 'center' ? x + w / 2 : x, y + SMALL_SIZE * scale, { align });
|
|
8844
8971
|
}
|
|
8845
8972
|
// The text, ellipsized to the width in the doc's current font.
|
|
8846
8973
|
_fit(doc, text, width) {
|
|
@@ -8865,7 +8992,7 @@ class ReportPdfService {
|
|
|
8865
8992
|
_boxNote(doc, text, x, y, w, h) {
|
|
8866
8993
|
doc.setFont('helvetica', 'italic');
|
|
8867
8994
|
doc.setFontSize(9);
|
|
8868
|
-
doc.setTextColor(
|
|
8995
|
+
doc.setTextColor(MUTED_COLOR$1);
|
|
8869
8996
|
doc.text(text, x + w / 2, y + h / 2, { align: 'center' });
|
|
8870
8997
|
}
|
|
8871
8998
|
// A list's on-page table. All of its fetched rows are laid out once to learn
|
|
@@ -8963,7 +9090,7 @@ class ReportPdfService {
|
|
|
8963
9090
|
// takes columnStyles[0], right-aligned when that column is numeric.
|
|
8964
9091
|
styles: {
|
|
8965
9092
|
fontStyle: 'italic',
|
|
8966
|
-
textColor:
|
|
9093
|
+
textColor: MUTED_COLOR$1,
|
|
8967
9094
|
halign: 'left',
|
|
8968
9095
|
...(singleLine ? { overflow: 'ellipsize' } : {}),
|
|
8969
9096
|
},
|
|
@@ -9075,69 +9202,73 @@ class ReportPdfService {
|
|
|
9075
9202
|
if (view.label) {
|
|
9076
9203
|
doc.setFont('helvetica', 'normal');
|
|
9077
9204
|
doc.setFontSize(9);
|
|
9078
|
-
doc.setTextColor(
|
|
9205
|
+
doc.setTextColor(MUTED_COLOR$1);
|
|
9079
9206
|
doc.text(view.label, x + w / 2, y + h / 2 + 16, { align: 'center' });
|
|
9080
9207
|
}
|
|
9081
9208
|
return;
|
|
9082
9209
|
}
|
|
9210
|
+
// A tile too short for the card's lines shrinks them all alike.
|
|
9083
9211
|
const parts = kpiExportParts(view, notes, h, KPI_HEIGHTS$1);
|
|
9084
|
-
|
|
9212
|
+
const heights = scaledKpiHeights(KPI_HEIGHTS$1, parts.scale);
|
|
9213
|
+
let top = y + Math.max(0, (h - kpiExportHeight(view, parts, heights)) / 2);
|
|
9085
9214
|
doc.setFont('helvetica', 'bold');
|
|
9086
|
-
doc.setFontSize(26);
|
|
9215
|
+
doc.setFontSize(26 * parts.scale);
|
|
9087
9216
|
doc.setTextColor('#1f2933');
|
|
9088
|
-
doc.text(value, x + w / 2, top + KPI_VALUE_BASELINE, { align: 'center' });
|
|
9089
|
-
top +=
|
|
9217
|
+
doc.text(value, x + w / 2, top + KPI_VALUE_BASELINE * parts.scale, { align: 'center' });
|
|
9218
|
+
top += heights.value;
|
|
9090
9219
|
if (view.label) {
|
|
9091
9220
|
doc.setFont('helvetica', 'normal');
|
|
9092
|
-
doc.setFontSize(9);
|
|
9221
|
+
doc.setFontSize(9 * parts.scale);
|
|
9093
9222
|
doc.setTextColor(MUTED_COLOR$1);
|
|
9094
|
-
doc.text(this._fit(doc, this._pdfText(view.label), w), x + w / 2, top + 9, { align: 'center' });
|
|
9095
|
-
top +=
|
|
9223
|
+
doc.text(this._fit(doc, this._pdfText(view.label), w), x + w / 2, top + 9 * parts.scale, { align: 'center' });
|
|
9224
|
+
top += heights.label;
|
|
9096
9225
|
}
|
|
9097
9226
|
if (parts.delta && view.delta) {
|
|
9098
|
-
this._kpiDelta(doc, view.delta, x, top, w);
|
|
9099
|
-
top +=
|
|
9227
|
+
this._kpiDelta(doc, view.delta, x, top, w, parts.scale);
|
|
9228
|
+
top += heights.delta;
|
|
9100
9229
|
}
|
|
9101
9230
|
if (parts.window) {
|
|
9102
|
-
this._smallLine(doc, this._pdfText(view.window), x, top, w, 'center', 'normal');
|
|
9103
|
-
top +=
|
|
9231
|
+
this._smallLine(doc, this._pdfText(view.window), x, top, w, 'center', 'normal', parts.scale);
|
|
9232
|
+
top += heights.window;
|
|
9104
9233
|
}
|
|
9105
9234
|
if (parts.target && view.target) {
|
|
9106
9235
|
this._kpiTarget(doc, view.target, x, top, w);
|
|
9107
|
-
top +=
|
|
9236
|
+
top += heights.target;
|
|
9108
9237
|
}
|
|
9109
9238
|
if (parts.sparkline && view.sparkline) {
|
|
9110
9239
|
this._kpiSparkline(doc, view.sparkline.values, view.sparkline.partial, x, top, w);
|
|
9111
|
-
top +=
|
|
9240
|
+
top += heights.sparkline;
|
|
9112
9241
|
}
|
|
9113
9242
|
for (const line of parts.notes) {
|
|
9114
9243
|
this._smallLine(doc, this._pdfText(line), x, top, w, 'center', 'italic');
|
|
9115
|
-
top +=
|
|
9244
|
+
top += heights.note;
|
|
9116
9245
|
}
|
|
9117
9246
|
}
|
|
9118
9247
|
// The delta row, centered as one group: a triangle pointing the way the
|
|
9119
9248
|
// value moved (none when the shown change rounds to zero) and the change in
|
|
9120
9249
|
// the color of what it means, then what it is compared with in grey.
|
|
9121
|
-
_kpiDelta(doc, delta, x, top, w) {
|
|
9250
|
+
_kpiDelta(doc, delta, x, top, w, scale = 1) {
|
|
9122
9251
|
const color = `#${REPORT_KPI_CHANGE_COLORS[delta.tone]}`;
|
|
9123
9252
|
const change = this._pdfText(delta.text);
|
|
9124
|
-
const
|
|
9253
|
+
const size = KPI_DELTA_SIZE * scale;
|
|
9254
|
+
const side = KPI_ARROW$1 * scale;
|
|
9255
|
+
const arrow = delta.trend === ReportKpiTrend.Flat ? 0 : side + KPI_ARROW_GAP$1 * scale;
|
|
9125
9256
|
doc.setFont('helvetica', 'bold');
|
|
9126
|
-
doc.setFontSize(
|
|
9257
|
+
doc.setFontSize(size);
|
|
9127
9258
|
const changeWidth = doc.getTextWidth(change);
|
|
9128
9259
|
doc.setFont('helvetica', 'normal');
|
|
9129
9260
|
const compared = delta.compared
|
|
9130
9261
|
? this._fit(doc, ` ${this._pdfText(delta.compared)}`, Math.max(0, w - arrow - changeWidth))
|
|
9131
9262
|
: '';
|
|
9132
9263
|
const width = arrow + changeWidth + (compared ? doc.getTextWidth(compared) : 0);
|
|
9133
|
-
const baseline = top +
|
|
9264
|
+
const baseline = top + size;
|
|
9134
9265
|
let left = x + Math.max(0, (w - width) / 2);
|
|
9135
9266
|
if (arrow) {
|
|
9136
9267
|
const up = delta.trend === ReportKpiTrend.Up;
|
|
9137
|
-
const tip = up ? baseline -
|
|
9138
|
-
const base = up ? baseline : baseline -
|
|
9268
|
+
const tip = up ? baseline - side : baseline;
|
|
9269
|
+
const base = up ? baseline : baseline - side;
|
|
9139
9270
|
doc.setFillColor(color);
|
|
9140
|
-
doc.triangle(left, base, left +
|
|
9271
|
+
doc.triangle(left, base, left + side, base, left + side / 2, tip, 'F');
|
|
9141
9272
|
left += arrow;
|
|
9142
9273
|
}
|
|
9143
9274
|
doc.setFont('helvetica', 'bold');
|
|
@@ -9245,7 +9376,7 @@ const SMALL_FONT_SIZE = 8;
|
|
|
9245
9376
|
const SUBTITLE_H = 0.18;
|
|
9246
9377
|
const NOTE_LINE_H = 0.16;
|
|
9247
9378
|
const MIN_BODY_H = 0.6;
|
|
9248
|
-
const MUTED_COLOR =
|
|
9379
|
+
const MUTED_COLOR = REPORT_MUTED_TEXT_COLOR;
|
|
9249
9380
|
// A KPI card's parts, top to bottom, inches (kpiExportParts sheds the
|
|
9250
9381
|
// sparkline, then the target, then the notes on a short tile).
|
|
9251
9382
|
const KPI_TARGET_TEXT_H = 0.18;
|
|
@@ -9270,7 +9401,7 @@ const KPI_ARROW_GAP = 0.04;
|
|
|
9270
9401
|
const PPTX_CHAR_WIDTH = 0.42;
|
|
9271
9402
|
// The target's progress bar, and the track it fills.
|
|
9272
9403
|
const KPI_BAR_W = 1.7;
|
|
9273
|
-
const KPI_TRACK_COLOR =
|
|
9404
|
+
const KPI_TRACK_COLOR = REPORT_KPI_TRACK_COLOR;
|
|
9274
9405
|
// The sparkline's widest box, and its series name in the data sheet when the
|
|
9275
9406
|
// measure has no label.
|
|
9276
9407
|
const KPI_SPARKLINE_W = 2;
|
|
@@ -9500,7 +9631,7 @@ class ReportPptxService {
|
|
|
9500
9631
|
if (!item.data || !item.data.rows?.length) {
|
|
9501
9632
|
slide.addText(emptyLabel(item), {
|
|
9502
9633
|
x, y: bodyY, w, h: bodyH,
|
|
9503
|
-
fontSize: 10, italic: true, color:
|
|
9634
|
+
fontSize: 10, italic: true, color: MUTED_COLOR, align: 'center', valign: 'middle',
|
|
9504
9635
|
});
|
|
9505
9636
|
return;
|
|
9506
9637
|
}
|
|
@@ -10085,7 +10216,7 @@ class ReportPptxService {
|
|
|
10085
10216
|
if (remaining > 0) {
|
|
10086
10217
|
body.push([{
|
|
10087
10218
|
text: moreRowsNotice(remaining),
|
|
10088
|
-
options: { colspan: columns.length, italic: true, color:
|
|
10219
|
+
options: { colspan: columns.length, italic: true, color: MUTED_COLOR },
|
|
10089
10220
|
}]);
|
|
10090
10221
|
}
|
|
10091
10222
|
// The label is interface text, so it keeps the default proofing language.
|
|
@@ -10120,7 +10251,7 @@ class ReportPptxService {
|
|
|
10120
10251
|
text: view.value,
|
|
10121
10252
|
options: pptxRunOptions({ fontSize: 28, bold: true, color: '1F2933', breakLine: true }, context.locale),
|
|
10122
10253
|
},
|
|
10123
|
-
{ text: view.label, options: { fontSize: 10, color:
|
|
10254
|
+
{ text: view.label, options: { fontSize: 10, color: MUTED_COLOR } },
|
|
10124
10255
|
], {
|
|
10125
10256
|
x, y, w, h,
|
|
10126
10257
|
align: 'center',
|
|
@@ -10128,8 +10259,11 @@ class ReportPptxService {
|
|
|
10128
10259
|
});
|
|
10129
10260
|
return;
|
|
10130
10261
|
}
|
|
10262
|
+
// A tile too short for the card's lines shrinks them all alike.
|
|
10131
10263
|
const parts = kpiExportParts(view, notes, h, KPI_HEIGHTS);
|
|
10132
|
-
|
|
10264
|
+
const heights = scaledKpiHeights(KPI_HEIGHTS, parts.scale);
|
|
10265
|
+
const lineFontSize = KPI_LINE_FONT_SIZE * parts.scale;
|
|
10266
|
+
let top = y + Math.max(0, (h - kpiExportHeight(view, parts, heights)) / 2);
|
|
10133
10267
|
const line = (runs, rowH, options) => {
|
|
10134
10268
|
slide.addText(runs, {
|
|
10135
10269
|
x, y: top, w, h: rowH,
|
|
@@ -10137,31 +10271,31 @@ class ReportPptxService {
|
|
|
10137
10271
|
});
|
|
10138
10272
|
top += rowH;
|
|
10139
10273
|
};
|
|
10140
|
-
line([{ text: view.value, options: pptxRunOptions({ bold: true, color: '1F2933' }, context.locale) }],
|
|
10274
|
+
line([{ text: view.value, options: pptxRunOptions({ bold: true, color: '1F2933' }, context.locale) }], heights.value, { fontSize: 28 * parts.scale });
|
|
10141
10275
|
if (view.label) {
|
|
10142
|
-
line(view.label,
|
|
10276
|
+
line(view.label, heights.label, { fontSize: 10 * parts.scale, color: MUTED_COLOR });
|
|
10143
10277
|
}
|
|
10144
10278
|
if (parts.delta && view.delta) {
|
|
10145
|
-
this._kpiDelta(pptx, slide, view.delta, context, x, top, w);
|
|
10146
|
-
top +=
|
|
10279
|
+
this._kpiDelta(pptx, slide, view.delta, context, x, top, w, parts.scale);
|
|
10280
|
+
top += heights.delta;
|
|
10147
10281
|
}
|
|
10148
10282
|
if (parts.window) {
|
|
10149
|
-
const days = this._fitEstimate(view.window,
|
|
10150
|
-
line([{ text: days, options: pptxRunOptions({}, context.locale) }],
|
|
10151
|
-
fontSize:
|
|
10283
|
+
const days = this._fitEstimate(view.window, lineFontSize, w);
|
|
10284
|
+
line([{ text: days, options: pptxRunOptions({}, context.locale) }], heights.window, {
|
|
10285
|
+
fontSize: lineFontSize, color: MUTED_COLOR,
|
|
10152
10286
|
});
|
|
10153
10287
|
}
|
|
10154
10288
|
if (parts.target && view.target) {
|
|
10155
10289
|
this._kpiTarget(pptx, slide, view.target, x, top, w);
|
|
10156
|
-
top +=
|
|
10290
|
+
top += heights.target;
|
|
10157
10291
|
}
|
|
10158
10292
|
if (parts.sparkline && view.sparkline) {
|
|
10159
10293
|
this._kpiSparkline(pptx, slide, item, view.sparkline.values, view.label, x, top, w);
|
|
10160
|
-
top +=
|
|
10294
|
+
top += heights.sparkline;
|
|
10161
10295
|
}
|
|
10162
10296
|
if (parts.notes.length) {
|
|
10163
10297
|
const fitted = parts.notes.map((note) => this._fitEstimate(note, SMALL_FONT_SIZE, w));
|
|
10164
|
-
line(this._lineRuns(fitted, { italic: true }), parts.notes.length *
|
|
10298
|
+
line(this._lineRuns(fitted, { italic: true }), parts.notes.length * heights.note, {
|
|
10165
10299
|
fontSize: SMALL_FONT_SIZE, color: MUTED_COLOR,
|
|
10166
10300
|
});
|
|
10167
10301
|
}
|
|
@@ -10171,28 +10305,31 @@ class ReportPptxService {
|
|
|
10171
10305
|
// it pointing the way the value moved (none when the shown change rounds to
|
|
10172
10306
|
// zero). PowerPoint lays the text out itself, so the triangle sits where
|
|
10173
10307
|
// the text's estimated width puts its start.
|
|
10174
|
-
_kpiDelta(pptx, slide, delta, context, x, top, w) {
|
|
10308
|
+
_kpiDelta(pptx, slide, delta, context, x, top, w, scale = 1) {
|
|
10175
10309
|
const color = REPORT_KPI_CHANGE_COLORS[delta.tone];
|
|
10176
|
-
const
|
|
10177
|
-
const
|
|
10310
|
+
const fontSize = KPI_LINE_FONT_SIZE * scale;
|
|
10311
|
+
const rowH = KPI_HEIGHTS.delta * scale;
|
|
10312
|
+
const side = KPI_ARROW * scale;
|
|
10313
|
+
const arrow = delta.trend === ReportKpiTrend.Flat ? 0 : side + KPI_ARROW_GAP * scale;
|
|
10314
|
+
const changeW = this._estimatedWidth(delta.text, fontSize);
|
|
10178
10315
|
const compared = delta.compared
|
|
10179
|
-
? this._fitEstimate(` ${delta.compared}`,
|
|
10316
|
+
? this._fitEstimate(` ${delta.compared}`, fontSize, w - arrow - changeW)
|
|
10180
10317
|
: '';
|
|
10181
|
-
const textW = Math.min(w - arrow, changeW + this._estimatedWidth(compared,
|
|
10318
|
+
const textW = Math.min(w - arrow, changeW + this._estimatedWidth(compared, fontSize));
|
|
10182
10319
|
const runs = [
|
|
10183
10320
|
{ text: delta.text, options: pptxRunOptions({ bold: true, color }, context.locale) },
|
|
10184
10321
|
...(compared ? [{ text: compared, options: { color: MUTED_COLOR } }] : []),
|
|
10185
10322
|
];
|
|
10186
10323
|
slide.addText(runs, {
|
|
10187
|
-
x: x + arrow, y: top, w: Math.max(0.1, w - arrow), h:
|
|
10188
|
-
fontSize
|
|
10324
|
+
x: x + arrow, y: top, w: Math.max(0.1, w - arrow), h: rowH,
|
|
10325
|
+
fontSize, align: 'center', valign: 'middle', margin: 0, fit: 'none',
|
|
10189
10326
|
});
|
|
10190
10327
|
if (arrow) {
|
|
10191
10328
|
slide.addShape(pptx.ShapeType.triangle, {
|
|
10192
10329
|
x: Math.max(x, x + (w - arrow - textW) / 2),
|
|
10193
|
-
y: top + (
|
|
10194
|
-
w:
|
|
10195
|
-
h:
|
|
10330
|
+
y: top + (rowH - side) / 2,
|
|
10331
|
+
w: side,
|
|
10332
|
+
h: side,
|
|
10196
10333
|
fill: { color },
|
|
10197
10334
|
line: { color, width: 0 },
|
|
10198
10335
|
flipV: delta.trend === ReportKpiTrend.Down,
|
|
@@ -10238,8 +10375,8 @@ class ReportPptxService {
|
|
|
10238
10375
|
// The sparkline as a native line chart with its axes, gridlines and legend
|
|
10239
10376
|
// hidden, scaled from its lowest value to its highest as on screen, its
|
|
10240
10377
|
// periods and values in the chart's data sheet. A gap stays a gap. Markers
|
|
10241
|
-
// show only when a value stands alone between gaps
|
|
10242
|
-
// draw.
|
|
10378
|
+
// show only when a value stands alone between gaps (the last one too),
|
|
10379
|
+
// which a line cannot draw.
|
|
10243
10380
|
_kpiSparkline(pptx, slide, item, values, label, x, top, w) {
|
|
10244
10381
|
const numbers = values.filter((value) => value !== null);
|
|
10245
10382
|
const low = Math.min(...numbers);
|
|
@@ -10247,6 +10384,10 @@ class ReportPptxService {
|
|
|
10247
10384
|
const pad = high === low ? Math.max(1, Math.abs(high)) : 0;
|
|
10248
10385
|
const boxW = Math.min(w * 0.7, KPI_SPARKLINE_W);
|
|
10249
10386
|
const geometry = kpiSparklineGeometry(values, false);
|
|
10387
|
+
// kpiSparklineGeometry keeps the last point out of the dots, as the
|
|
10388
|
+
// screen marks it apart; a last value after a gap is a lone point too.
|
|
10389
|
+
const lastStandsAlone = values[values.length - 1] !== null
|
|
10390
|
+
&& (values.length === 1 || values[values.length - 2] === null);
|
|
10250
10391
|
const periods = item.data?.kpi?.sparkline?.points?.map((point) => point.period) ?? [];
|
|
10251
10392
|
const color = REPORT_KPI_SPARKLINE_COLOR;
|
|
10252
10393
|
this._addChart(slide, { line: pptx.ChartType.line }, [{
|
|
@@ -10256,7 +10397,7 @@ class ReportPptxService {
|
|
|
10256
10397
|
options: {
|
|
10257
10398
|
chartColors: [color],
|
|
10258
10399
|
lineSize: 1.5,
|
|
10259
|
-
lineDataSymbol: geometry.dots.length ? 'circle' : 'none',
|
|
10400
|
+
lineDataSymbol: geometry.dots.length || lastStandsAlone ? 'circle' : 'none',
|
|
10260
10401
|
lineDataSymbolSize: 4,
|
|
10261
10402
|
},
|
|
10262
10403
|
}], [{
|
|
@@ -10762,7 +10903,7 @@ class ReportComponent {
|
|
|
10762
10903
|
ReportExportService,
|
|
10763
10904
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
10764
10905
|
// this route's chunk.
|
|
10765
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
10906
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CFnbJx44.mjs').then((module) => module.default) }),
|
|
10766
10907
|
], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }], ngImport: i0, template: "<div\n #split\n class=\"report fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n style=\"min-height: 500px;\"\n basePath=\"reports\"\n [requestData]=\"{ reportId: selected()?.id ?? null }\"\n [introMessage]=\"introMessage\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n <div class=\"viewer fs-flex fs-column\">\n <div class=\"fs-row.align-center.gap-sm\">\n <fs-autocomplete-chips\n class=\"fs-flex\"\n [fetch]=\"fetchReports\"\n [(ngModel)]=\"selected\"\n [disabled]=\"loadingReports()\"\n [padless]=\"true\"\n [multiple]=\"false\"\n [fetchOnFocus]=\"true\"\n (ngModelChange)=\"reportChange($event)\"\n placeholder=\"Report\"\n name=\"report\">\n <ng-template\n fsAutocompleteChipsTemplate\n let-object=\"object\">\n {{ object.name }}\n </ng-template>\n <ng-template\n fsAutocompleteChipsStatic\n (click)=\"createReport()\">\n Create Report\n </ng-template>\n </fs-autocomplete-chips>\n @if (report() && editMode()) {\n <!-- What layout editing can add or switch to, and the way out of it \u2014\n the same outlined buttons as the fs-ai-report toolbar. -->\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"master-toggle\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n }\n @if (report()) {\n <fs-menu>\n <ng-template\n fs-menu-item\n (click)=\"reportSettings()\">\n <mat-icon>\n tune\n </mat-icon>\n Report settings\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"toggleEditMode()\"\n [disabled]=\"editMode()\">\n <mat-icon>\n dashboard_customize\n </mat-icon>\n {{ editMode() ? 'Done editing layout' : 'Edit layout' }}\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n <mat-icon>\n slideshow\n </mat-icon>\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n <mat-icon>\n picture_as_pdf\n </mat-icon>\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n <mat-icon>\n table_view\n </mat-icon>\n Export Raw Data\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n <mat-icon>\n table_view\n </mat-icon>\n Export Raw Data (This Page)\n </ng-template>\n }\n </fs-menu>\n }\n </div>\n @if (report()) {\n <!-- Report-level filters only (the report's actions live in the menu\n above). fs-filter reads its config once at init, so it's keyed on the\n filter signature: when the report-level filter set changes the block\n is recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters) {\n @for (key of [reportFilterKey]; track key) {\n <fs-filter [config]=\"reportFilterConfig()\"></fs-filter>\n }\n }\n <app-report-canvas\n [report]=\"report()\"\n [editMode]=\"editMode()\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\">\n </app-report-canvas>\n }\n </div>\n</div>", styles: [".report{height:100%;min-height:500px}.report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:500px;min-width:0;border:none}.report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.report .viewer fs-filter{flex:0 0 auto;margin-top:10px;margin-bottom:0}.report .viewer ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.report .resizer:before{content:\"\";width:0px;background:#0000001f;transition:width .12s ease,background-color .12s ease}.report .resizer:hover:before{width:3px;background:var(--brand-primary-color)}.report.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.report.resizing .resizer:before{width:3px;background:var(--brand-primary-color)}.report.resizing .chat,.report.resizing .viewer{pointer-events:none}::ng-deep body.body-report-reports .mat-mdc-card-content{display:flex;flex-direction:column;box-sizing:border-box}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel{flex:1;min-height:0}::ng-deep body.body-report-reports .mat-mdc-card-content mat-tab-nav-panel router-outlet+ng-component{height:100%}.master-toggle.active{background:#eceff3}\n"], dependencies: [{ kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: FsAutocompleteChipsModule }, { kind: "component", type: i2$2.FsAutocompleteChipsComponent, selector: "fs-autocomplete-chips", inputs: ["fetch", "appearance", "floatLabel", "readonly", "size", "label", "placeholder", "chipImage", "chipBackground", "chipColor", "chipIcon", "chipIconColor", "chipClass", "chipPadding", "shape", "hint", "allowText", "allowObject", "delay", "minPanelWidth", "maxPanelHeight", "validateText", "removable", "allowClear", "color", "background", "orderable", "padless", "initOnClick", "fetchOnFocus", "multiple", "multipleAdd", "confirm", "disabled", "groupBy", "panelWidth", "panelClass", "compareWith"], outputs: ["selected", "removed", "reordered", "clear", "panelOpened", "panelClosed"] }, { kind: "directive", type: i2$2.FsAutocompleteObjectDirective, selector: "[fsAutocompleteObject],[fsAutocompleteChipsTemplate]" }, { kind: "directive", type: i2$2.FsAutocompleteChipsStaticDirective, selector: "[fsAutocompleteChipsStatic]", inputs: ["show", "disable"], outputs: ["click", "selected"] }, { kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i1$2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "ready", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "component", type: MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode", "pageId"], outputs: ["componentSettings", "reportChanged", "pageChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10767
10908
|
}
|
|
10768
10909
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: ReportComponent, decorators: [{
|
|
@@ -10777,7 +10918,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
10777
10918
|
ReportExportService,
|
|
10778
10919
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily with
|
|
10779
10920
|
// this route's chunk.
|
|
10780
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
10921
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CFnbJx44.mjs').then((module) => module.default) }),
|
|
10781
10922
|
], imports: [
|
|
10782
10923
|
FormsModule,
|
|
10783
10924
|
FsAutocompleteChipsModule,
|
|
@@ -12464,7 +12605,7 @@ class FsAiReportComponent {
|
|
|
12464
12605
|
ReportAiChatData,
|
|
12465
12606
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
12466
12607
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
12467
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
12608
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CFnbJx44.mjs').then((module) => module.default) }),
|
|
12468
12609
|
], viewQueries: [{ propertyName: "_split", first: true, predicate: ["split"], descendants: true, static: true }, { propertyName: "_chatPanel", first: true, predicate: ["chatPanel"], descendants: true, read: ElementRef, static: true }, { propertyName: "_canvas", first: true, predicate: ReportCanvasComponent, descendants: true }], ngImport: i0, template: "<div class=\"fs-ai-report fs-column\">\n\n <div class=\"fs-ai-report__header fs-row.align-center\">\n <h2 class=\"fs-ai-report__title fs-flex\">{{ report()?.name }}</h2>\n\n @if (report()) {\n <!-- Report-level filters, inline with the toolbar actions. fs-filter\n reads its config once at init, so it's keyed on the filter\n signature: when the report-level filter set changes the block is\n recreated and re-reads the rebuilt config. -->\n @if (reportHasFilters()) {\n @for (key of [reportFilterKey()]; track key) {\n <fs-filter\n class=\"fs-ai-report__filters\"\n [config]=\"reportFilterConfig()\">\n </fs-filter>\n }\n }\n\n <!-- What layout editing can add or switch to, and the way out of it.\n One style for all three \u2014 outlined, icon and text. \"Header & footer\"\n is a toggle: it keeps its label and turns gray while it is on. -->\n @if (editMode()) {\n <div class=\"fs-ai-report__edit-tools fs-row.align-center.gap-xs\">\n <button\n mat-stroked-button\n type=\"button\"\n matTooltip=\"Add a block of text\"\n (click)=\"addText()\">\n <mat-icon>title</mat-icon>\n Add text\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n class=\"fs-ai-report__master\"\n [class.active]=\"isMasterMode\"\n [matTooltip]=\"isMasterMode ? 'Back to editing this page' : 'Edit the header and footer shown on every page'\"\n (click)=\"toggleMasterMode()\">\n <mat-icon>web_asset</mat-icon>\n Header & footer\n </button>\n <button\n mat-stroked-button\n type=\"button\"\n (click)=\"toggleEditMode()\">\n <mat-icon>check</mat-icon>\n Done editing\n </button>\n </div>\n }\n\n <!-- The zone every date in this report is filtered and grouped in, the\n same for every viewer. Shown only when the server sends it; a click\n opens Report settings, where it is changed. -->\n @if (report()?.effectiveTimezone; as effectiveTimezone) {\n <button\n mat-button\n type=\"button\"\n class=\"fs-ai-report__timezone\"\n [matTooltip]=\"'Dates are filtered and grouped in ' + effectiveTimezone + ' for everyone. Change it in Report settings.'\"\n (click)=\"reportSettings()\">\n <mat-icon>schedule</mat-icon>\n {{ timezoneLabel() }}\n </button>\n }\n\n <!-- One icon for both states: it reads as pressed while editing rather\n than swapping to something else. -->\n <button\n mat-icon-button\n type=\"button\"\n class=\"fs-ai-report__toggle\"\n [class.active]=\"editMode()\"\n [matTooltip]=\"editMode() ? 'Done editing' : 'Edit layout'\"\n (click)=\"toggleEditMode()\">\n <mat-icon>dashboard_customize</mat-icon>\n </button>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Export\"\n [fsMenuTriggerFor]=\"exportMenu\">\n <mat-icon>download</mat-icon>\n </button>\n <fs-menu #exportMenu>\n <ng-template\n fs-menu-item\n (click)=\"exportPowerpoint()\">\n Export PowerPoint\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportPdf()\">\n Export PDF\n </ng-template>\n <ng-template\n fs-menu-item\n (click)=\"exportData()\">\n Export Raw Data\n </ng-template>\n @if ((report()?.pages?.length ?? 0) > 1) {\n <ng-template\n fs-menu-item\n (click)=\"exportData(true)\">\n Export Raw Data (This Page)\n </ng-template>\n }\n </fs-menu>\n\n <button\n mat-icon-button\n type=\"button\"\n matTooltip=\"Report settings\"\n (click)=\"reportSettings()\">\n <mat-icon>settings</mat-icon>\n </button>\n }\n </div>\n\n <div\n #split\n class=\"fs-ai-report__split fs-row.align-start\">\n <fs-ai-chat\n #chatPanel\n class=\"chat\"\n [basePath]=\"basePath()\"\n [requestData]=\"chatRequestData()\"\n [ready]=\"chatPageId() !== null\"\n (response)=\"onChatResponse($event)\">\n </fs-ai-chat>\n\n <div\n class=\"resizer\"\n (pointerdown)=\"onResizeStart($event)\">\n </div>\n\n <div class=\"viewer fs-flex fs-column\">\n @if (report()) {\n <app-report-canvas\n [report]=\"report()\"\n [editMode]=\"editMode()\"\n [pageId]=\"pageId()\"\n (componentSettings)=\"componentSettings($event)\"\n (reportChanged)=\"onCanvasReportChanged()\"\n (pageChanged)=\"onCanvasPageChanged($event)\">\n </app-report-canvas>\n }\n </div>\n </div>\n</div>\n", styles: [".fs-ai-report{height:100%;min-height:500px;display:flex;flex-direction:column}.fs-ai-report__header{flex:0 0 auto;margin-bottom:10px}.fs-ai-report__title{margin:0;min-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.fs-ai-report__filters{flex:0 1 auto;min-width:0;display:block;margin:0 5px 0 0}.fs-ai-report__filters ::ng-deep .filter-bar-container{align-items:center!important}.fs-ai-report__filters ::ng-deep .mat-mdc-form-field-subscript-wrapper{display:none}.fs-ai-report__timezone{--mat-button-text-label-text-color: var(--fs-color-text-muted, light-dark(rgba(0, 0, 0, .54), rgba(255, 255, 255, .6)));color:var(--fs-color-text-muted, light-dark(rgba(0, 0, 0, .54), rgba(255, 255, 255, .6)));white-space:nowrap;flex:0 0 auto}.fs-ai-report__toggle.active{background:#e3ecf7;color:var(--brand-primary-color, #2196f3)}.fs-ai-report__master.active{background:#eceff3}.fs-ai-report__edit-tools{margin:0 5px}.fs-ai-report__split{flex:1 1 auto;min-height:0}.fs-ai-report .chat{display:block;flex:0 0 25%;width:100%;height:100%;min-height:400px;min-width:0;border:none}.fs-ai-report .viewer{display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden}.fs-ai-report .resizer{flex:0 0 11px;align-self:stretch;display:flex;justify-content:center;cursor:col-resize;touch-action:none;-webkit-user-select:none;user-select:none}.fs-ai-report .resizer:before{content:\"\";width:3px;border-radius:3px;background:transparent;transition:background-color .12s ease}.fs-ai-report .resizer:hover:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing{cursor:col-resize;-webkit-user-select:none;user-select:none}.fs-ai-report__split.resizing .resizer:before{background:var(--brand-primary-color)}.fs-ai-report__split.resizing .chat,.fs-ai-report__split.resizing .viewer{pointer-events:none}\n"], dependencies: [{ kind: "ngmodule", type: FsFilterModule }, { kind: "component", type: i1$2.FilterComponent, selector: "fs-filter", inputs: ["config"], outputs: ["closed", "opened", "ready"] }, { kind: "ngmodule", type: FsMenuModule }, { kind: "component", type: i2.FsMenuComponent, selector: "fs-menu", inputs: ["class", "buttonClass", "buttonType", "buttonColor"], outputs: ["opened", "closed"] }, { kind: "directive", type: i2.FsMenuItemDirective, selector: "fs-menu-group,[fs-menu-item]" }, { kind: "directive", type: i2.FsMenuTriggerDirective, selector: "[fsMenuTriggerFor]", inputs: ["fsMenuTriggerFor"] }, { kind: "component", type: FsAiChatComponent, selector: "fs-ai-chat", inputs: ["basePath", "requestData", "ready", "introMessage", "showThinking"], outputs: ["response", "showThinkingChange"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$3.MatButton, selector: " button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button] ", exportAs: ["matButton"] }, { kind: "component", type: i3$3.MatIconButton, selector: "button[mat-icon-button]", exportAs: ["matButton"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i2$4.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type: ReportCanvasComponent, selector: "app-report-canvas", inputs: ["report", "editMode", "pageId"], outputs: ["componentSettings", "reportChanged", "pageChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
12469
12610
|
}
|
|
12470
12611
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FsAiReportComponent, decorators: [{
|
|
@@ -12482,7 +12623,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
12482
12623
|
ReportAiChatData,
|
|
12483
12624
|
{ provide: FsAiChatData, useExisting: ReportAiChatData },
|
|
12484
12625
|
// Tree-shaken ECharts core + the 'report' house theme, loaded lazily.
|
|
12485
|
-
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-
|
|
12626
|
+
provideEchartsCore({ echarts: () => import('./firestitch-report-echarts-CFnbJx44.mjs').then((module) => module.default) }),
|
|
12486
12627
|
], imports: [
|
|
12487
12628
|
FsFilterModule,
|
|
12488
12629
|
FsMenuModule,
|
|
@@ -12516,4 +12657,4 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
12516
12657
|
*/
|
|
12517
12658
|
|
|
12518
12659
|
export { FsAiReportsComponent as F, GRANULARITY_OPTIONS as G, REPORT_CHART_COLORS_CSS as R, REPORT_CHART_FONT_FAMILY as a, REPORT_AXIS_LABEL_FONT_SIZE as b, ReportComponent as c, FsAiReportComponent as d, ReportData as e, ReportService as f, ReportFilterStateService as g, ReportNodeType as h, ReportState as i, ReportValueFormat as j, ReportListColumnFormat as k, ReportNumberUnits as l, ReportChartDataLabels as m, ReportChartColor as n, ReportChartLineStyle as o, ReportChartMarkStat as p, ReportChartDateLabel as q, ReportChangePolarity as r };
|
|
12519
|
-
//# sourceMappingURL=firestitch-report-firestitch-report-
|
|
12660
|
+
//# sourceMappingURL=firestitch-report-firestitch-report-CSylkyko.mjs.map
|