@opendata-ai/openchart-core 6.7.1 → 6.8.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.7.1",
3
+ "version": "6.8.0",
4
4
  "description": "Types, theme, colors, accessibility, and utilities for openchart",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Riley Hilliard",
@@ -144,6 +144,41 @@ describe('computeChrome', () => {
144
144
  expect(narrow.subtitle!.y).toBeGreaterThan(wide.subtitle!.y);
145
145
  });
146
146
 
147
+ it('reserves extra height when subtitle contains newline characters', () => {
148
+ const chrome: Chrome = { title: 'Title', subtitle: 'Line one\nLine two' };
149
+ const withNewline = computeChrome(chrome, theme, 600);
150
+
151
+ const chromeNoNewline: Chrome = { title: 'Title', subtitle: 'Line one Line two' };
152
+ const withoutNewline = computeChrome(chromeNoNewline, theme, 600);
153
+
154
+ // The newline version should reserve more top height since it forces two lines
155
+ expect(withNewline.topHeight).toBeGreaterThan(withoutNewline.topHeight);
156
+ });
157
+
158
+ it('handles consecutive newlines in chrome text', () => {
159
+ const chrome: Chrome = { title: 'Title', subtitle: 'Above\n\nBelow' };
160
+ const result = computeChrome(chrome, theme, 600);
161
+
162
+ // Three segments: "Above", "", "Below" -> 3 lines total
163
+ // This should be taller than a simple two-line subtitle
164
+ const twoLine: Chrome = { title: 'Title', subtitle: 'Above\nBelow' };
165
+ const twoLineResult = computeChrome(twoLine, theme, 600);
166
+
167
+ expect(result.topHeight).toBeGreaterThan(twoLineResult.topHeight);
168
+ });
169
+
170
+ it('handles newlines combined with long text that also word-wraps', () => {
171
+ const longSegment = 'This is a very long segment that should wrap at narrow widths on its own';
172
+ const chrome: Chrome = { title: 'Title', subtitle: `${longSegment}\nShort` };
173
+ const result = computeChrome(chrome, theme, 300);
174
+
175
+ // At narrow width, the long segment wraps AND the \n adds another line
176
+ const noNewline: Chrome = { title: 'Title', subtitle: longSegment };
177
+ const noNewlineResult = computeChrome(noNewline, theme, 300);
178
+
179
+ expect(result.topHeight).toBeGreaterThan(noNewlineResult.topHeight);
180
+ });
181
+
147
182
  it('uses measureText function when provided', () => {
148
183
  const measureText = (text: string, fontSize: number) => ({
149
184
  width: text.length * fontSize * 0.6,
@@ -91,6 +91,17 @@ function estimateLineCount(
91
91
  ): number {
92
92
  if (maxWidth <= 0) return 1;
93
93
 
94
+ // Split on explicit newlines first, then estimate wrapping per segment
95
+ const segments = text.split('\n');
96
+ if (segments.length > 1) {
97
+ return segments.reduce((total, segment) => {
98
+ return (
99
+ total +
100
+ (segment.length === 0 ? 1 : estimateLineCount(segment, style, maxWidth, _measureText))
101
+ );
102
+ }, 0);
103
+ }
104
+
94
105
  const charWidth = estimateCharWidth(style.fontSize, style.fontWeight);
95
106
  const maxChars = Math.floor(maxWidth / charWidth);
96
107
 
package/src/types/spec.ts CHANGED
@@ -483,9 +483,17 @@ export type Annotation = TextAnnotation | RangeAnnotation | RefLineAnnotation;
483
483
 
484
484
  /**
485
485
  * Dark mode behavior.
486
- * - "auto": respect system preference (prefers-color-scheme)
487
- * - "force": always render in dark mode
488
- * - "off": always render in light mode (default)
486
+ *
487
+ * - `"auto"` - Checks the `prefers-color-scheme` media query to detect the
488
+ * user's system-level preference. This does NOT detect class-based dark mode
489
+ * toggles (e.g. `document.documentElement.classList.toggle('dark')`). If your
490
+ * app uses class-based dark mode, use VizThemeProvider with `"force"` or
491
+ * `"off"` instead of `"auto"` and toggle based on your app's state.
492
+ * - `"force"` - Always render in dark mode regardless of system preference.
493
+ * - `"off"` - Always render in light mode (default).
494
+ *
495
+ * All components (Chart, Sankey, Graph) inherit darkMode from VizThemeProvider
496
+ * when no explicit prop is passed.
489
497
  */
490
498
  export type DarkMode = 'auto' | 'force' | 'off';
491
499
 
@@ -576,6 +584,8 @@ export interface LegendConfig {
576
584
  columns?: number;
577
585
  /** Max number of legend entries before truncation. Remaining entries show as "+N more". */
578
586
  symbolLimit?: number;
587
+ /** Maximum number of rows for top-positioned legends before truncation. Defaults to 2. */
588
+ maxRows?: number;
579
589
  }
580
590
 
581
591
  // ---------------------------------------------------------------------------
@@ -916,6 +926,8 @@ export interface SankeySpec {
916
926
  iterations?: number;
917
927
  /** Link coloring strategy. Defaults to 'gradient'. */
918
928
  linkStyle?: SankeyLinkColor;
929
+ /** Link fill opacity (0-1). Defaults to 0.5 in light mode, 0.75 in dark mode. */
930
+ linkOpacity?: number;
919
931
  /** Editorial chrome (title, subtitle, source, byline, footer). */
920
932
  chrome?: Chrome;
921
933
  /** Legend display configuration. */
@@ -926,6 +938,14 @@ export interface SankeySpec {
926
938
  darkMode?: DarkMode;
927
939
  /** Animation configuration for entrance animations. */
928
940
  animation?: AnimationSpec;
941
+ /**
942
+ * d3-format string applied to flow values in tooltips and ARIA labels.
943
+ * Uses the literal suffix extension: ".0f%" appends "%" to the formatted
944
+ * number (data value 28 renders as "28%"). For currency: "$,.0f" or "$~s".
945
+ * For SI suffixes: "~s" (renders 1000 as "1k"). When not set, values use
946
+ * the default number formatter.
947
+ */
948
+ valueFormat?: string;
929
949
  }
930
950
 
931
951
  /**