@opendata-ai/openchart-engine 1.2.0
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/index.d.ts +366 -0
- package/dist/index.js +4227 -0
- package/dist/index.js.map +1 -0
- package/package.json +62 -0
- package/src/__test-fixtures__/specs.ts +124 -0
- package/src/__tests__/axes.test.ts +114 -0
- package/src/__tests__/compile-chart.test.ts +337 -0
- package/src/__tests__/dimensions.test.ts +151 -0
- package/src/__tests__/legend.test.ts +113 -0
- package/src/__tests__/scales.test.ts +109 -0
- package/src/annotations/__tests__/compute.test.ts +454 -0
- package/src/annotations/compute.ts +603 -0
- package/src/charts/__tests__/registry.test.ts +110 -0
- package/src/charts/bar/__tests__/compute.test.ts +294 -0
- package/src/charts/bar/__tests__/labels.test.ts +75 -0
- package/src/charts/bar/compute.ts +205 -0
- package/src/charts/bar/index.ts +33 -0
- package/src/charts/bar/labels.ts +132 -0
- package/src/charts/column/__tests__/compute.test.ts +277 -0
- package/src/charts/column/compute.ts +282 -0
- package/src/charts/column/index.ts +33 -0
- package/src/charts/column/labels.ts +108 -0
- package/src/charts/dot/__tests__/compute.test.ts +344 -0
- package/src/charts/dot/compute.ts +257 -0
- package/src/charts/dot/index.ts +46 -0
- package/src/charts/dot/labels.ts +97 -0
- package/src/charts/line/__tests__/compute.test.ts +437 -0
- package/src/charts/line/__tests__/labels.test.ts +93 -0
- package/src/charts/line/area.ts +288 -0
- package/src/charts/line/compute.ts +177 -0
- package/src/charts/line/index.ts +68 -0
- package/src/charts/line/labels.ts +144 -0
- package/src/charts/pie/__tests__/compute.test.ts +276 -0
- package/src/charts/pie/compute.ts +234 -0
- package/src/charts/pie/index.ts +49 -0
- package/src/charts/pie/labels.ts +142 -0
- package/src/charts/registry.ts +64 -0
- package/src/charts/scatter/__tests__/compute.test.ts +304 -0
- package/src/charts/scatter/__tests__/trendline.test.ts +191 -0
- package/src/charts/scatter/compute.ts +124 -0
- package/src/charts/scatter/index.ts +41 -0
- package/src/charts/scatter/trendline.ts +100 -0
- package/src/charts/utils.ts +120 -0
- package/src/compile.ts +368 -0
- package/src/compiler/__tests__/compile.test.ts +87 -0
- package/src/compiler/__tests__/normalize.test.ts +210 -0
- package/src/compiler/__tests__/validate.test.ts +440 -0
- package/src/compiler/index.ts +47 -0
- package/src/compiler/normalize.ts +269 -0
- package/src/compiler/types.ts +148 -0
- package/src/compiler/validate.ts +581 -0
- package/src/graphs/__tests__/community.test.ts +228 -0
- package/src/graphs/__tests__/compile-graph.test.ts +315 -0
- package/src/graphs/__tests__/encoding.test.ts +314 -0
- package/src/graphs/community.ts +92 -0
- package/src/graphs/compile-graph.ts +291 -0
- package/src/graphs/encoding.ts +302 -0
- package/src/graphs/types.ts +98 -0
- package/src/index.ts +74 -0
- package/src/layout/axes.ts +194 -0
- package/src/layout/dimensions.ts +199 -0
- package/src/layout/gridlines.ts +84 -0
- package/src/layout/scales.ts +426 -0
- package/src/legend/compute.ts +186 -0
- package/src/tables/__tests__/bar-column.test.ts +147 -0
- package/src/tables/__tests__/category-colors.test.ts +153 -0
- package/src/tables/__tests__/compile-table.test.ts +208 -0
- package/src/tables/__tests__/format-cells.test.ts +126 -0
- package/src/tables/__tests__/heatmap.test.ts +124 -0
- package/src/tables/__tests__/pagination.test.ts +78 -0
- package/src/tables/__tests__/search.test.ts +94 -0
- package/src/tables/__tests__/sort.test.ts +107 -0
- package/src/tables/__tests__/sparkline.test.ts +122 -0
- package/src/tables/bar-column.ts +94 -0
- package/src/tables/category-colors.ts +67 -0
- package/src/tables/compile-table.ts +420 -0
- package/src/tables/format-cells.ts +110 -0
- package/src/tables/heatmap.ts +121 -0
- package/src/tables/pagination.ts +46 -0
- package/src/tables/search.ts +66 -0
- package/src/tables/sort.ts +69 -0
- package/src/tables/sparkline.ts +113 -0
- package/src/tables/utils.ts +16 -0
- package/src/tooltips/__tests__/compute.test.ts +328 -0
- package/src/tooltips/compute.ts +231 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Heatmap coloring for table columns.
|
|
3
|
+
*
|
|
4
|
+
* Colors cell backgrounds using sequential or diverging color scales,
|
|
5
|
+
* then picks an accessible text color for each background.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CellStyle, ColumnConfig, ResolvedTheme } from '@opendata-ai/openchart-core';
|
|
9
|
+
import { adaptColorForDarkMode } from '@opendata-ai/openchart-core';
|
|
10
|
+
import { interpolateRgb } from 'd3-interpolate';
|
|
11
|
+
import { scaleSequential } from 'd3-scale';
|
|
12
|
+
import { accessibleTextColor } from './utils';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build an interpolator from an array of color stops.
|
|
16
|
+
* Uses d3-interpolate for smooth color transitions.
|
|
17
|
+
*/
|
|
18
|
+
function interpolatorFromStops(stops: string[]): (t: number) => string {
|
|
19
|
+
if (stops.length === 0) return () => '#ffffff';
|
|
20
|
+
if (stops.length === 1) return () => stops[0];
|
|
21
|
+
|
|
22
|
+
return (t: number) => {
|
|
23
|
+
const clamped = Math.max(0, Math.min(1, t));
|
|
24
|
+
const segment = clamped * (stops.length - 1);
|
|
25
|
+
const lo = Math.floor(segment);
|
|
26
|
+
const hi = Math.min(lo + 1, stops.length - 1);
|
|
27
|
+
const frac = segment - lo;
|
|
28
|
+
return interpolateRgb(stops[lo], stops[hi])(frac);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve palette from column config or theme.
|
|
34
|
+
*
|
|
35
|
+
* - If palette is an array of color stops, use directly
|
|
36
|
+
* - If palette is a string name, look it up in theme sequential then diverging
|
|
37
|
+
* - If no palette specified, use the first sequential palette from the theme
|
|
38
|
+
*/
|
|
39
|
+
function resolvePalette(palette: string | string[] | undefined, theme: ResolvedTheme): string[] {
|
|
40
|
+
if (Array.isArray(palette)) return palette;
|
|
41
|
+
|
|
42
|
+
const seqPalettes = theme.colors.sequential;
|
|
43
|
+
const divPalettes = theme.colors.diverging;
|
|
44
|
+
|
|
45
|
+
if (typeof palette === 'string') {
|
|
46
|
+
if (seqPalettes[palette]) return seqPalettes[palette];
|
|
47
|
+
if (divPalettes[palette]) return divPalettes[palette];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Default: first sequential palette
|
|
51
|
+
const firstSeqKey = Object.keys(seqPalettes)[0];
|
|
52
|
+
return firstSeqKey ? seqPalettes[firstSeqKey] : ['#deebf7', '#08519c'];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Compute heatmap cell styles for a column.
|
|
57
|
+
*
|
|
58
|
+
* Returns a Map keyed by original data index with background and text colors.
|
|
59
|
+
*/
|
|
60
|
+
export function computeHeatmapColors(
|
|
61
|
+
data: Record<string, unknown>[],
|
|
62
|
+
column: ColumnConfig,
|
|
63
|
+
theme: ResolvedTheme,
|
|
64
|
+
darkMode: boolean,
|
|
65
|
+
): Map<number, CellStyle> {
|
|
66
|
+
const result = new Map<number, CellStyle>();
|
|
67
|
+
const config = column.heatmap;
|
|
68
|
+
if (!config) return result;
|
|
69
|
+
|
|
70
|
+
// Determine which field provides the color values
|
|
71
|
+
const colorField = config.colorByField ?? column.key;
|
|
72
|
+
|
|
73
|
+
// Extract numeric values and compute domain
|
|
74
|
+
const numericValues: { index: number; value: number }[] = [];
|
|
75
|
+
for (let i = 0; i < data.length; i++) {
|
|
76
|
+
const raw = data[i][colorField];
|
|
77
|
+
if (typeof raw === 'number' && Number.isFinite(raw)) {
|
|
78
|
+
numericValues.push({ index: i, value: raw });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (numericValues.length === 0) return result;
|
|
83
|
+
|
|
84
|
+
// Domain: from config or data min/max
|
|
85
|
+
let domain: [number, number];
|
|
86
|
+
if (config.domain) {
|
|
87
|
+
domain = config.domain;
|
|
88
|
+
} else {
|
|
89
|
+
let min = Infinity;
|
|
90
|
+
let max = -Infinity;
|
|
91
|
+
for (const { value } of numericValues) {
|
|
92
|
+
if (value < min) min = value;
|
|
93
|
+
if (value > max) max = value;
|
|
94
|
+
}
|
|
95
|
+
domain = [min, max];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Resolve palette and build scale
|
|
99
|
+
let stops = resolvePalette(config.palette, theme);
|
|
100
|
+
if (darkMode) {
|
|
101
|
+
const lightBg = '#ffffff';
|
|
102
|
+
const darkBg = theme.colors.background;
|
|
103
|
+
stops = stops.map((c) => adaptColorForDarkMode(c, lightBg, darkBg));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const interpolator = interpolatorFromStops(stops);
|
|
107
|
+
const scale = scaleSequential(interpolator).domain(domain).clamp(true);
|
|
108
|
+
|
|
109
|
+
// Apply to each row
|
|
110
|
+
for (const { index, value } of numericValues) {
|
|
111
|
+
const bg = scale(value);
|
|
112
|
+
const textColor = accessibleTextColor(bg);
|
|
113
|
+
|
|
114
|
+
result.set(index, {
|
|
115
|
+
backgroundColor: bg,
|
|
116
|
+
color: textColor,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table pagination: slice data into pages.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Paginate data rows.
|
|
7
|
+
*
|
|
8
|
+
* Returns the current page's rows along with pagination metadata.
|
|
9
|
+
* Page is 0-indexed and clamped to valid range.
|
|
10
|
+
* If pageSize is 0 or negative, pagination is disabled (returns all rows).
|
|
11
|
+
*/
|
|
12
|
+
export function paginateData(
|
|
13
|
+
data: Record<string, unknown>[],
|
|
14
|
+
page: number,
|
|
15
|
+
pageSize: number,
|
|
16
|
+
): {
|
|
17
|
+
rows: Record<string, unknown>[];
|
|
18
|
+
totalRows: number;
|
|
19
|
+
totalPages: number;
|
|
20
|
+
page: number;
|
|
21
|
+
} {
|
|
22
|
+
const totalRows = data.length;
|
|
23
|
+
|
|
24
|
+
// Disabled pagination
|
|
25
|
+
if (pageSize <= 0) {
|
|
26
|
+
return {
|
|
27
|
+
rows: data,
|
|
28
|
+
totalRows,
|
|
29
|
+
totalPages: 1,
|
|
30
|
+
page: 0,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const totalPages = Math.max(1, Math.ceil(totalRows / pageSize));
|
|
35
|
+
// Clamp page to valid range
|
|
36
|
+
const clampedPage = Math.max(0, Math.min(page, totalPages - 1));
|
|
37
|
+
const start = clampedPage * pageSize;
|
|
38
|
+
const end = Math.min(start + pageSize, totalRows);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
rows: data.slice(start, end),
|
|
42
|
+
totalRows,
|
|
43
|
+
totalPages,
|
|
44
|
+
page: clampedPage,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table search: build a search index and filter rows by query.
|
|
3
|
+
*
|
|
4
|
+
* The search index concatenates formatted cell values per row so
|
|
5
|
+
* substring matching works across all columns.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ColumnConfig } from '@opendata-ai/openchart-core';
|
|
9
|
+
import { formatValueForSearch } from './format-cells';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Build a search index mapping original data indices to searchable strings.
|
|
13
|
+
* Each row's searchable string is the concatenation of all column values,
|
|
14
|
+
* separated by spaces, lowercased.
|
|
15
|
+
*/
|
|
16
|
+
export function buildSearchIndex(
|
|
17
|
+
data: Record<string, unknown>[],
|
|
18
|
+
columns: ColumnConfig[],
|
|
19
|
+
): Map<number, string> {
|
|
20
|
+
const index = new Map<number, string>();
|
|
21
|
+
|
|
22
|
+
for (let i = 0; i < data.length; i++) {
|
|
23
|
+
const row = data[i];
|
|
24
|
+
const parts: string[] = [];
|
|
25
|
+
|
|
26
|
+
for (const col of columns) {
|
|
27
|
+
parts.push(formatValueForSearch(row[col.key], col));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
index.set(i, parts.join(' ').toLowerCase());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return index;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Filter data by a search query using the pre-built search index.
|
|
38
|
+
*
|
|
39
|
+
* Returns the filtered data and the original indices of matching rows.
|
|
40
|
+
* Empty query returns all data.
|
|
41
|
+
*/
|
|
42
|
+
export function filterBySearch(
|
|
43
|
+
data: Record<string, unknown>[],
|
|
44
|
+
query: string,
|
|
45
|
+
searchIndex: Map<number, string>,
|
|
46
|
+
originalIndices: number[],
|
|
47
|
+
): { data: Record<string, unknown>[]; indices: number[] } {
|
|
48
|
+
if (!query || query.trim() === '') {
|
|
49
|
+
return { data, indices: originalIndices };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const lowerQuery = query.toLowerCase();
|
|
53
|
+
const filteredData: Record<string, unknown>[] = [];
|
|
54
|
+
const filteredIndices: number[] = [];
|
|
55
|
+
|
|
56
|
+
for (let i = 0; i < data.length; i++) {
|
|
57
|
+
const originalIdx = originalIndices[i];
|
|
58
|
+
const searchText = searchIndex.get(originalIdx);
|
|
59
|
+
if (searchText?.includes(lowerQuery)) {
|
|
60
|
+
filteredData.push(data[i]);
|
|
61
|
+
filteredIndices.push(originalIdx);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { data: filteredData, indices: filteredIndices };
|
|
66
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable sort for table data.
|
|
3
|
+
*
|
|
4
|
+
* Sorts data by a column key with type-aware comparison:
|
|
5
|
+
* - Numbers: numeric comparison
|
|
6
|
+
* - Strings: localeCompare
|
|
7
|
+
* - Dates: timestamp comparison
|
|
8
|
+
* - Nulls: always sorted last regardless of direction
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { SortState } from '@opendata-ai/openchart-core';
|
|
12
|
+
|
|
13
|
+
/** Result of sorting: sorted data rows with their original indices preserved. */
|
|
14
|
+
export interface SortResult {
|
|
15
|
+
data: Record<string, unknown>[];
|
|
16
|
+
originalIndices: number[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Sort data rows by the specified column.
|
|
21
|
+
* Returns a new array (no mutation). Stable sort preserves
|
|
22
|
+
* original order for rows with equal values.
|
|
23
|
+
*
|
|
24
|
+
* Also returns the original indices so callers can track
|
|
25
|
+
* which row came from where (needed for heatmap/category color lookups).
|
|
26
|
+
*/
|
|
27
|
+
export function sortData(data: Record<string, unknown>[], sort: SortState): SortResult {
|
|
28
|
+
const { column, direction } = sort;
|
|
29
|
+
const multiplier = direction === 'asc' ? 1 : -1;
|
|
30
|
+
|
|
31
|
+
// Create index-value pairs for stable sort
|
|
32
|
+
const indexed = data.map((row, i) => ({ row, index: i }));
|
|
33
|
+
|
|
34
|
+
indexed.sort((a, b) => {
|
|
35
|
+
const aVal = a.row[column];
|
|
36
|
+
const bVal = b.row[column];
|
|
37
|
+
|
|
38
|
+
// Nulls always last
|
|
39
|
+
const aNull = aVal == null;
|
|
40
|
+
const bNull = bVal == null;
|
|
41
|
+
if (aNull && bNull) return a.index - b.index; // preserve order
|
|
42
|
+
if (aNull) return 1;
|
|
43
|
+
if (bNull) return -1;
|
|
44
|
+
|
|
45
|
+
let cmp = 0;
|
|
46
|
+
|
|
47
|
+
// Number comparison
|
|
48
|
+
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
49
|
+
cmp = aVal - bVal;
|
|
50
|
+
}
|
|
51
|
+
// Date comparison
|
|
52
|
+
else if (aVal instanceof Date && bVal instanceof Date) {
|
|
53
|
+
cmp = aVal.getTime() - bVal.getTime();
|
|
54
|
+
}
|
|
55
|
+
// String comparison (or mixed types)
|
|
56
|
+
else {
|
|
57
|
+
cmp = String(aVal).localeCompare(String(bVal));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Stable sort: fall back to original index for equal values
|
|
61
|
+
if (cmp === 0) return a.index - b.index;
|
|
62
|
+
return cmp * multiplier;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
data: indexed.map((item) => item.row),
|
|
67
|
+
originalIndices: indexed.map((item) => item.index),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sparkline computation for inline mini-charts in table cells.
|
|
3
|
+
*
|
|
4
|
+
* Produces normalized data points (0-1 range) for sparkline rendering.
|
|
5
|
+
* The adapter handles the actual drawing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
ResolvedTheme,
|
|
10
|
+
SparklineColumnConfig,
|
|
11
|
+
SparklineData,
|
|
12
|
+
} from '@opendata-ai/openchart-core';
|
|
13
|
+
|
|
14
|
+
export type { SparklineData };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract numeric values from a row for sparkline rendering.
|
|
18
|
+
*
|
|
19
|
+
* If valuesField is specified, reads an array from that field.
|
|
20
|
+
* Otherwise uses the column's own key (expects an array value).
|
|
21
|
+
*/
|
|
22
|
+
function extractValues(
|
|
23
|
+
row: Record<string, unknown>,
|
|
24
|
+
columnKey: string,
|
|
25
|
+
config: SparklineColumnConfig,
|
|
26
|
+
): number[] {
|
|
27
|
+
const field = config.valuesField ?? columnKey;
|
|
28
|
+
const raw = row[field];
|
|
29
|
+
|
|
30
|
+
if (!Array.isArray(raw)) return [];
|
|
31
|
+
|
|
32
|
+
return raw
|
|
33
|
+
.map((v) => (typeof v === 'number' && Number.isFinite(v) ? v : null))
|
|
34
|
+
.filter((v): v is number => v !== null);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Compute sparkline data for a single row.
|
|
39
|
+
*
|
|
40
|
+
* Normalizes values to 0-1 range. Returns null if no valid values.
|
|
41
|
+
*/
|
|
42
|
+
export function computeSparkline(
|
|
43
|
+
values: number[],
|
|
44
|
+
config: SparklineColumnConfig,
|
|
45
|
+
theme: ResolvedTheme,
|
|
46
|
+
_darkMode: boolean,
|
|
47
|
+
): SparklineData | null {
|
|
48
|
+
if (values.length === 0) return null;
|
|
49
|
+
|
|
50
|
+
const type = config.type ?? 'line';
|
|
51
|
+
const color = config.color ?? theme.colors.categorical[0];
|
|
52
|
+
|
|
53
|
+
let min = Infinity;
|
|
54
|
+
let max = -Infinity;
|
|
55
|
+
for (const v of values) {
|
|
56
|
+
if (v < min) min = v;
|
|
57
|
+
if (v > max) max = v;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const range = max - min;
|
|
61
|
+
const normalize = (v: number): number => (range === 0 ? 0.5 : (v - min) / range);
|
|
62
|
+
|
|
63
|
+
const startValue = values[0];
|
|
64
|
+
const endValue = values[values.length - 1];
|
|
65
|
+
|
|
66
|
+
if (type === 'line') {
|
|
67
|
+
const points = values.map((v, i) => ({
|
|
68
|
+
x: values.length === 1 ? 0.5 : i / (values.length - 1),
|
|
69
|
+
y: normalize(v),
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
type,
|
|
74
|
+
points,
|
|
75
|
+
bars: [],
|
|
76
|
+
color,
|
|
77
|
+
count: values.length,
|
|
78
|
+
startValue,
|
|
79
|
+
endValue,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Bar (horizontal) or column (vertical): normalized as proportions
|
|
84
|
+
const bars = values.map(normalize);
|
|
85
|
+
const points = values.map((v, i) => ({
|
|
86
|
+
x: values.length === 1 ? 0.5 : i / (values.length - 1),
|
|
87
|
+
y: normalize(v),
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
type,
|
|
92
|
+
points,
|
|
93
|
+
bars,
|
|
94
|
+
color,
|
|
95
|
+
count: values.length,
|
|
96
|
+
startValue,
|
|
97
|
+
endValue,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Extract values and compute sparkline data for a cell.
|
|
103
|
+
*/
|
|
104
|
+
export function computeSparklineForRow(
|
|
105
|
+
row: Record<string, unknown>,
|
|
106
|
+
columnKey: string,
|
|
107
|
+
config: SparklineColumnConfig,
|
|
108
|
+
theme: ResolvedTheme,
|
|
109
|
+
darkMode: boolean,
|
|
110
|
+
): SparklineData | null {
|
|
111
|
+
const values = extractValues(row, columnKey, config);
|
|
112
|
+
return computeSparkline(values, config, theme, darkMode);
|
|
113
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for table column computations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { contrastRatio } from '@opendata-ai/openchart-core';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Pick a text color (black or white) that meets better contrast against the background.
|
|
9
|
+
*/
|
|
10
|
+
export function accessibleTextColor(bg: string): string {
|
|
11
|
+
const white = '#ffffff';
|
|
12
|
+
const black = '#000000';
|
|
13
|
+
const whiteRatio = contrastRatio(white, bg);
|
|
14
|
+
const blackRatio = contrastRatio(black, bg);
|
|
15
|
+
return whiteRatio >= blackRatio ? white : black;
|
|
16
|
+
}
|