@gooddata/sdk-ui-charts 11.62.0-alpha.2 → 11.62.0-alpha.4

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.
Files changed (19) hide show
  1. package/NOTICE +33 -610
  2. package/esm/charts/headline/internal/headlines/baseHeadline/baseHeadlineDataItems/comparisonItems/ComparisonValue.js +2 -2
  3. package/esm/charts/headline/internal/headlines/baseHeadline/baseHeadlineDataItems/comparisonItems/useComparisonDataItem.d.ts.map +1 -1
  4. package/esm/charts/headline/internal/headlines/baseHeadline/baseHeadlineDataItems/comparisonItems/useComparisonDataItem.js +1 -3
  5. package/esm/charts/repeater/internal/columnSizing.d.ts.map +1 -1
  6. package/esm/charts/repeater/internal/columnSizing.js +11 -13
  7. package/esm/highcharts/adapter/chartPlugins.d.ts.map +1 -1
  8. package/esm/highcharts/adapter/chartPlugins.js +5 -0
  9. package/esm/highcharts/adapter/plugins/autohideLabels/lineDataLabelLayout.d.ts +77 -0
  10. package/esm/highcharts/adapter/plugins/autohideLabels/lineDataLabelLayout.d.ts.map +1 -0
  11. package/esm/highcharts/adapter/plugins/autohideLabels/lineDataLabelLayout.js +183 -0
  12. package/esm/highcharts/adapter/plugins/autohideLabels/skipOverlappingLineDataLabels.d.ts +17 -0
  13. package/esm/highcharts/adapter/plugins/autohideLabels/skipOverlappingLineDataLabels.d.ts.map +1 -0
  14. package/esm/highcharts/adapter/plugins/autohideLabels/skipOverlappingLineDataLabels.js +144 -0
  15. package/esm/highcharts/adapter/plugins/inlineFontSize.d.ts +16 -0
  16. package/esm/highcharts/adapter/plugins/inlineFontSize.d.ts.map +1 -0
  17. package/esm/highcharts/adapter/plugins/inlineFontSize.js +64 -0
  18. package/esm/highcharts/chartTypes/_chartOptions/chartOptionsForSettings.js +2 -2
  19. package/package.json +15 -15
