@adecore/agents-react 0.22.0 → 0.22.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/dist/chat/ui/intelligent-ui/UiBlockFrame.js +3 -3
- package/dist/chat/ui/intelligent-ui/UiFallbackPart.js +1 -1
- package/dist/chat/ui/intelligent-ui/chart-data.d.ts +22 -2
- package/dist/chat/ui/intelligent-ui/chart-data.js +83 -19
- package/dist/chat/ui/intelligent-ui/renderers/chart.d.ts +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/chart.js +99 -35
- package/dist/chat/ui/intelligent-ui/renderers/choices.d.ts +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/choices.js +8 -2
- package/dist/chat/ui/intelligent-ui/renderers/content.js +5 -3
- package/dist/chat/ui/intelligent-ui/renderers/inputs.js +9 -4
- package/dist/chat/ui/intelligent-ui/renderers/links.js +1 -1
- package/dist/chat/ui/intelligent-ui/renderers/text.js +5 -2
- package/dist/chat/ui/intelligent-ui/table-data.js +4 -4
- package/dist/chat/ui/intelligent-ui/use-truncated.d.ts +1 -0
- package/dist/chat/ui/intelligent-ui/use-truncated.js +28 -0
- package/dist/locales/en/agent-chat.json +9 -3
- package/dist/locales/nl/agent-chat.json +9 -3
- package/package.json +9 -4
- package/src/chat/ui/intelligent-ui/UiBlockFrame.tsx +5 -5
- package/src/chat/ui/intelligent-ui/UiFallbackPart.tsx +6 -1
- package/src/chat/ui/intelligent-ui/chart-data.ts +106 -20
- package/src/chat/ui/intelligent-ui/renderers/chart.tsx +237 -72
- package/src/chat/ui/intelligent-ui/renderers/choices.tsx +9 -4
- package/src/chat/ui/intelligent-ui/renderers/content.tsx +31 -15
- package/src/chat/ui/intelligent-ui/renderers/inputs.tsx +16 -5
- package/src/chat/ui/intelligent-ui/renderers/links.tsx +3 -3
- package/src/chat/ui/intelligent-ui/renderers/text.tsx +18 -8
- package/src/chat/ui/intelligent-ui/table-data.ts +4 -4
- package/src/chat/ui/intelligent-ui/use-truncated.ts +33 -0
- package/src/locales/en/agent-chat.json +9 -3
- package/src/locales/nl/agent-chat.json +9 -3
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { uiChildrenOf } from './node-text.js';
|
|
2
2
|
/* Rows shown before a person asks for all of them. */
|
|
3
3
|
export const UI_TABLE_ROWS = 50;
|
|
4
|
-
/*
|
|
5
|
-
const
|
|
4
|
+
/* The furthest a JavaScript Date reaches from the epoch either way, in milliseconds. */
|
|
5
|
+
const DATE_RANGE = 8.64e15;
|
|
6
6
|
const NUMERIC_KINDS = new Set(['number', 'bytes', 'duration']);
|
|
7
7
|
export function isNumericColumn(column) {
|
|
8
8
|
return NUMERIC_KINDS.has(column.as);
|
|
@@ -29,7 +29,7 @@ export function uiTableColumns(node) {
|
|
|
29
29
|
const keys = [];
|
|
30
30
|
for (const row of rows) {
|
|
31
31
|
for (const key of Object.keys(row)) {
|
|
32
|
-
if (!keys.includes(key)
|
|
32
|
+
if (!keys.includes(key)) {
|
|
33
33
|
keys.push(key);
|
|
34
34
|
}
|
|
35
35
|
}
|
|
@@ -67,7 +67,7 @@ export function uiTableCell(value, column) {
|
|
|
67
67
|
}
|
|
68
68
|
case 'date': {
|
|
69
69
|
const at = typeof value === 'number' ? value : typeof value === 'string' ? Date.parse(value) : Number.NaN;
|
|
70
|
-
return Number.isFinite(at) ? { kind: 'date', at } : { kind: 'text', text: textOf(value) };
|
|
70
|
+
return Number.isFinite(at) && Math.abs(at) <= DATE_RANGE ? { kind: 'date', at } : { kind: 'text', text: textOf(value) };
|
|
71
71
|
}
|
|
72
72
|
case 'file':
|
|
73
73
|
return typeof value === 'string' ? { kind: 'file', path: value } : { kind: 'text', text: textOf(value) };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useTruncated<T extends HTMLElement>(within?: boolean): [(element: T | null) => (() => void) | undefined, boolean];
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
function cut(element) {
|
|
3
|
+
return element.scrollWidth > element.clientWidth + 1;
|
|
4
|
+
}
|
|
5
|
+
/*
|
|
6
|
+
* Whether an element's text is cut short where it stands, for a tooltip or a list of values that shows
|
|
7
|
+
* it whole exactly then. `within` asks the same of every `.truncate` element inside it instead. Measured
|
|
8
|
+
* again when the box resizes or the text changes, since neither moves the other.
|
|
9
|
+
*/
|
|
10
|
+
export function useTruncated(within = false) {
|
|
11
|
+
const [truncated, setTruncated] = useState(false);
|
|
12
|
+
const measure = useCallback((element) => {
|
|
13
|
+
if (element === null || typeof ResizeObserver === 'undefined') {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const check = () => setTruncated(within ? [...element.querySelectorAll('.truncate')].some(cut) : cut(element));
|
|
17
|
+
check();
|
|
18
|
+
const resize = new ResizeObserver(check);
|
|
19
|
+
resize.observe(element);
|
|
20
|
+
const text = new MutationObserver(check);
|
|
21
|
+
text.observe(element, { characterData: true, childList: true, subtree: true });
|
|
22
|
+
return () => {
|
|
23
|
+
resize.disconnect();
|
|
24
|
+
text.disconnect();
|
|
25
|
+
};
|
|
26
|
+
}, [within]);
|
|
27
|
+
return [measure, truncated];
|
|
28
|
+
}
|
|
@@ -527,7 +527,13 @@
|
|
|
527
527
|
"chart": {
|
|
528
528
|
"values": "Values",
|
|
529
529
|
"summary": "{{count}} values, highest {{label}} at {{value}}",
|
|
530
|
-
"summarySeries": "{{count}} categories, highest {{series}} for {{label}} at {{value}}"
|
|
530
|
+
"summarySeries": "{{count}} categories, highest {{series}} for {{label}} at {{value}}",
|
|
531
|
+
"rowsLeftOut": "The first {{shown}} of {{total}} rows are drawn.",
|
|
532
|
+
"seriesLeftOut_one": "{{series}} is not drawn.",
|
|
533
|
+
"seriesLeftOut_other": "{{series}} are not drawn.",
|
|
534
|
+
"sideBySide": "Negative values cannot be stacked, so the series stand side by side.",
|
|
535
|
+
"showValues": "Show all values",
|
|
536
|
+
"hideValues": "Hide values"
|
|
531
537
|
},
|
|
532
538
|
"image": {
|
|
533
539
|
"label": "Image",
|
|
@@ -551,8 +557,8 @@
|
|
|
551
557
|
"answered": "Answered with “{{label}}”",
|
|
552
558
|
"chose": "Chose “{{label}}”",
|
|
553
559
|
"choseOlder": "Chose “{{label}}” on the reply of {{time}}",
|
|
554
|
-
"
|
|
555
|
-
"
|
|
560
|
+
"notes_one": "{{count}} note",
|
|
561
|
+
"notes_other": "{{count}} notes",
|
|
556
562
|
"shownAsText": "Shown as text",
|
|
557
563
|
"unknownComponent": "This version cannot draw {{component}}",
|
|
558
564
|
"renderFailed": "Could not draw this part",
|
|
@@ -527,7 +527,13 @@
|
|
|
527
527
|
"chart": {
|
|
528
528
|
"values": "Waarden",
|
|
529
529
|
"summary": "{{count}} waarden, hoogste {{label}} met {{value}}",
|
|
530
|
-
"summarySeries": "{{count}} categorieën, hoogste {{series}} bij {{label}} met {{value}}"
|
|
530
|
+
"summarySeries": "{{count}} categorieën, hoogste {{series}} bij {{label}} met {{value}}",
|
|
531
|
+
"rowsLeftOut": "De eerste {{shown}} van {{total}} rijen zijn getekend.",
|
|
532
|
+
"seriesLeftOut_one": "{{series}} is niet getekend.",
|
|
533
|
+
"seriesLeftOut_other": "{{series}} zijn niet getekend.",
|
|
534
|
+
"sideBySide": "Negatieve waarden kunnen niet gestapeld worden, dus de reeksen staan naast elkaar.",
|
|
535
|
+
"showValues": "Alle waarden tonen",
|
|
536
|
+
"hideValues": "Waarden verbergen"
|
|
531
537
|
},
|
|
532
538
|
"image": {
|
|
533
539
|
"label": "Afbeelding",
|
|
@@ -551,8 +557,8 @@
|
|
|
551
557
|
"answered": "Beantwoord met “{{label}}”",
|
|
552
558
|
"chose": "Gekozen: “{{label}}”",
|
|
553
559
|
"choseOlder": "Gekozen: “{{label}}”, bij het antwoord van {{time}}",
|
|
554
|
-
"
|
|
555
|
-
"
|
|
560
|
+
"notes_one": "{{count}} opmerking",
|
|
561
|
+
"notes_other": "{{count}} opmerkingen",
|
|
556
562
|
"shownAsText": "Als tekst getoond",
|
|
557
563
|
"unknownComponent": "Deze versie kan {{component}} niet tekenen",
|
|
558
564
|
"renderFailed": "Dit deel kon niet getekend worden",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/agents-react",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"description": "React agent chat threads, composers, approvals, provider accounts and usage views.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -639,6 +639,11 @@
|
|
|
639
639
|
"types": "./dist/chat/ui/intelligent-ui/use-later.d.ts",
|
|
640
640
|
"default": "./dist/chat/ui/intelligent-ui/use-later.js"
|
|
641
641
|
},
|
|
642
|
+
"./chat/ui/intelligent-ui/use-truncated": {
|
|
643
|
+
"source": "./src/chat/ui/intelligent-ui/use-truncated.ts",
|
|
644
|
+
"types": "./dist/chat/ui/intelligent-ui/use-truncated.d.ts",
|
|
645
|
+
"default": "./dist/chat/ui/intelligent-ui/use-truncated.js"
|
|
646
|
+
},
|
|
642
647
|
"./chat/ui/intelligent-ui/use-ui-links": {
|
|
643
648
|
"source": "./src/chat/ui/intelligent-ui/use-ui-links.ts",
|
|
644
649
|
"types": "./dist/chat/ui/intelligent-ui/use-ui-links.d.ts",
|
|
@@ -1031,8 +1036,8 @@
|
|
|
1031
1036
|
"test:visuals": "bun --config=integration.bunfig.toml --conditions=source test ./src/chat/visual-viewport.integration.test.ts"
|
|
1032
1037
|
},
|
|
1033
1038
|
"dependencies": {
|
|
1034
|
-
"@adecore/agent-contracts": "0.22.
|
|
1035
|
-
"@adecore/intelligent-ui": "0.22.
|
|
1039
|
+
"@adecore/agent-contracts": "0.22.1",
|
|
1040
|
+
"@adecore/intelligent-ui": "0.22.1",
|
|
1036
1041
|
"@codemirror/commands": "^6.11.1",
|
|
1037
1042
|
"@codemirror/language": "^6.12.4",
|
|
1038
1043
|
"@codemirror/state": "^6.7.6",
|
|
@@ -1053,7 +1058,7 @@
|
|
|
1053
1058
|
"zustand": "^5.0.15"
|
|
1054
1059
|
},
|
|
1055
1060
|
"peerDependencies": {
|
|
1056
|
-
"@adecore/ui": "^0.22.
|
|
1061
|
+
"@adecore/ui": "^0.22.1",
|
|
1057
1062
|
"i18next": "^26.4.2",
|
|
1058
1063
|
"react": "^19.3.0",
|
|
1059
1064
|
"react-dom": "^19.3.0",
|
|
@@ -2,14 +2,14 @@ import { useId, useLayoutEffect, useRef, useState, type ReactNode, type RefObjec
|
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import type { UiViewNode } from '@adecore/intelligent-ui';
|
|
5
|
-
import { Lock, Radio, TriangleAlert
|
|
5
|
+
import { Info, Lock, Radio, TriangleAlert } from 'lucide-react';
|
|
6
6
|
import { Icon, Skeleton, Spinner, Tooltip, useTickingText } from '@adecore/ui';
|
|
7
7
|
import { formatAgo, formatMoment } from '@adecore/ui/format';
|
|
8
8
|
import { uiBlockHead, uiReasonText } from './node-text';
|
|
9
9
|
import { UiHeadContext, type UiLiveStatus } from './render-context';
|
|
10
10
|
import { useLater } from './use-later';
|
|
11
11
|
|
|
12
|
-
/* Something the compiler
|
|
12
|
+
/* Something the compiler noted about a block: a prop it dropped, or a part it could only draw as text. */
|
|
13
13
|
export interface UiBlockFix {
|
|
14
14
|
code: string;
|
|
15
15
|
message: string;
|
|
@@ -82,7 +82,7 @@ function LivePart({ live }: { live: UiLiveStatus }) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/*
|
|
85
|
-
* The line under a block, only when it has something to say: live, answered,
|
|
85
|
+
* The line under a block, only when it has something to say: live, answered, noted or drawn as
|
|
86
86
|
* text, in that order. A hairline of the soft border runs the full width of the card over it.
|
|
87
87
|
*/
|
|
88
88
|
function UiBlockFooter({ live = null, answered = null, fixes = NO_FIXES, shownAsText = null }: UiBlockFooterProps) {
|
|
@@ -111,8 +111,8 @@ function UiBlockFooter({ live = null, answered = null, fixes = NO_FIXES, shownAs
|
|
|
111
111
|
className="flex items-center gap-1.5 rounded-sm hover:text-text-muted"
|
|
112
112
|
onClick={() => setFixesOpen(!fixesOpen)}
|
|
113
113
|
>
|
|
114
|
-
<Icon icon={
|
|
115
|
-
{t('blocks.
|
|
114
|
+
<Icon icon={Info} size={12} />
|
|
115
|
+
{t('blocks.notes', { count: fixes.length })}
|
|
116
116
|
</button>
|
|
117
117
|
)}
|
|
118
118
|
{shownAsText !== null && (
|
|
@@ -15,7 +15,12 @@ export function UiFallbackPart({ fallback, problem }: { fallback: string; proble
|
|
|
15
15
|
<span className="text-xs text-text-faint">
|
|
16
16
|
{problem.kind === 'unknown' ? t('blocks.unknownComponent', { component: problem.component }) : t('blocks.renderFailed')}
|
|
17
17
|
</span>
|
|
18
|
-
{fallback !== '' &&
|
|
18
|
+
{fallback !== '' && (
|
|
19
|
+
// A fallback is often data without a space in it, such as the JSON of a chart.
|
|
20
|
+
<div className="min-w-0 wrap-anywhere">
|
|
21
|
+
<Markdown text={fallback} fileLinks={false} />
|
|
22
|
+
</div>
|
|
23
|
+
)}
|
|
19
24
|
</div>
|
|
20
25
|
);
|
|
21
26
|
}
|
|
@@ -5,55 +5,141 @@ export const UI_CHART_SERIES = 6;
|
|
|
5
5
|
/* Categories a chart draws; more would not fit a column of a thread. */
|
|
6
6
|
export const UI_CHART_ROWS = 60;
|
|
7
7
|
|
|
8
|
+
type ChartRow = UiProps<'Chart'>['data'][number];
|
|
9
|
+
type ChartKind = UiProps<'Chart'>['kind'];
|
|
10
|
+
|
|
8
11
|
export interface UiChartSeries {
|
|
9
12
|
key: string;
|
|
10
13
|
/* One per label, null where a row has no number for this series. */
|
|
11
14
|
values: (number | null)[];
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
|
|
17
|
+
/* Every row and every numeric series of a chart's data, whatever a drawing can hold. */
|
|
18
|
+
export interface UiChartValues {
|
|
15
19
|
labels: string[];
|
|
16
20
|
series: UiChartSeries[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UiChartData extends UiChartValues {
|
|
24
|
+
/* What it is drawn as: stacked data with a negative value stands side by side as bars. */
|
|
25
|
+
kind: ChartKind;
|
|
17
26
|
/* The range the axis covers, from zero or below it, never empty. */
|
|
18
27
|
min: number;
|
|
19
28
|
max: number;
|
|
20
29
|
/* Whether rows past `UI_CHART_ROWS` were left out. */
|
|
21
30
|
truncated: boolean;
|
|
31
|
+
/* The rows of the data, drawn or not. */
|
|
32
|
+
total: number;
|
|
33
|
+
/* The numeric series past the first six, which only the values hold. */
|
|
34
|
+
omitted: string[];
|
|
22
35
|
}
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
* The series a chart can draw: every key besides `label` with a finite number in some row, the
|
|
26
|
-
* first six of them. A value that is missing or no finite number is a gap. Null when there is no
|
|
27
|
-
* row or no series, which draws the fallback.
|
|
28
|
-
*/
|
|
29
|
-
export function uiChartData(data: UiProps<'Chart'>['data'], kind: UiProps<'Chart'>['kind']): UiChartData | null {
|
|
30
|
-
const rows = data.slice(0, UI_CHART_ROWS);
|
|
37
|
+
function numericKeys(rows: readonly ChartRow[]): string[] {
|
|
31
38
|
const keys: string[] = [];
|
|
32
39
|
for (const row of rows) {
|
|
33
40
|
for (const [key, value] of Object.entries(row)) {
|
|
34
|
-
if (key !== 'label' && !keys.includes(key) &&
|
|
41
|
+
if (key !== 'label' && !keys.includes(key) && typeof value === 'number' && Number.isFinite(value)) {
|
|
35
42
|
keys.push(key);
|
|
36
43
|
}
|
|
37
44
|
}
|
|
38
45
|
}
|
|
46
|
+
return keys;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function valuesOf(rows: readonly ChartRow[], keys: readonly string[]): UiChartValues {
|
|
50
|
+
return {
|
|
51
|
+
labels: rows.map((row) => (typeof row.label === 'string' || typeof row.label === 'number' ? String(row.label) : '')),
|
|
52
|
+
series: keys.map((key) => ({
|
|
53
|
+
key,
|
|
54
|
+
values: rows.map((row) => {
|
|
55
|
+
const value = row[key];
|
|
56
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
57
|
+
})
|
|
58
|
+
}))
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* All of a chart's numbers, for the table a person can open beside a drawing that left some out. */
|
|
63
|
+
export function uiChartValues(data: UiProps<'Chart'>['data']): UiChartValues {
|
|
64
|
+
return valuesOf(data, numericKeys(data));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* The rounded ends of a value axis, or null when no drawing can scale to them: the span has to be
|
|
69
|
+
* finite and above zero, and so does one over it, which rules out a subnormal span as well.
|
|
70
|
+
*/
|
|
71
|
+
export function uiChartRange(min: number, max: number): { low: number; high: number } | null {
|
|
72
|
+
const low = min < 0 ? -niceCeiling(-min) : 0;
|
|
73
|
+
const high = max > 0 ? niceCeiling(max) : low === 0 ? 1 : 0;
|
|
74
|
+
const span = high - low;
|
|
75
|
+
return span > 0 && Number.isFinite(span) && Number.isFinite(1 / span) ? { low, high } : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/*
|
|
79
|
+
* The series a chart can draw: every key besides `label` with a finite number in some row, the
|
|
80
|
+
* first six of them. A value that is missing or no finite number is a gap. Null when there is no
|
|
81
|
+
* row or no series, or when the numbers are too large to scale, which draws the fallback.
|
|
82
|
+
*/
|
|
83
|
+
export function uiChartData(data: UiProps<'Chart'>['data'], kind: ChartKind): UiChartData | null {
|
|
84
|
+
const rows = data.slice(0, UI_CHART_ROWS);
|
|
85
|
+
const keys = numericKeys(rows);
|
|
39
86
|
if (rows.length === 0 || keys.length === 0) {
|
|
40
87
|
return null;
|
|
41
88
|
}
|
|
42
|
-
const series = keys.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const value = row[key];
|
|
46
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
47
|
-
})
|
|
48
|
-
}));
|
|
49
|
-
const labels = rows.map((row) => (typeof row.label === 'string' || typeof row.label === 'number' ? String(row.label) : ''));
|
|
89
|
+
const { labels, series } = valuesOf(rows, keys.slice(0, UI_CHART_SERIES));
|
|
90
|
+
const negative = series.some(({ values }) => values.some((value) => value !== null && value < 0));
|
|
91
|
+
const drawn = kind === 'stacked' && negative ? 'bar' : kind;
|
|
50
92
|
const extents =
|
|
51
|
-
|
|
52
|
-
? labels.map((_, index) => series.reduce((sum, { values }) => sum +
|
|
93
|
+
drawn === 'stacked'
|
|
94
|
+
? labels.map((_, index) => series.reduce((sum, { values }) => sum + (values[index] ?? 0), 0))
|
|
53
95
|
: series.flatMap(({ values }) => values.filter((value) => value !== null));
|
|
54
96
|
const min = Math.min(0, ...extents);
|
|
55
97
|
const max = Math.max(0, ...extents);
|
|
56
|
-
|
|
98
|
+
if (uiChartRange(min, max) === null) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
labels,
|
|
103
|
+
series,
|
|
104
|
+
kind: drawn,
|
|
105
|
+
min,
|
|
106
|
+
max: max === min ? min + 1 : max,
|
|
107
|
+
truncated: data.length > rows.length,
|
|
108
|
+
total: data.length,
|
|
109
|
+
omitted: numericKeys(data).filter((key) => !series.some((drawnSeries) => drawnSeries.key === key))
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* The width of one character of a chart's type at a 16px root, an estimate since SVG text cannot wrap or be measured before it draws. */
|
|
114
|
+
export const UI_CHART_CHARACTER = 7.5;
|
|
115
|
+
/* The narrowest a value axis is, and the gap between a tick label and the plot. */
|
|
116
|
+
const AXIS_WIDTH = 44;
|
|
117
|
+
const AXIS_GAP = 8;
|
|
118
|
+
|
|
119
|
+
/* The character width for a root font size, since chart type is set in rem and follows a person's larger text. */
|
|
120
|
+
export function uiChartCharacter(rootFontSize: number): number {
|
|
121
|
+
return Number.isFinite(rootFontSize) && rootFontSize > 0 ? (UI_CHART_CHARACTER * rootFontSize) / 16 : UI_CHART_CHARACTER;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function uiChartTextWidth(text: string, character = UI_CHART_CHARACTER): number {
|
|
125
|
+
return Math.ceil(text.length * character);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Text cut to the room it has, so a label never reaches past its place; the whole of it is in the values. */
|
|
129
|
+
export function uiChartText(text: string, room: number, character = UI_CHART_CHARACTER): string {
|
|
130
|
+
const fit = Math.floor(room / character);
|
|
131
|
+
return text.length <= fit ? text : `${text.slice(0, Math.max(1, fit - 1))}…`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* A value axis as wide as its longest tick label, at most two fifths of the chart; a label longer than that is cut. */
|
|
135
|
+
export function uiChartAxis(ticks: readonly string[], width: number, character = UI_CHART_CHARACTER): { axisWidth: number; tickTexts: string[] } {
|
|
136
|
+
const scale = character / UI_CHART_CHARACTER;
|
|
137
|
+
const room = Math.max(AXIS_WIDTH * scale, Math.floor(width * 0.4));
|
|
138
|
+
const tickTexts = ticks.map((tick) => uiChartText(tick, room - AXIS_GAP, character));
|
|
139
|
+
return {
|
|
140
|
+
axisWidth: Math.round(Math.min(room, Math.max(AXIS_WIDTH * scale, ...tickTexts.map((text) => uiChartTextWidth(text, character) + AXIS_GAP)))),
|
|
141
|
+
tickTexts
|
|
142
|
+
};
|
|
57
143
|
}
|
|
58
144
|
|
|
59
145
|
/* A round number at or above `value`, for the top of an axis: 1, 2, 2.5 or 5 times a power of ten. */
|