@opendata-ai/openchart-core 6.28.6 → 7.0.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.js CHANGED
@@ -699,27 +699,43 @@ function findAccessibleColor(baseColor, bg, targetRatio = 4.5) {
699
699
  }
700
700
 
701
701
  // src/colors/palettes.ts
702
+ var ACHROMATIC_RAMP = {
703
+ fg: "#f7f8f8",
704
+ // primary text
705
+ fg2: "#d0d6e0",
706
+ // body text
707
+ fgMuted: "#a1a1aa",
708
+ // secondary series (zinc-400)
709
+ fgSubtle: "#71717a",
710
+ // tertiary series (zinc-500)
711
+ fgFaint: "#52525b",
712
+ // quaternary series (zinc-600)
713
+ secondary: "#27272a",
714
+ // hover / raised surface
715
+ card: "#111113",
716
+ // card surface
717
+ bg: "#09090b"
718
+ // canvas
719
+ };
702
720
  var CATEGORICAL_PALETTE = [
703
- "#1b7fa3",
704
- // teal-blue (primary)
705
- "#c44e52",
706
- // warm red (secondary)
707
- "#6a9f58",
708
- // softer green (tertiary)
709
- "#d47215",
710
- // orange
711
- "#507e79",
712
- // muted teal
713
- "#9a6a8d",
714
- // purple
715
- "#c4636b",
716
- // rose
717
- "#9c755f",
718
- // brown
719
- "#a88f22",
720
- // olive gold
721
- "#858078"
722
- // warm gray
721
+ "#06b6d4",
722
+ // cyan, primary accent (sRGB literal, ~205°)
723
+ "#eb7289",
724
+ // rose — oklch(70% 0.15 10)
725
+ "#3bb974",
726
+ // emerald — oklch(70% 0.15 155)
727
+ "#ad87ed",
728
+ // violet — oklch(70% 0.15 300)
729
+ "#e69c3a",
730
+ // amber — oklch(75% 0.14 70)
731
+ "#4ba3f7",
732
+ // sky — oklch(70% 0.15 250)
733
+ "#eb8656",
734
+ // orange — oklch(72% 0.14 45)
735
+ "#8494fa",
736
+ // indigo — oklch(70% 0.15 275)
737
+ "#00b9c3"
738
+ // teal — oklch(70% 0.15 200)
723
739
  ];