@@ -4,8 +4,8 @@ import { useBaseHeadlineDataItem } from "../useBaseHeadlineDataItem.js";
4
4
  export function ComparisonValue({ dataItem, comparisonStyle, isSubItem }) {
5
5
  const { formattedItem } = useBaseHeadlineDataItem(dataItem);
6
6
  const style = {
7
- ...(formattedItem?.cssStyle || {}),
8
- ...(comparisonStyle || {}),
7
+ ...formattedItem?.cssStyle,
8
+ ...comparisonStyle,
9
9
  };
10
10
  const valueClassNames = cx(["comparison-headline-value", "s-comparison-headline-value", "headline-value", "s-headline-value"], {
11
11
  "headline-value--empty s-headline-value--empty": formattedItem?.isValueEmpty && !comparisonStyle?.color,
@@ -1 +1 @@
1
- {"version":3,"file":"useComparisonDataItem.d.ts","sourceRoot":"","sources":["../../../../../../../../src/charts/headline/internal/headlines/baseHeadline/baseHeadlineDataItems/comparisonItems/useComparisonDataItem.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,OAAO,EACH,KAAK,kBAAkB,EACvB,KAAK,cAAc,EAGtB,MAAM,yCAAyC,CAAC;AAsCjD,eAAO,MAAM,qBAAqB,mBACd,cAAc,GAAG,SAAS,GAAG,IAAI,aACtC,kBAAkB;;;uCA5Bf,SAAS;CAmD1B,CAAC"}
1
+ {"version":3,"file":"useComparisonDataItem.d.ts","sourceRoot":"","sources":["../../../../../../../../src/charts/headline/internal/headlines/baseHeadline/baseHeadlineDataItems/comparisonItems/useComparisonDataItem.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAE3C,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C,OAAO,EACH,KAAK,kBAAkB,EACvB,KAAK,cAAc,EAGtB,MAAM,yCAAyC,CAAC;AAsCjD,eAAO,MAAM,qBAAqB,mBACd,cAAc,GAAG,SAAS,GAAG,IAAI,aACtC,kBAAkB;;;uCA5Bf,SAAS;CAiD1B,CAAC"}
@@ -35,9 +35,7 @@ export const useComparisonDataItem = (evaluationType, dataItem) => {
35
35
  const indicator = isArrowEnabled && evaluationType ? ComparisonIndicators[evaluationType] : undefined;
36
36
  // Prepare aria-label factory using the same formatting logic as ComparisonValue
37
37
  const comparisonAriaLabelFactory = createComparisonAriaLabelFactory(isArrowEnabled ? evaluationType : undefined, config, dataItem);
38
- const style = {
39
- ...(color ? { color } : {}),
40
- };
38
+ const style = color ? { color } : {};
41
39
  return {
42
40
  style,
43
41
  indicator,
@@ -1 +1 @@
1
- {"version":3,"file":"columnSizing.d.ts","sourceRoot":"","sources":["../../../../src/charts/repeater/internal/columnSizing.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAE9C,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAKzE,OAAO,EAEH,KAAK,+BAA+B,EACpC,KAAK,iCAAiC,EACtC,KAAK,6BAA6B,EAClC,KAAK,+BAA+B,EACpC,KAAK,mCAAmC,EAExC,KAAK,uBAAuB,EAC/B,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,wBAAgB,eAAe,CAAC,aAAa,EAAE,aAAa,GAAG,uBAAuB,EAAE,CAsBvF;AAsDD,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,kBAAkB,GAAG,OAAO,CAEzE;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,iCAAiC,CAE7F;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,+BAA+B,CAEzF;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,+BAA+B,CAEzF;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,6BAA6B,CAErF;AAED,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,mCAAmC,CAEjG;AAED,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,gBAAgB,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,MAAM,sBAMpG"}
1
+ {"version":3,"file":"columnSizing.d.ts","sourceRoot":"","sources":["../../../../src/charts/repeater/internal/columnSizing.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,OAAO,CAAC;AAE9C,OAAO,EAAE,KAAK,MAAM,EAAE,KAAK,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAKzE,OAAO,EAEH,KAAK,+BAA+B,EACpC,KAAK,iCAAiC,EACtC,KAAK,6BAA6B,EAClC,KAAK,+BAA+B,EACpC,KAAK,mCAAmC,EAExC,KAAK,uBAAuB,EAC/B,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAEvD,wBAAgB,eAAe,CAAC,aAAa,EAAE,aAAa,GAAG,uBAAuB,EAAE,CAoBvF;AAsDD,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,kBAAkB,GAAG,OAAO,CAEzE;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,iCAAiC,CAE7F;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,+BAA+B,CAEzF;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,+BAA+B,CAEzF;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,6BAA6B,CAErF;AAED,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,IAAI,mCAAmC,CAEjG;AAED,wBAAgB,sBAAsB,CAAC,aAAa,EAAE,gBAAgB,CAAC,aAAa,CAAC,EAAE,MAAM,EAAE,MAAM,sBAMpG"}
@@ -7,19 +7,17 @@ export function getColumnWidths(resizingState) {
7
7
  if (!columnApi) {
8
8
  return [];
9
9
  }
10
- return [
11
- ...resizingState.manuallyResizedColumns.map((col) => {
12
- const column = columnApi.getColumn(col.getColId());
13
- const item = getSizeItem(resizingState, column);
14
- const sizeItem = getColumnWidthItem(item, column, {
15
- value: column.getActualWidth(),
16
- //TODO: Grow to fit
17
- allowGrowToFit: false,
18
- });
19
- invariant(sizeItem, `unable to find size item by filed ${col.getColId()}`);
20
- return sizeItem;
21
- }),
22
- ];
10
+ return resizingState.manuallyResizedColumns.map((col) => {
11
+ const column = columnApi.getColumn(col.getColId());
12
+ const item = getSizeItem(resizingState, column);
13
+ const sizeItem = getColumnWidthItem(item, column, {
14
+ value: column.getActualWidth(),
15
+ //TODO: Grow to fit
16
+ allowGrowToFit: false,
17
+ });
18
+ invariant(sizeItem, `unable to find size item by filed ${col.getColId()}`);
19
+ return sizeItem;
20
+ });
23
21
  }
24
22
  function getSizeItem(resizingState, column) {
25
23
  return resizingState.items.find((item) => {
@@ -1 +1 @@
1
- {"version":3,"file":"chartPlugins.d.ts","sourceRoot":"","sources":["../../../src/highcharts/adapter/chartPlugins.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAiBlD,wBAAgB,gBAAgB,CAC5B,UAAU,EAAE,GAAG,EACf,4BAA4B,CAAC,EAAE,OAAO,EACtC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACtB,IAAI,CAwBN"}
1
+ {"version":3,"file":"chartPlugins.d.ts","sourceRoot":"","sources":["../../../src/highcharts/adapter/chartPlugins.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAmBlD,wBAAgB,gBAAgB,CAC5B,UAAU,EAAE,GAAG,EACf,4BAA4B,CAAC,EAAE,OAAO,EACtC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,GACtB,IAAI,CA2BN"}
@@ -4,8 +4,10 @@ import { initAccessibleTooltipPluginOnce } from "./plugins/accessibleTooltip.js"
4
4
  import { adjustTickAmount } from "./plugins/adjustTickAmount.js";
5
5
  import { autohideLabels } from "./plugins/autohideLabels/autohideLabels.js";
6
6
  import { skipDataLabelsWithoutSpace } from "./plugins/autohideLabels/skipDataLabelsWithoutSpace.js";
7
+ import { skipOverlappingLineDataLabels } from "./plugins/autohideLabels/skipOverlappingLineDataLabels.js";
7
8
  import { extendDataLabelColors } from "./plugins/dataLabelsColors.js";
8
9
  import { groupCategoriesWrapper } from "./plugins/group-categories-wrapper.js";
10
+ import { inlineFontSize } from "./plugins/inlineFontSize.js";
9
11
  import { linearTickPositions } from "./plugins/linearTickPositions.js";
10
12
  import { applyPointHaloOptions } from "./plugins/pointHalo.js";
11
13
  import { registerPointShapeSymbols } from "./plugins/pointShapeSymbols.js";
@@ -16,6 +18,7 @@ export function initChartPlugins(highcharts, enableAccessibleChartTooltip, theme
16
18
  if (!basePluginsInitialized) {
17
19
  autohideLabels(highcharts);
18
20
  skipDataLabelsWithoutSpace(highcharts);
21
+ skipOverlappingLineDataLabels(highcharts);
19
22
  applyPointHaloOptions(highcharts);
20
23
  linearTickPositions(highcharts);
21
24
  groupedCategories(highcharts);
@@ -25,6 +28,8 @@ export function initChartPlugins(highcharts, enableAccessibleChartTooltip, theme
25
28
  renderBubbles(highcharts);
26
29
  // register custom point-shape marker symbols beyond the five Highcharts built-ins
27
30
  registerPointShapeSymbols(highcharts);
31
+ // read text font size from the inline style to avoid forced style recalculations
32
+ inlineFontSize(highcharts);
28
33
  basePluginsInitialized = true;
29
34
  }
30
35
  if (enableAccessibleChartTooltip) {
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Text box of a data label in the coordinates Highcharts uses to detect overlaps
3
+ * (Chart.hideOverlappingLabels, getAbsoluteBox).
4
+ */
5
+ export interface ILabelBox {
6
+ x: number;
7
+ y: number;
8
+ width: number;
9
+ height: number;
10
+ }
11
+ export interface ILabelBoxInput {
12
+ plotX: number;
13
+ plotY: number;
14
+ /** Text width and height, without padding. */
15
+ textWidth: number;
16
+ textHeight: number;
17
+ padding: number;
18
+ /** Label options x and y offsets. */
19
+ x: number;
20
+ y: number;
21
+ verticalAlign: "top" | "middle" | "bottom";
22
+ plotWidth: number;
23
+ plotHeight: number;
24
+ /** Position of the series axes against the plot area (Highcharts horizontalAxisShift and verticalAxisShift). */
25
+ horizontalAxisShift: number;
26
+ verticalAxisShift: number;
27
+ /** Translation of the data labels group. */
28
+ translateX: number;
29
+ translateY: number;
30
+ }
31
+ /**
32
+ * Predicts the box of a center aligned, unrotated data label without a background box, as
33
+ * Highcharts places it in Series.alignDataLabel and justifyDataLabel.
34
+ */
35
+ export declare function predictLabelBox(input: ILabelBoxInput): ILabelBox;
36
+ /** Same test as Highcharts' isIntersectRect in hideOverlappingLabels. */
37
+ export declare function areBoxesIntersecting(a: ILabelBox, b: ILabelBox): boolean;
38
+ export interface IChartTextMetrics {
39
+ canvas: CanvasRenderingContext2D;
40
+ rootFontSize: number;
41
+ fontFamily: string;
42
+ textHeights: Map<string, number>;
43
+ }
44
+ export declare function getTextMetrics(chart: any): IChartTextMetrics | null;
45
+ export declare function resolveFontSize(fontSize: unknown, rootFontSize: number): number | undefined;
46
+ /** Height of a single line text box; it does not depend on the text, so one probe per font is enough. */
47
+ export declare function getTextHeight(chart: any, metrics: IChartTextMetrics, fontSize: number, fontWeight: string): number;
48
+ /** Forgets the measured fonts of the chart, for example after a web font loaded. */
49
+ export declare function resetTextMetrics(chart: any): void;
50
+ /**
51
+ * Label options the box model reproduces: a center aligned, unrotated SVG label without a box,
52
+ * justified into the plot area. Visibility options (enabled, allowOverlap) are not checked here.
53
+ */
54
+ export declare function isSupportedLabelPlacement(options: any): boolean;
55
+ /**
56
+ * hideOverlappingLabels compares the labels of the label collectors (plot lines and bands) and
57
+ * the stack labels before the data labels, so they could hide data labels.
58
+ */
59
+ export declare function hasOtherOverlappingLabels(chart: any): boolean;
60
+ /** Whether hideOverlappingLabels collects the data labels of the series. */
61
+ export declare function isLabelledSeries(series: any): boolean;
62
+ /** Series with data labels, in the order hideOverlappingLabels collects their labels. */
63
+ export declare function getLabelledSeries(chart: any): any[];
64
+ /**
65
+ * Width of the SVG text box (getBBox) for text measured on a canvas. SVG text is laid out in
66
+ * 1/64 px units, and the box also contains glyph ink that is outside of the advance width,
67
+ * for example the right edge of the last glyph in some bold fonts.
68
+ */
69
+ export declare function getSvgTextWidth(metrics: TextMetrics): number;
70
+ /**
71
+ * Predicts the text box of the point's data label. The text width is the SVG text box width
72
+ * (see getSvgTextWidth).
73
+ */
74
+ export declare function predictPointLabelBox(series: any, point: any, labelOptions: any, textWidth: number, textHeight: number): ILabelBox;
75
+ /** Highcharts hides the labels of points outside of the plot area (Series.alignDataLabel). */
76
+ export declare function isPointInsidePlot(series: any, point: any): boolean;
77
+ //# sourceMappingURL=lineDataLabelLayout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lineDataLabelLayout.d.ts","sourceRoot":"","sources":["../../../../../src/highcharts/adapter/plugins/autohideLabels/lineDataLabelLayout.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,MAAM,WAAW,SAAS;IACtB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,aAAa,EAAE,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,gHAAgH;IAChH,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,4CAA4C;IAC5C,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACtB;AAID;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG,SAAS,CA8ChE;AAED,yEAAyE;AACzE,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,GAAG,OAAO,CAExE;AAED,MAAM,WAAW,iBAAiB;IAC9B,MAAM,EAAE,wBAAwB,CAAC;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAMD,wBAAgB,cAAc,CAAC,KAAK,EAAE,GAAG,GAAG,iBAAiB,GAAG,IAAI,CAmBnE;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAO3F;AAED,yGAAyG;AACzG,wBAAgB,aAAa,CACzB,KAAK,EAAE,GAAG,EACV,OAAO,EAAE,iBAAiB,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACnB,MAAM,CAaR;AAED,oFAAoF;AACpF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,GAAG,GAAG,IAAI,CAEjD;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAoB/D;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,GAAG,GAAG,OAAO,CAU7D;AAED,4EAA4E;AAC5E,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAErD;AAED,yFAAyF;AACzF,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,EAAE,CAEnD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CAM5D;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAChC,MAAM,EAAE,GAAG,EACX,KAAK,EAAE,GAAG,EACV,YAAY,EAAE,GAAG,EACjB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACnB,SAAS,CAkBX;AAED,8FAA8F;AAC9F,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,GAAG,OAAO,CAOlE"}
@@ -0,0 +1,183 @@
1
+ // (C) 2026 GoodData Corporation
2
+ const VERTICAL_ALIGN_FACTOR = { top: 0, middle: 0.5, bottom: 1 };
3
+ /**
4
+ * Predicts the box of a center aligned, unrotated data label without a background box, as
5
+ * Highcharts places it in Series.alignDataLabel and justifyDataLabel.
6
+ */
7
+ export function predictLabelBox(input) {
8
+ const { plotX, padding, plotWidth, plotHeight, horizontalAxisShift, verticalAxisShift } = input;
9
+ const plotY = Math.round(input.plotY);
10
+ const width = input.textWidth + 2 * padding;
11
+ const height = input.textHeight + 2 * padding;
12
+ const alignX = (x) => Math.round(plotX + x - width / 2);
13
+ const alignY = (y, verticalAlign) => Math.round(plotY + y - height * VERTICAL_ALIGN_FACTOR[verticalAlign]);
14
+ // justifyDataLabel: every check reads the first alignment and the original vertical align
15
+ const alignedX = alignX(input.x);
16
+ const alignedY = alignY(input.y, input.verticalAlign);
17
+ let { x, y, verticalAlign } = input;
18
+ const offLeft = alignedX + padding + horizontalAxisShift;
19
+ if (offLeft < 0) {
20
+ x -= offLeft;
21
+ }
22
+ const offRight = alignedX + width - padding + horizontalAxisShift;
23
+ if (offRight > plotWidth) {
24
+ x += plotWidth - offRight;
25
+ }
26
+ const offTop = alignedY + padding + verticalAxisShift;
27
+ if (offTop < 0) {
28
+ if (input.verticalAlign === "bottom" && input.y >= 0) {
29
+ verticalAlign = "top";
30
+ }
31
+ else {
32
+ y -= offTop;
33
+ }
34
+ }
35
+ const offBottom = alignedY + height - padding + verticalAxisShift;
36
+ if (offBottom > plotHeight) {
37
+ if (input.verticalAlign === "top" && input.y <= 0) {
38
+ verticalAlign = "bottom";
39
+ }
40
+ else {
41
+ y += plotHeight - offBottom;
42
+ }
43
+ }
44
+ return {
45
+ x: alignX(x) + input.translateX + padding,
46
+ y: alignY(y, verticalAlign) + input.translateY + padding,
47
+ width: input.textWidth,
48
+ height: input.textHeight,
49
+ };
50
+ }
51
+ /** Same test as Highcharts' isIntersectRect in hideOverlappingLabels. */
52
+ export function areBoxesIntersecting(a, b) {
53
+ return !(b.x >= a.x + a.width || b.x + b.width <= a.x || b.y >= a.y + a.height || b.y + b.height <= a.y);
54
+ }
55
+ const textMetrics = new WeakMap();
56
+ const FONT_SIZE = /^(\d+(?:\.\d+)?)(px|em)$/;
57
+ export function getTextMetrics(chart) {
58
+ if (!textMetrics.has(chart)) {
59
+ const root = chart.renderer?.box;
60
+ const canvas = typeof document === "undefined" ? null : document.createElement("canvas").getContext("2d");
61
+ if (!root?.isConnected || !canvas) {
62
+ textMetrics.set(chart, null);
63
+ }
64
+ else {
65
+ // one style read per chart; the data labels inherit the font of the svg root
66
+ const style = window.getComputedStyle(root);
67
+ textMetrics.set(chart, {
68
+ canvas,
69
+ rootFontSize: parseFloat(style.getPropertyValue("font-size")),
70
+ fontFamily: style.getPropertyValue("font-family"),
71
+ textHeights: new Map(),
72
+ });
73
+ }
74
+ }
75
+ return textMetrics.get(chart) ?? null;
76
+ }
77
+ export function resolveFontSize(fontSize, rootFontSize) {
78
+ const match = FONT_SIZE.exec(String(fontSize ?? ""));
79
+ if (!match || Number.isNaN(rootFontSize)) {
80
+ return undefined;
81
+ }
82
+ const value = parseFloat(match[1]);
83
+ return match[2] === "px" ? value : value * rootFontSize;
84
+ }
85
+ /** Height of a single line text box; it does not depend on the text, so one probe per font is enough. */
86
+ export function getTextHeight(chart, metrics, fontSize, fontWeight) {
87
+ const key = `${fontSize}|${fontWeight}`;
88
+ let height = metrics.textHeights.get(key);
89
+ if (height === undefined) {
90
+ const probe = chart.renderer
91
+ .text("0", 0, 0)
92
+ .css({ fontSize: `${fontSize}px`, fontWeight })
93
+ .add();
94
+ height = probe.getBBox().height;
95
+ probe.destroy();
96
+ metrics.textHeights.set(key, height);
97
+ }
98
+ return height;
99
+ }
100
+ /** Forgets the measured fonts of the chart, for example after a web font loaded. */
101
+ export function resetTextMetrics(chart) {
102
+ textMetrics.delete(chart);
103
+ }
104
+ /**
105
+ * Label options the box model reproduces: a center aligned, unrotated SVG label without a box,
106
+ * justified into the plot area. Visibility options (enabled, allowOverlap) are not checked here.
107
+ */
108
+ export function isSupportedLabelPlacement(options) {
109
+ return (options &&
110
+ !Array.isArray(options) &&
111
+ (options.align ?? "center") === "center" &&
112
+ ["top", "middle", "bottom", undefined].includes(options.verticalAlign) &&
113
+ !options.rotation &&
114
+ !options.useHTML &&
115
+ !options.inside &&
116
+ !options.shape &&
117
+ !options.backgroundColor &&
118
+ !options.borderWidth &&
119
+ (options.overflow ?? "justify") === "justify" &&
120
+ options.format === undefined &&
121
+ options.filter === undefined &&
122
+ options.labelrank === undefined &&
123
+ options.alignTo === undefined &&
124
+ options.style?.width === undefined &&
125
+ options.style?.textOverflow === undefined);
126
+ }
127
+ /**
128
+ * hideOverlappingLabels compares the labels of the label collectors (plot lines and bands) and
129
+ * the stack labels before the data labels, so they could hide data labels.
130
+ */
131
+ export function hasOtherOverlappingLabels(chart) {
132
+ const hasCollectedLabels = (chart.labelCollectors ?? []).some((collect) => collect().length > 0);
133
+ const hasStackLabels = (chart.yAxis ?? []).some((axis) => Object.values(axis.stacking?.stacks ?? {}).some((stack) => Object.values(stack ?? {}).some((item) => item?.label)));
134
+ return hasCollectedLabels || hasStackLabels;
135
+ }
136
+ /** Whether hideOverlappingLabels collects the data labels of the series. */
137
+ export function isLabelledSeries(series) {
138
+ return Boolean(series?.visible && series.hasDataLabels?.());
139
+ }
140
+ /** Series with data labels, in the order hideOverlappingLabels collects their labels. */
141
+ export function getLabelledSeries(chart) {
142
+ return (chart.series ?? []).filter(isLabelledSeries);
143
+ }
144
+ /**
145
+ * Width of the SVG text box (getBBox) for text measured on a canvas. SVG text is laid out in
146
+ * 1/64 px units, and the box also contains glyph ink that is outside of the advance width,
147
+ * for example the right edge of the last glyph in some bold fonts.
148
+ */
149
+ export function getSvgTextWidth(metrics) {
150
+ const advance = Math.ceil(metrics.width * 64) / 64;
151
+ return (Math.max(advance, metrics.actualBoundingBoxRight ?? 0) +
152
+ Math.max(0, metrics.actualBoundingBoxLeft ?? 0));
153
+ }
154
+ /**
155
+ * Predicts the text box of the point's data label. The text width is the SVG text box width
156
+ * (see getSvgTextWidth).
157
+ */
158
+ export function predictPointLabelBox(series, point, labelOptions, textWidth, textHeight) {
159
+ const { chart } = series;
160
+ return predictLabelBox({
161
+ plotX: point.plotX,
162
+ plotY: point.plotY,
163
+ textWidth,
164
+ textHeight,
165
+ padding: labelOptions.padding ?? 0,
166
+ x: labelOptions.x ?? 0,
167
+ y: labelOptions.y ?? 0,
168
+ verticalAlign: labelOptions.verticalAlign ?? "bottom",
169
+ plotWidth: chart.plotWidth,
170
+ plotHeight: chart.plotHeight,
171
+ horizontalAxisShift: (series.xAxis?.left ?? chart.plotLeft) - chart.plotLeft,
172
+ verticalAxisShift: (series.yAxis?.top ?? chart.plotTop) - chart.plotTop,
173
+ translateX: series.xAxis?.left ?? chart.plotLeft,
174
+ translateY: series.yAxis?.top ?? chart.plotTop,
175
+ });
176
+ }
177
+ /** Highcharts hides the labels of points outside of the plot area (Series.alignDataLabel). */
178
+ export function isPointInsidePlot(series, point) {
179
+ const { plotX, plotY } = point;
180
+ return (plotX !== undefined &&
181
+ plotY !== undefined &&
182
+ series.chart.isInsidePlot(plotX, Math.round(plotY), { paneCoordinates: true, series }));
183
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Skips creating line chart data labels that Highcharts would hide as overlapping.
3
+ *
4
+ * With 'auto' data labels Highcharts creates a label for every point and then hides the ones that
5
+ * overlap a label before them (Chart.hideOverlappingLabels). On dense line charts most labels end
6
+ * up hidden, and creating them is most of the render time.
7
+ *
8
+ * The plugin runs the same overlap test before the labels are created. Label boxes are predicted
9
+ * from the point position and the text width measured on a canvas, which does not touch the
10
+ * layout. A label is skipped only when it overlaps a label that is predicted to stay visible.
11
+ * Highcharts still runs its own overlap test on the created labels.
12
+ *
13
+ * Only plain line charts are handled. When anything in a render pass cannot be predicted, the
14
+ * rest of the pass draws all labels, as before.
15
+ */
16
+ export declare function skipOverlappingLineDataLabels(Highcharts: any): void;
17
+ //# sourceMappingURL=skipOverlappingLineDataLabels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"skipOverlappingLineDataLabels.d.ts","sourceRoot":"","sources":["../../../../../src/highcharts/adapter/plugins/autohideLabels/skipOverlappingLineDataLabels.ts"],"names":[],"mappings":"AAoHA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,GAAG,GAAG,IAAI,CA0EnE"}
@@ -0,0 +1,144 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { areBoxesIntersecting, getLabelledSeries, getSvgTextWidth, getTextHeight, getTextMetrics, hasOtherOverlappingLabels, isLabelledSeries, isPointInsidePlot, isSupportedLabelPlacement, predictPointLabelBox, resolveFontSize, } from "./lineDataLabelLayout.js";
3
+ const passes = new WeakMap();
4
+ /** Charts that had labels skipped: their next redraw must draw the labels of every series again. */
5
+ const chartsWithSkippedLabels = new WeakSet();
6
+ // whitespace that SVG collapses but canvas measures, and markup that Highcharts parses
7
+ const UNMEASURABLE_TEXT = /^\s|\s$|\s\s|[<&]/;
8
+ function isSupportedLabelOptions(options) {
9
+ return options?.enabled !== false && !options?.allowOverlap && isSupportedLabelPlacement(options);
10
+ }
11
+ function isSupportedSeries(series) {
12
+ return (series.type === "line" &&
13
+ !series.options?.stacking &&
14
+ !series.options?.dataSorting?.enabled &&
15
+ isSupportedLabelOptions(series.options?.dataLabels));
16
+ }
17
+ function createRenderPass(chart) {
18
+ const series = getLabelledSeries(chart);
19
+ const enabled = chart.options?.chart?.type === "line" &&
20
+ !chart.inverted &&
21
+ !chart.polar &&
22
+ !chart.styledMode &&
23
+ series.length > 0 &&
24
+ series.every(isSupportedSeries) &&
25
+ !hasOtherOverlappingLabels(chart);
26
+ return { enabled, series, nextSeries: 0, boxes: [] };
27
+ }
28
+ /**
29
+ * Decides whether the label of the point can be visible, given the labels before it.
30
+ * Returns undefined when the label cannot be predicted; the rest of the pass then draws all labels.
31
+ */
32
+ function predictVisibility(pass, series, point, labelOptions, text) {
33
+ const { chart } = series;
34
+ if (typeof text !== "string" ||
35
+ UNMEASURABLE_TEXT.test(text) ||
36
+ point.options?.dataLabels ||
37
+ point.dlOptions ||
38
+ point.labelrank !== undefined ||
39
+ !isSupportedLabelOptions(labelOptions)) {
40
+ return undefined;
41
+ }
42
+ const metrics = getTextMetrics(chart);
43
+ if (!metrics) {
44
+ return undefined;
45
+ }
46
+ const fontSize = resolveFontSize(labelOptions.style?.fontSize, metrics.rootFontSize);
47
+ if (fontSize === undefined) {
48
+ return undefined;
49
+ }
50
+ // labels of points outside of the plot area are hidden; they take no part in overlapping
51
+ if (!isPointInsidePlot(series, point)) {
52
+ return true;
53
+ }
54
+ const fontWeight = String(labelOptions.style?.fontWeight ?? "normal");
55
+ metrics.canvas.font = `${fontWeight} ${fontSize}px ${metrics.fontFamily}`;
56
+ const box = predictPointLabelBox(series, point, labelOptions, getSvgTextWidth(metrics.canvas.measureText(text)), getTextHeight(chart, metrics, fontSize, fontWeight));
57
+ if (pass.boxes.some((kept) => areBoxesIntersecting(kept, box))) {
58
+ return false;
59
+ }
60
+ pass.boxes.push(box);
61
+ return true;
62
+ }
63
+ /**
64
+ * Skips creating line chart data labels that Highcharts would hide as overlapping.
65
+ *
66
+ * With 'auto' data labels Highcharts creates a label for every point and then hides the ones that
67
+ * overlap a label before them (Chart.hideOverlappingLabels). On dense line charts most labels end
68
+ * up hidden, and creating them is most of the render time.
69
+ *
70
+ * The plugin runs the same overlap test before the labels are created. Label boxes are predicted
71
+ * from the point position and the text width measured on a canvas, which does not touch the
72
+ * layout. A label is skipped only when it overlaps a label that is predicted to stay visible.
73
+ * Highcharts still runs its own overlap test on the created labels.
74
+ *
75
+ * Only plain line charts are handled. When anything in a render pass cannot be predicted, the
76
+ * rest of the pass draws all labels, as before.
77
+ */
78
+ export function skipOverlappingLineDataLabels(Highcharts) {
79
+ Highcharts.wrap(Highcharts.Series.prototype, "drawDataLabels", function (proceed, ...args) {
80
+ // series not collected by hideOverlappingLabels take no part in the pass or its series order
81
+ if (!isLabelledSeries(this)) {
82
+ return proceed.apply(this, args);
83
+ }
84
+ const chart = this.chart;
85
+ let pass = passes.get(chart);
86
+ if (!pass) {
87
+ pass = createRenderPass(chart);
88
+ passes.set(chart, pass);
89
+ }
90
+ const [points] = args;
91
+ const isNextSeries = pass.series[pass.nextSeries] === this;
92
+ if (isNextSeries) {
93
+ pass.nextSeries++;
94
+ }
95
+ // the labels must be predicted series by series, from the first one, for all points
96
+ if (!pass.enabled || !isNextSeries || (points && points !== this.points)) {
97
+ pass.enabled = false;
98
+ return proceed.apply(this, args);
99
+ }
100
+ const dataLabelsOptions = this.options.dataLabels;
101
+ const formatter = dataLabelsOptions.formatter;
102
+ const currentPass = pass;
103
+ // "this" is the point
104
+ dataLabelsOptions.formatter = function (labelOptions) {
105
+ const text = formatter.call(this, labelOptions);
106
+ if (!currentPass.enabled) {
107
+ return text;
108
+ }
109
+ const visible = predictVisibility(currentPass, this.series, this, labelOptions, text);
110
+ if (visible === undefined) {
111
+ currentPass.enabled = false;
112
+ return text;
113
+ }
114
+ if (!visible) {
115
+ chartsWithSkippedLabels.add(chart);
116
+ return undefined;
117
+ }
118
+ return text;
119
+ };
120
+ try {
121
+ return proceed.apply(this, args);
122
+ }
123
+ finally {
124
+ dataLabelsOptions.formatter = formatter;
125
+ }
126
+ });
127
+ Highcharts.wrap(Highcharts.Chart.prototype, "redraw", function (proceed, ...args) {
128
+ // Labels of a series that is not redrawn are not updated. A skipped label must appear
129
+ // when the labels before it change, so redraw the labels of all series.
130
+ if (chartsWithSkippedLabels.has(this)) {
131
+ chartsWithSkippedLabels.delete(this);
132
+ (this.series ?? []).forEach((series) => {
133
+ if (isLabelledSeries(series)) {
134
+ series.isDirty = true;
135
+ }
136
+ });
137
+ }
138
+ return proceed.apply(this, args);
139
+ });
140
+ // every render and redraw ends with the render event; the next one starts a new pass
141
+ Highcharts.addEvent(Highcharts.Chart, "render", function () {
142
+ passes.delete(this);
143
+ });
144
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Resolves the font size of an SVG element in px from inline styles only.
3
+ * Inline px is used as is. Inline em and % are multiplied up to the SVG root,
4
+ * whose computed font size is read once and cached.
5
+ * Returns undefined when the font size cannot be resolved this way, and for detached elements,
6
+ * where getComputedStyle returns an empty value.
7
+ */
8
+ export declare function resolveInlineFontSize(element: any): number | undefined;
9
+ /**
10
+ * Highcharts reads font-size through getComputedStyle for each text element
11
+ * (TextBuilder.buildSVG cache key, SVGRenderer.fontMetrics). Each read forces a synchronous
12
+ * style recalculation because the SVG just changed. In non-styled mode Highcharts writes
13
+ * the font size as an inline style, so resolve it from there and use getComputedStyle only as a fallback.
14
+ */
15
+ export declare function inlineFontSize(Highcharts: any): void;
16
+ //# sourceMappingURL=inlineFontSize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inlineFontSize.d.ts","sourceRoot":"","sources":["../../../../src/highcharts/adapter/plugins/inlineFontSize.ts"],"names":[],"mappings":"AAgBA;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CA0BtE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,GAAG,GAAG,IAAI,CAepD"}
@@ -0,0 +1,64 @@
1
+ // (C) 2026 GoodData Corporation
2
+ const FONT_SIZE = /^(\d+(?:\.\d+)?)(px|em|%)$/;
3
+ const svgRootFontSizes = new WeakMap();
4
+ function getSvgRootFontSize(svg) {
5
+ let fontSize = svgRootFontSizes.get(svg);
6
+ if (fontSize === undefined) {
7
+ // One forced style recalculation per chart instead of one per text element
8
+ fontSize = parseFloat(window.getComputedStyle(svg).getPropertyValue("font-size"));
9
+ svgRootFontSizes.set(svg, fontSize);
10
+ }
11
+ return fontSize;
12
+ }
13
+ /**
14
+ * Resolves the font size of an SVG element in px from inline styles only.
15
+ * Inline px is used as is. Inline em and % are multiplied up to the SVG root,
16
+ * whose computed font size is read once and cached.
17
+ * Returns undefined when the font size cannot be resolved this way, and for detached elements,
18
+ * where getComputedStyle returns an empty value.
19
+ */
20
+ export function resolveInlineFontSize(element) {
21
+ if (!element?.isConnected) {
22
+ return undefined;
23
+ }
24
+ let factor = 1;
25
+ let node = element;
26
+ while (node?.nodeType === 1) {
27
+ if (node.nodeName === "svg") {
28
+ const rootFontSize = getSvgRootFontSize(node);
29
+ return Number.isNaN(rootFontSize) ? undefined : factor * rootFontSize;
30
+ }
31
+ const fontSize = node.style?.fontSize;
32
+ if (fontSize) {
33
+ const match = FONT_SIZE.exec(fontSize);
34
+ if (!match) {
35
+ return undefined;
36
+ }
37
+ const value = parseFloat(match[1]);
38
+ if (match[2] === "px") {
39
+ return factor * value;
40
+ }
41
+ factor *= match[2] === "em" ? value : value / 100;
42
+ }
43
+ node = node.parentNode;
44
+ }
45
+ return undefined;
46
+ }
47
+ /**
48
+ * Highcharts reads font-size through getComputedStyle for each text element
49
+ * (TextBuilder.buildSVG cache key, SVGRenderer.fontMetrics). Each read forces a synchronous
50
+ * style recalculation because the SVG just changed. In non-styled mode Highcharts writes
51
+ * the font size as an inline style, so resolve it from there and use getComputedStyle only as a fallback.
52
+ */
53
+ export function inlineFontSize(Highcharts) {
54
+ Highcharts.wrap(Highcharts.SVGElement.prototype, "getStyle", function getStyle(proceed, prop) {
55
+ // "this" is an SVGElement wrapper, or a DOM node when called via getStyle.call(node)
56
+ if (prop === "font-size" && !this.renderer?.styledMode) {
57
+ const fontSize = resolveInlineFontSize(this.element || this);
58
+ if (fontSize !== undefined) {
59
+ return `${Math.round(fontSize * 1000) / 1000}px`;
60
+ }
61
+ }
62
+ return proceed.call(this, prop);
63
+ });
64
+ }
@@ -25,11 +25,11 @@ export function updateConfigWithSettings(config, settings) {
25
25
  if (!settings) {
26
26
  return {
27
27
  enableCompactSize: true,
28
- ...(config || {}),
28
+ ...config,
29
29
  };
30
30
  }
31
31
  const updatedConfig = {
32
- ...(config || {}),
32
+ ...config,
33
33
  enableCompactSize: true,
34
34
  ...(config?.enableJoinedAttributeAxisName === undefined
35
35
  ? {