@opendata-ai/openchart-core 6.7.1 → 6.9.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.9.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,15 @@ 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;
931
+ /**
932
+ * Which side of each node to place labels. 'auto' uses the default heuristic
933
+ * (left-column right, right-column left, middle right). 'right' forces all
934
+ * labels to the right of their nodes. 'left' forces all labels to the left.
935
+ * Defaults to 'auto'.
936
+ */
937
+ nodeLabelAlign?: 'auto' | 'left' | 'right';
919
938
  /** Editorial chrome (title, subtitle, source, byline, footer). */
920
939
  chrome?: Chrome;
921
940
  /** Legend display configuration. */
@@ -926,6 +945,14 @@ export interface SankeySpec {
926
945
  darkMode?: DarkMode;
927
946
  /** Animation configuration for entrance animations. */
928
947
  animation?: AnimationSpec;
948
+ /**
949
+ * d3-format string applied to flow values in tooltips and ARIA labels.
950
+ * Uses the literal suffix extension: ".0f%" appends "%" to the formatted
951
+ * number (data value 28 renders as "28%"). For currency: "$,.0f" or "$~s".
952
+ * For SI suffixes: "~s" (renders 1000 as "1k"). When not set, values use
953
+ * the default number formatter.
954
+ */
955
+ valueFormat?: string;
929
956
  }
930
957
 
931
958
  /**