724
740
  var SEQUENTIAL_BLUE = {
725
741
  name: "blue",
@@ -792,9 +808,17 @@ var DIVERGING_PALETTES = {
792
808
  };
793
809
 
794
810
  // src/theme/dark-mode.ts
795
- var DARK_BG = "#1a1a2e";
796
- var DARK_TEXT = "#e0e0e0";
811
+ var DARK_BG = ACHROMATIC_RAMP.bg;
812
+ var DARK_TEXT = ACHROMATIC_RAMP.fg;
797
813
  function adaptColorForDarkMode(color2, lightBg, darkBg) {
814
+ if (rgb(color2) == null) {
815
+ if (typeof console !== "undefined" && console.warn) {
816
+ console.warn(
817
+ `[openchart] adaptColorForDarkMode: unparseable color "${color2}", returning unchanged. Use precomputed sRGB hex.`
818
+ );
819
+ }
820
+ return color2;
821
+ }
798
822
  const originalRatio = contrastRatio(color2, lightBg);
799
823
  const c = hsl(color2);
800
824
  if (c == null || Number.isNaN(c.h)) {
@@ -840,9 +864,10 @@ function adaptTheme(theme) {
840
864
  const alreadyDark = inputBg === "transparent" || _luminanceFromHex(inputBg) < 0.2;
841
865
  const darkBg = alreadyDark ? inputBg : DARK_BG;
842
866
  const darkText = alreadyDark ? theme.colors.text : DARK_TEXT;
843
- const darkGridline = alreadyDark ? theme.colors.gridline : "#333344";
844
- const darkAxis = alreadyDark ? theme.colors.axis : "#888899";
845
- const categorical = alreadyDark ? theme.colors.categorical : theme.colors.categorical.map((c) => adaptColorForDarkMode(c, inputBg, darkBg));
867
+ const darkGridline = alreadyDark ? theme.colors.gridline : "rgba(255,255,255,0.05)";
868
+ const darkAxis = alreadyDark ? theme.colors.axis : "#a1a1aa";
869
+ const darkMuted = ACHROMATIC_RAMP.fgMuted;
870
+ const categorical = theme.colors.categorical;
846
871
  return {
847
872
  ...theme,
848
873
  isDark: true,
@@ -852,16 +877,24 @@ function adaptTheme(theme) {
852
877
  text: darkText,
853
878
  gridline: darkGridline,
854
879
  axis: darkAxis,
855
- annotationFill: "rgba(255,255,255,0.08)",
856
- annotationText: "#bbbbcc",
857
- categorical
880
+ annotationFill: "rgba(255,255,255,0.06)",
881
+ annotationText: darkMuted,
882
+ categorical,
883
+ // Sparkline trend colors tuned for dark surfaces: teal-leaning green
884
+ // and coral red read better than the saturated light-mode tokens.
885
+ // Any non-default value is treated as a user override and preserved.
886
+ positive: theme.colors.positive !== "#16a34a" ? theme.colors.positive : "#34d399",
887
+ negative: theme.colors.negative !== "#dc2626" ? theme.colors.negative : "#f87171"
858
888
  },
859
889
  chrome: {
890
+ // Eyebrow keeps its accent tint (cyan in both modes); the other
891
+ // chrome elements desaturate to a muted gray on the dark canvas.
892
+ eyebrow: theme.chrome.eyebrow,
860
893
  title: { ...theme.chrome.title, color: darkText },
861
- subtitle: { ...theme.chrome.subtitle, color: "#aaaaaa" },
862
- source: { ...theme.chrome.source, color: "#888888" },
863
- byline: { ...theme.chrome.byline, color: "#888888" },
864
- footer: { ...theme.chrome.footer, color: "#888888" }
894
+ subtitle: { ...theme.chrome.subtitle, color: darkMuted },
895
+ source: { ...theme.chrome.source, color: darkMuted },
896
+ byline: { ...theme.chrome.byline, color: darkMuted },
897
+ footer: { ...theme.chrome.footer, color: darkMuted }
865
898
  }
866
899
  };
867
900
  }
@@ -873,26 +906,31 @@ var DEFAULT_THEME = {
873
906
  sequential: SEQUENTIAL_PALETTES,
874
907
  diverging: DIVERGING_PALETTES,
875
908
  background: "#ffffff",
876
- text: "#1d1d1d",
877
- gridline: "#e8e8e8",
878
- axis: "#888888",
909
+ text: "#09090b",
910
+ gridline: "rgba(0,0,0,0.06)",
911
+ // Used for axis lines/ticks AND axis tick label fill. Must clear WCAG AA
912
+ // contrast (4.5:1) on white because tick labels are rendered with this
913
+ // color. Zinc-500 hits ~5.7:1.
914
+ axis: "#71717a",
879
915
  annotationFill: "rgba(0,0,0,0.04)",
880
- annotationText: "#555555"
916
+ annotationText: "#71717a",
917
+ positive: "#16a34a",
918
+ negative: "#dc2626"
881
919
  },
882
920
  fonts: {
883
- family: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
921
+ family: '"Inter Variable", Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
884
922
  mono: '"JetBrains Mono", "Fira Code", "Cascadia Code", monospace',
885
923
  sizes: {
886
- title: 22,
887
- subtitle: 15,
924
+ title: 26,
925
+ subtitle: 14,
888
926
  body: 13,
889
927
  small: 11,
890
928
  axisTick: 11
891
929
  },
892
930
  weights: {
893
931
  normal: 400,
894
- medium: 500,
895
- semibold: 600,
932
+ medium: 510,
933
+ semibold: 590,
896
934
  bold: 700
897
935
  }
898
936
  },
@@ -903,37 +941,43 @@ var DEFAULT_THEME = {
903
941
  chartToFooter: 8,
904
942
  axisMargin: 6
905
943
  },
906
- borderRadius: 4,
944
+ borderRadius: 2,
907
945
  chrome: {
946
+ eyebrow: {
947
+ fontSize: 11,
948
+ fontWeight: 510,
949
+ color: "#06b6d4",
950
+ lineHeight: 1.4
951
+ },
908
952
  title: {
909
- fontSize: 22,
910
- fontWeight: 700,
911
- color: "#333333",
912
- lineHeight: 1.3
953
+ fontSize: 26,
954
+ fontWeight: 590,
955
+ color: "#09090b",
956
+ lineHeight: 1.15
913
957
  },
914
958
  subtitle: {
915
- fontSize: 15,
959
+ fontSize: 14,
916
960
  fontWeight: 400,
917
- color: "#666666",
918
- lineHeight: 1.4
961
+ color: "#71717a",
962
+ lineHeight: 1.45
919
963
  },
920
964
  source: {
921
- fontSize: 12,
965
+ fontSize: 11,
922
966
  fontWeight: 400,
923
- color: "#999999",
924
- lineHeight: 1.3
967
+ color: "#71717a",
968
+ lineHeight: 1.4
925
969
  },
926
970
  byline: {
927
- fontSize: 12,
971
+ fontSize: 11,
928
972
  fontWeight: 400,
929
- color: "#999999",
930
- lineHeight: 1.3
973
+ color: "#71717a",
974
+ lineHeight: 1.4
931
975
  },
932
976
  footer: {
933
- fontSize: 12,
977
+ fontSize: 11,
934
978
  fontWeight: 400,
935
- color: "#999999",
936
- lineHeight: 1.3
979
+ color: "#71717a",
980
+ lineHeight: 1.4
937
981
  }
938
982
  }
939
983
  };
@@ -1003,6 +1047,8 @@ function adaptChromeForDarkBg(theme, textColor) {
1003
1047
  return {
1004
1048
  ...theme,
1005
1049
  chrome: {
1050
+ // Eyebrow keeps its accent tint regardless of surface mode.
1051
+ eyebrow: theme.chrome.eyebrow,
1006
1052
  title: {
1007
1053
  ...theme.chrome.title,
1008
1054
  color: theme.chrome.title.color === light.title.color ? textColor : theme.chrome.title.color
@@ -1153,7 +1199,7 @@ function estimateLineCount(text, style, maxWidth, measureText) {
1153
1199
  }
1154
1200
  return lines;
1155
1201
  }
1156
- function computeChrome(chrome, theme, width, measureText, chromeMode = "full", padding, watermark = true) {
1202
+ function computeChrome(chrome, theme, width, measureText, chromeMode = "full", padding, watermark = true, bottomLegendHeight = 0) {
1157
1203
  if (!chrome || chromeMode === "hidden") {
1158
1204
  return { topHeight: 0, bottomHeight: 0 };
1159
1205
  }
@@ -1164,6 +1210,26 @@ function computeChrome(chrome, theme, width, measureText, chromeMode = "full", p
1164
1210
  const fontFamily = theme.fonts.family;
1165
1211
  let topY = pad2;
1166
1212
  const topElements = {};
1213
+ const eyebrowNorm = chromeMode === "compact" ? null : normalizeChromeText(chrome.eyebrow);
1214
+ if (eyebrowNorm) {
1215
+ const style = buildTextStyle(
1216
+ theme.chrome.eyebrow,
1217
+ fontFamily,
1218
+ theme.chrome.eyebrow.color,
1219
+ width,
1220
+ eyebrowNorm.style
1221
+ );
1222
+ const lineCount = estimateLineCount(eyebrowNorm.text, style, maxWidth, measureText);
1223
+ const element = {
1224
+ text: eyebrowNorm.text,
1225
+ x: pad2 + (eyebrowNorm.offset?.dx ?? 0),
1226
+ y: topY + (eyebrowNorm.offset?.dy ?? 0),
1227
+ maxWidth,
1228
+ style
1229
+ };
1230
+ topElements.eyebrow = element;
1231
+ topY += estimateTextHeight(style.fontSize, lineCount, style.lineHeight) + chromeGap;
1232
+ }
1167
1233
  const titleNorm = normalizeChromeText(chrome.title);
1168
1234
  if (titleNorm) {
1169
1235
  const style = buildTextStyle(
@@ -1204,14 +1270,16 @@ function computeChrome(chrome, theme, width, measureText, chromeMode = "full", p
1204
1270
  topElements.subtitle = element;
1205
1271
  topY += estimateTextHeight(style.fontSize, lineCount, style.lineHeight) + chromeGap;
1206
1272
  }
1207
- const hasTopChrome = titleNorm || subtitleNorm;
1273
+ const hasTopChrome = eyebrowNorm || titleNorm || subtitleNorm;
1208
1274
  const chromeToChart = width < COMPACT_WIDTH ? Math.min(theme.spacing.chromeToChart, 2) : theme.spacing.chromeToChart;
1209
1275
  const topHeight = hasTopChrome ? topY - pad2 + chromeToChart - chromeGap : 0;
1210
1276
  if (chromeMode === "compact") {
1211
1277
  let compactBottom = 0;
1212
1278
  if (watermark && width >= BRAND_MIN_WIDTH) {
1213
1279
  const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);
1214
- compactBottom = theme.spacing.chartToFooter + brandHeight + pad2;
1280
+ compactBottom = theme.spacing.chartToFooter + brandHeight + pad2 + bottomLegendHeight;
1281
+ } else if (bottomLegendHeight > 0) {
1282
+ compactBottom = bottomLegendHeight;
1215
1283
  }
1216
1284
  return {
1217
1285
  topHeight,
@@ -1219,7 +1287,9 @@ function computeChrome(chrome, theme, width, measureText, chromeMode = "full", p
1219
1287
  ...topElements
1220
1288
  };
1221
1289
  }
1222
- const shouldReserveBrandWidth = watermark && width >= BRAND_MIN_WIDTH;
1290
+ const brandNorm = normalizeChromeText(chrome.brand);
1291
+ const showWatermark = watermark && !brandNorm;
1292
+ const shouldReserveBrandWidth = (showWatermark || !!brandNorm) && width >= BRAND_MIN_WIDTH;
1223
1293
  const bottomMaxWidth = maxWidth - (shouldReserveBrandWidth ? BRAND_RESERVE_WIDTH : 0);
1224
1294
  const bottomElements = {};
1225
1295
  let bottomHeight = 0;
@@ -1250,6 +1320,7 @@ function computeChrome(chrome, theme, width, measureText, chromeMode = "full", p
1250
1320
  }
1251
1321
  if (bottomItems.length > 0) {
1252
1322
  bottomHeight += theme.spacing.chartToFooter;
1323
+ bottomHeight += bottomLegendHeight;
1253
1324
  for (const item of bottomItems) {
1254
1325
  const style = buildTextStyle(
1255
1326
  item.defaults,
@@ -1271,7 +1342,7 @@ function computeChrome(chrome, theme, width, measureText, chromeMode = "full", p
1271
1342
  bottomHeight += height + chromeGap;
1272
1343
  }
1273
1344
  bottomHeight -= chromeGap;
1274
- if (watermark && width >= BRAND_MIN_WIDTH) {
1345
+ if (showWatermark && width >= BRAND_MIN_WIDTH) {
1275
1346
  const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);
1276
1347
  const contentBelowFirstItem = bottomHeight - theme.spacing.chartToFooter;
1277
1348
  if (brandHeight > contentBelowFirstItem) {
@@ -1279,15 +1350,41 @@ function computeChrome(chrome, theme, width, measureText, chromeMode = "full", p
1279
1350
  }
1280
1351
  }
1281
1352
  bottomHeight += pad2;
1282
- } else if (watermark && width >= BRAND_MIN_WIDTH) {
1353
+ } else if (showWatermark && width >= BRAND_MIN_WIDTH) {
1283
1354
  const brandHeight = estimateTextHeight(BRAND_FONT_SIZE, 1);
1284
- bottomHeight = theme.spacing.chartToFooter + brandHeight + pad2;
1355
+ bottomHeight = theme.spacing.chartToFooter + brandHeight + pad2 + bottomLegendHeight;
1356
+ } else if (bottomLegendHeight > 0) {
1357
+ bottomHeight = bottomLegendHeight;
1358
+ }
1359
+ let brandElement;
1360
+ if (brandNorm && width >= BRAND_MIN_WIDTH) {
1361
+ const brandStyle = buildTextStyle(
1362
+ theme.chrome.footer,
1363
+ fontFamily,
1364
+ theme.chrome.footer.color,
1365
+ width,
1366
+ brandNorm.style
1367
+ );
1368
+ brandStyle.textAnchor = "end";
1369
+ const brandY = bottomItems.length > 0 ? theme.spacing.chartToFooter : theme.spacing.chartToFooter;
1370
+ brandElement = {
1371
+ text: brandNorm.text,
1372
+ x: width - pad2 + (brandNorm.offset?.dx ?? 0),
1373
+ y: brandY + (brandNorm.offset?.dy ?? 0),
1374
+ maxWidth: BRAND_RESERVE_WIDTH,
1375
+ style: brandStyle
1376
+ };
1377
+ if (bottomItems.length === 0) {
1378
+ const brandHeight = estimateTextHeight(brandStyle.fontSize, 1, brandStyle.lineHeight);
1379
+ bottomHeight = theme.spacing.chartToFooter + brandHeight + pad2;
1380
+ }
1285
1381
  }
1286
1382
  return {
1287
1383
  topHeight,
1288
1384
  bottomHeight,
1289
1385
  ...topElements,
1290
- ...bottomElements
1386
+ ...bottomElements,
1387
+ ...brandElement ? { brand: brandElement } : {}
1291
1388
  };
1292
1389
  }
1293
1390