@opendata-ai/openchart-core 6.10.0 → 6.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendata-ai/openchart-core",
3
- "version": "6.10.0",
3
+ "version": "6.11.0",
4
4
  "description": "Types, theme, colors, accessibility, and utilities for openchart",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Riley Hilliard",
@@ -0,0 +1,95 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { GradientDef } from '../spec';
3
+ import { getRepresentativeColor, isGradientDef } from '../spec';
4
+
5
+ describe('isGradientDef', () => {
6
+ it('returns true for a valid linear gradient', () => {
7
+ expect(
8
+ isGradientDef({
9
+ gradient: 'linear',
10
+ stops: [
11
+ { offset: 0, color: '#f00' },
12
+ { offset: 1, color: '#00f' },
13
+ ],
14
+ }),
15
+ ).toBe(true);
16
+ });
17
+
18
+ it('returns true for a valid radial gradient', () => {
19
+ expect(
20
+ isGradientDef({
21
+ gradient: 'radial',
22
+ stops: [{ offset: 0, color: '#f00' }],
23
+ }),
24
+ ).toBe(true);
25
+ });
26
+
27
+ it('returns false for a plain string', () => {
28
+ expect(isGradientDef('#ff0000')).toBe(false);
29
+ });
30
+
31
+ it('returns false for null', () => {
32
+ expect(isGradientDef(null)).toBe(false);
33
+ });
34
+
35
+ it('returns false for undefined', () => {
36
+ expect(isGradientDef(undefined)).toBe(false);
37
+ });
38
+
39
+ it('returns false for an object without gradient property', () => {
40
+ expect(isGradientDef({ stops: [] })).toBe(false);
41
+ });
42
+
43
+ it('returns false for an object without stops', () => {
44
+ expect(isGradientDef({ gradient: 'linear' })).toBe(false);
45
+ });
46
+
47
+ it('returns false for an object with invalid gradient type', () => {
48
+ expect(isGradientDef({ gradient: 'mesh', stops: [] })).toBe(false);
49
+ });
50
+ });
51
+
52
+ describe('getRepresentativeColor', () => {
53
+ it('returns the string directly when given a string', () => {
54
+ expect(getRepresentativeColor('#1b7fa3')).toBe('#1b7fa3');
55
+ });
56
+
57
+ it('returns the last stop color for a linear gradient', () => {
58
+ const grad: GradientDef = {
59
+ gradient: 'linear',
60
+ stops: [
61
+ { offset: 0, color: '#f00' },
62
+ { offset: 0.5, color: '#0f0' },
63
+ { offset: 1, color: '#00f' },
64
+ ],
65
+ };
66
+ expect(getRepresentativeColor(grad)).toBe('#00f');
67
+ });
68
+
69
+ it('returns the last stop color for a radial gradient', () => {
70
+ const grad: GradientDef = {
71
+ gradient: 'radial',
72
+ stops: [
73
+ { offset: 0, color: '#fff' },
74
+ { offset: 1, color: '#000' },
75
+ ],
76
+ };
77
+ expect(getRepresentativeColor(grad)).toBe('#000');
78
+ });
79
+
80
+ it('returns #000000 for a gradient with empty stops array', () => {
81
+ const grad: GradientDef = {
82
+ gradient: 'linear',
83
+ stops: [],
84
+ };
85
+ expect(getRepresentativeColor(grad)).toBe('#000000');
86
+ });
87
+
88
+ it('returns the single stop color for a gradient with one stop', () => {
89
+ const grad: GradientDef = {
90
+ gradient: 'radial',
91
+ stops: [{ offset: 0, color: '#abc' }],
92
+ };
93
+ expect(getRepresentativeColor(grad)).toBe('#abc');
94
+ });
95
+ });
@@ -114,6 +114,8 @@ export type {
114
114
  FieldType,
115
115
  FilterPredicate,
116
116
  FilterTransform,
117
+ GradientDef,
118
+ GradientStop,
117
119
  GraphEdge,
118
120
  GraphEncoding,
119
121
  GraphEncodingChannel,
@@ -125,12 +127,14 @@ export type {
125
127
  LabelDensity,
126
128
  LayerSpec,
127
129
  LegendConfig,
130
+ LinearGradient,
128
131
  LogicalAnd,
129
132
  LogicalNot,
130
133
  LogicalOr,
131
134
  MarkDef,
132
135
  MarkType,
133
136
  NodeOverride,
137
+ RadialGradient,
134
138
  RangeAnnotation,
135
139
  RefLineAnnotation,
136
140
  ResolveConfig,
@@ -155,9 +159,11 @@ export type {
155
159
  } from './spec';
156
160
  export {
157
161
  CHART_TYPES,
162
+ getRepresentativeColor,
158
163
  isChartSpec,
159
164
  isConditionalDef,
160
165
  isEncodingChannel,
166
+ isGradientDef,
161
167
  isGraphSpec,
162
168
  isLayerSpec,
163
169
  isRangeAnnotation,
@@ -9,6 +9,7 @@
9
9
  * fully resolved with computed positions, colors, and dimensions.
10
10
  */
11
11
 
12
+ import type { GradientDef } from './spec';
12
13
  import type { ResolvedTheme } from './theme';
13
14
 
14
15
  // ---------------------------------------------------------------------------
@@ -235,8 +236,8 @@ export interface AreaMark {
235
236
  path: string;
236
237
  /** SVG path string for just the top boundary (for stroking the data line only). */
237
238
  topPath: string;
238
- /** Fill color. */
239
- fill: string;
239
+ /** Fill color or gradient. */
240
+ fill: string | GradientDef;
240
241
  /** Fill opacity. */
241
242
  fillOpacity: number;
242
243
  /** Optional stroke for the top boundary. */
@@ -277,8 +278,8 @@ export interface RectMark {
277
278
  width: number;
278
279
  /** Height. */
279
280
  height: number;
280
- /** Fill color. */
281
- fill: string;
281
+ /** Fill color or gradient. */
282
+ fill: string | GradientDef;
282
283
  /** Stroke color. */
283
284
  stroke?: string;
284
285
  /** Stroke width. */
@@ -321,8 +322,8 @@ export interface ArcMark {
321
322
  startAngle: number;
322
323
  /** End angle in radians. */
323
324
  endAngle: number;
324
- /** Fill color. */
325
- fill: string;
325
+ /** Fill color or gradient. */
326
+ fill: string | GradientDef;
326
327
  /** Stroke color (usually white for slice separation). */
327
328
  stroke: string;
328
329
  /** Stroke width. */
@@ -349,8 +350,8 @@ export interface PointMark {
349
350
  cy: number;
350
351
  /** Radius in pixels. */
351
352
  r: number;
352
- /** Fill color. */
353
- fill: string;
353
+ /** Fill color or gradient. */
354
+ fill: string | GradientDef;
354
355
  /** Stroke color. */
355
356
  stroke: string;
356
357
  /** Stroke width. */
package/src/types/spec.ts CHANGED
@@ -48,6 +48,87 @@ export type MarkType =
48
48
  /** @deprecated Use MarkType instead. Kept for internal migration references. */
49
49
  export type ChartType = MarkType;
50
50
 
51
+ // ---------------------------------------------------------------------------
52
+ // Gradient definitions (Vega-aligned)
53
+ // ---------------------------------------------------------------------------
54
+
55
+ /** A single color stop in a gradient definition. */
56
+ export interface GradientStop {
57
+ /** Position along the gradient, 0 to 1. */
58
+ offset: number;
59
+ /** CSS color string at this stop. */
60
+ color: string;
61
+ /** Opacity at this stop, 0 to 1. Maps to SVG stop-opacity. */
62
+ opacity?: number;
63
+ }
64
+
65
+ /**
66
+ * Linear gradient definition.
67
+ * Coordinates are in [0,1] normalized space relative to the mark's bounding box
68
+ * (maps to SVG gradientUnits="objectBoundingBox").
69
+ */
70
+ export interface LinearGradient {
71
+ gradient: 'linear';
72
+ /** Color stops from start to end. */
73
+ stops: GradientStop[];
74
+ /** Start x coordinate (0-1). Default: 0. */
75
+ x1?: number;
76
+ /** Start y coordinate (0-1). Default: 0. */
77
+ y1?: number;
78
+ /** End x coordinate (0-1). Default: 0. */
79
+ x2?: number;
80
+ /** End y coordinate (0-1). Default: 1 (top-to-bottom). */
81
+ y2?: number;
82
+ }
83
+
84
+ /**
85
+ * Radial gradient definition.
86
+ * Coordinates are in [0,1] normalized space relative to the mark's bounding box.
87
+ */
88
+ export interface RadialGradient {
89
+ gradient: 'radial';
90
+ /** Color stops from inner to outer. */
91
+ stops: GradientStop[];
92
+ /** Inner circle center x (0-1). Default: 0.5. */
93
+ x1?: number;
94
+ /** Inner circle center y (0-1). Default: 0.5. */
95
+ y1?: number;
96
+ /** Inner circle radius (0-1). Default: 0. */
97
+ r1?: number;
98
+ /** Outer circle center x (0-1). Default: 0.5. */
99
+ x2?: number;
100
+ /** Outer circle center y (0-1). Default: 0.5. */
101
+ y2?: number;
102
+ /** Outer circle radius (0-1). Default: 0.5. */
103
+ r2?: number;
104
+ }
105
+
106
+ /** A gradient definition, either linear or radial. */
107
+ export type GradientDef = LinearGradient | RadialGradient;
108
+
109
+ /** Type guard: check if a value is a GradientDef object. */
110
+ export function isGradientDef(value: unknown): value is GradientDef {
111
+ return (
112
+ typeof value === 'object' &&
113
+ value !== null &&
114
+ 'gradient' in value &&
115
+ 'stops' in value &&
116
+ ((value as GradientDef).gradient === 'linear' || (value as GradientDef).gradient === 'radial')
117
+ );
118
+ }
119
+
120
+ /**
121
+ * Extract a single representative color from a fill value.
122
+ * Returns the fill directly if it's a string, or the last stop color
123
+ * if it's a gradient. Used by tooltips, labels, legends, and voronoi
124
+ * overlays that need a flat color.
125
+ */
126
+ export function getRepresentativeColor(fill: string | GradientDef): string {
127
+ if (typeof fill === 'string') return fill;
128
+ const stops = fill.stops;
129
+ return stops.length > 0 ? stops[stops.length - 1].color : '#000000';
130
+ }
131
+
51
132
  // ---------------------------------------------------------------------------
52
133
  // Mark definition (Vega-Lite aligned)
53
134
  // ---------------------------------------------------------------------------
@@ -94,8 +175,8 @@ export interface MarkDef {
94
175
  filled?: boolean;
95
176
  /** Default opacity (0-1). */
96
177
  opacity?: number;
97
- /** Default fill color. */
98
- fill?: string;
178
+ /** Default fill color or gradient. */
179
+ fill?: string | GradientDef;
99
180
  /** Default stroke color. */
100
181
  stroke?: string;
101
182
  /** Default stroke width. */