@opendata-ai/openchart-engine 7.10.0 → 7.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/dist/index.d.ts +20 -1
- package/dist/index.js +26 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/dimensions.test.ts +49 -4
- package/src/__tests__/legend.test.ts +51 -0
- package/src/compile.ts +5 -0
- package/src/index.ts +8 -0
- package/src/layout/dimensions.ts +39 -12
- package/src/layout/plan.ts +3 -2
- package/src/legend/compute.ts +7 -1
- package/src/legend/wrap.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opendata-ai/openchart-engine",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.11.0",
|
|
4
4
|
"description": "Headless compiler for openchart: spec validation, data compilation, scales, and layout",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Riley Hilliard",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"typecheck": "tsc --noEmit"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@opendata-ai/openchart-core": "7.
|
|
51
|
+
"@opendata-ai/openchart-core": "7.11.0",
|
|
52
52
|
"d3-array": "^3.2.0",
|
|
53
53
|
"d3-format": "^3.1.2",
|
|
54
54
|
"d3-interpolate": "^3.0.0",
|
|
@@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
4
4
|
import { compileChart } from '../compile';
|
|
5
5
|
import type { NormalizedChartSpec } from '../compiler/types';
|
|
6
6
|
import { computeDimensions } from '../layout/dimensions';
|
|
7
|
+
import { INLINE_TICK_OVERHANG_PAD } from '../layout/shared';
|
|
7
8
|
import { legendGap } from '../legend/wrap';
|
|
8
9
|
|
|
9
10
|
const baseSpec: NormalizedChartSpec = {
|
|
@@ -596,16 +597,60 @@ describe('computeDimensions', () => {
|
|
|
596
597
|
// topAxisGap). This means the chart area gains back ~axisMargin (6px)
|
|
597
598
|
// that would otherwise be redundant spacing.
|
|
598
599
|
//
|
|
599
|
-
// The top margin with legend includes:
|
|
600
|
-
// + inlineTickOverhang(17) instead of the no-legend
|
|
601
|
-
// Net: margin delta = 28 + 8 + 17 - 23 =
|
|
602
|
-
// If axisMargin were doubling up: 28 + 8 + 23 - 23 =
|
|
600
|
+
// The top margin with legend includes: gapAbove(6) + legendHeight(28)
|
|
601
|
+
// + legendGap(8) + inlineTickOverhang(17) instead of the no-legend
|
|
602
|
+
// topAxisGap(23). Net: margin delta = 6 + 28 + 8 + 17 - 23 = 36px.
|
|
603
|
+
// If axisMargin were doubling up: 6 + 28 + 8 + 23 - 23 = 42px.
|
|
603
604
|
const topMarginDelta = dimsWithTopLegend.margins.top - dimsNoLegend.margins.top;
|
|
604
605
|
expect(topMarginDelta).toBeLessThan(topLegend.bounds.height + legendGap(600) + 1);
|
|
605
606
|
// With a legend present, the chart area should still be shorter
|
|
606
607
|
expect(dimsWithTopLegend.chartArea.height).toBeLessThan(dimsNoLegend.chartArea.height);
|
|
607
608
|
});
|
|
608
609
|
|
|
610
|
+
it('returns effectiveAxisGap on the primary return path', () => {
|
|
611
|
+
const overhang = lightTheme.fonts.sizes.axisTick + INLINE_TICK_OVERHANG_PAD;
|
|
612
|
+
|
|
613
|
+
// With a top legend only the inline-tick overhang separates legend and
|
|
614
|
+
// chart area (placeLegend consumes this to lift the legend above it).
|
|
615
|
+
const withTopLegend = computeDimensions(
|
|
616
|
+
baseSpec,
|
|
617
|
+
{ width: 600, height: 400 },
|
|
618
|
+
topLegend,
|
|
619
|
+
lightTheme,
|
|
620
|
+
);
|
|
621
|
+
expect(withTopLegend.effectiveAxisGap).toBe(overhang);
|
|
622
|
+
|
|
623
|
+
// Without a top legend the full topAxisGap applies.
|
|
624
|
+
const noLegend = computeDimensions(
|
|
625
|
+
baseSpec,
|
|
626
|
+
{ width: 600, height: 400 },
|
|
627
|
+
emptyLegend,
|
|
628
|
+
lightTheme,
|
|
629
|
+
);
|
|
630
|
+
expect(noLegend.effectiveAxisGap).toBe(lightTheme.spacing.axisMargin + overhang);
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('returns effectiveAxisGap on the chrome-strip guardrail path', () => {
|
|
634
|
+
// A cramped height forces the guardrail early-return with stripped chrome.
|
|
635
|
+
const talkativeSpec: NormalizedChartSpec = {
|
|
636
|
+
...baseSpec,
|
|
637
|
+
chrome: {
|
|
638
|
+
title: { text: 'A chart title that occupies chrome space' },
|
|
639
|
+
subtitle: { text: 'A subtitle that gets stripped in compact chrome' },
|
|
640
|
+
source: { text: 'Source: somewhere' },
|
|
641
|
+
},
|
|
642
|
+
};
|
|
643
|
+
const dims = computeDimensions(
|
|
644
|
+
talkativeSpec,
|
|
645
|
+
{ width: 600, height: 120 },
|
|
646
|
+
topLegend,
|
|
647
|
+
lightTheme,
|
|
648
|
+
);
|
|
649
|
+
// Prove the guardrail fired: compact chrome drops the subtitle.
|
|
650
|
+
expect(dims.chrome.subtitle).toBeUndefined();
|
|
651
|
+
expect(dims.effectiveAxisGap).toBe(lightTheme.fonts.sizes.axisTick + INLINE_TICK_OVERHANG_PAD);
|
|
652
|
+
});
|
|
653
|
+
|
|
609
654
|
it('tightens legend gap on narrow viewports', () => {
|
|
610
655
|
const wideDims = computeDimensions(
|
|
611
656
|
baseSpec,
|
|
@@ -3,6 +3,7 @@ import { resolveTheme } from '@opendata-ai/openchart-core';
|
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
import { compileChart } from '../compile';
|
|
5
5
|
import type { NormalizedChartSpec } from '../compiler/types';
|
|
6
|
+
import { INLINE_TICK_OVERHANG_PAD } from '../layout/shared';
|
|
6
7
|
import { computeLegend } from '../legend/compute';
|
|
7
8
|
|
|
8
9
|
const specWithColor: NormalizedChartSpec = {
|
|
@@ -510,6 +511,56 @@ describe('computeLegend', () => {
|
|
|
510
511
|
const gap = layout.area.y - legendBottom;
|
|
511
512
|
expect(gap).toBe(0);
|
|
512
513
|
});
|
|
514
|
+
|
|
515
|
+
// Inline y ticks (line/area) draw their topmost label ABOVE chartArea.y,
|
|
516
|
+
// inside the inlineTickOverhang zone the margin stack reserved. The legend
|
|
517
|
+
// must sit above that zone, not flush against chartArea.y.
|
|
518
|
+
const inlineTopLegendSpec = {
|
|
519
|
+
mark: 'line' as const,
|
|
520
|
+
data: [
|
|
521
|
+
{ date: '2020-01-01', value: 10, country: 'US' },
|
|
522
|
+
{ date: '2021-01-01', value: 40, country: 'US' },
|
|
523
|
+
{ date: '2020-01-01', value: 15, country: 'UK' },
|
|
524
|
+
{ date: '2021-01-01', value: 35, country: 'UK' },
|
|
525
|
+
],
|
|
526
|
+
encoding: {
|
|
527
|
+
x: { field: 'date', type: 'temporal' as const },
|
|
528
|
+
y: { field: 'value', type: 'quantitative' as const },
|
|
529
|
+
color: { field: 'country', type: 'nominal' as const },
|
|
530
|
+
},
|
|
531
|
+
legend: { show: true, position: 'top' as const },
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
it('top legend clears the inline y-tick overhang at compact width', () => {
|
|
535
|
+
const layout = compileChart(inlineTopLegendSpec, { width: 360, height: 400 });
|
|
536
|
+
|
|
537
|
+
expect(layout.legend.position).toBe('top');
|
|
538
|
+
expect(layout.legend.entries.length).toBeGreaterThan(0);
|
|
539
|
+
|
|
540
|
+
const inlineTickOverhang = layout.theme.fonts.sizes.axisTick + INLINE_TICK_OVERHANG_PAD;
|
|
541
|
+
const legendBottom = layout.legend.bounds.y + layout.legend.bounds.height;
|
|
542
|
+
expect(legendBottom).toBeLessThanOrEqual(layout.area.y - inlineTickOverhang + 1e-6);
|
|
543
|
+
|
|
544
|
+
// Pinned placement. Before the fix the legend bottom sat flush at
|
|
545
|
+
// chartArea.y (legend bottom was 62 = chartArea.y), overlapping the
|
|
546
|
+
// topmost inline tick label; it now sits the 17px overhang higher,
|
|
547
|
+
// while the 6px minimum gap above the legend shifts chartArea down.
|
|
548
|
+
expect(layout.legend.bounds.y).toBe(31);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('gutter y-ticks keep the legend flush at the legendGap (no overhang shift)', () => {
|
|
552
|
+
// Bar charts use gutter y-tick labels, so inlineTickOverhang is 0 and
|
|
553
|
+
// the legend keeps its legendGap-only offset from the chart area.
|
|
554
|
+
const layout = compileChart(topLegendSpec, { width: 600, height: 400 });
|
|
555
|
+
|
|
556
|
+
const legendBottom = layout.legend.bounds.y + layout.legend.bounds.height;
|
|
557
|
+
expect(layout.area.y - legendBottom).toBe(8);
|
|
558
|
+
|
|
559
|
+
// Pinned placement: 17 before the placement fix; the +6 comes from the
|
|
560
|
+
// TOP_LEGEND_GAP_ABOVE margin (not from any overhang shift — gutter
|
|
561
|
+
// ticks have no overhang, so the legend-to-chart gap above stays 8).
|
|
562
|
+
expect(layout.legend.bounds.y).toBe(23);
|
|
563
|
+
});
|
|
513
564
|
});
|
|
514
565
|
|
|
515
566
|
describe('entryPositions contract', () => {
|
package/src/compile.ts
CHANGED
|
@@ -640,12 +640,15 @@ export function compileChart(spec: unknown, options: CompileOptions): ChartLayou
|
|
|
640
640
|
const chartArea = options.frozenChartArea ?? dims.chartArea;
|
|
641
641
|
|
|
642
642
|
// Place legend in its final position relative to the computed chart area.
|
|
643
|
+
// effectiveAxisGap lifts a top legend above the inline-tick overhang zone
|
|
644
|
+
// the margin stack reserved below it.
|
|
643
645
|
const finalLegend = placeLegend(
|
|
644
646
|
plan.legendContent,
|
|
645
647
|
chartArea,
|
|
646
648
|
options.width,
|
|
647
649
|
theme,
|
|
648
650
|
plan.xAxisExtent,
|
|
651
|
+
dims.effectiveAxisGap,
|
|
649
652
|
);
|
|
650
653
|
|
|
651
654
|
// Inline y-axis labels: inset the x-scale range start by the widest tick
|
|
@@ -898,12 +901,14 @@ function compileFaceted(
|
|
|
898
901
|
);
|
|
899
902
|
const chartArea = dims.chartArea;
|
|
900
903
|
|
|
904
|
+
// Same effectiveAxisGap threading as the regular (non-faceted) path.
|
|
901
905
|
const finalLegend = placeLegend(
|
|
902
906
|
plan.legendContent,
|
|
903
907
|
chartArea,
|
|
904
908
|
options.width,
|
|
905
909
|
theme,
|
|
906
910
|
plan.xAxisExtent,
|
|
911
|
+
dims.effectiveAxisGap,
|
|
907
912
|
);
|
|
908
913
|
|
|
909
914
|
// Estimate y-axis gutter width (used as left reservation for outer axes)
|
package/src/index.ts
CHANGED
|
@@ -112,6 +112,14 @@ export {
|
|
|
112
112
|
registerChartRenderer,
|
|
113
113
|
} from './charts/registry';
|
|
114
114
|
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Legend geometry
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
// Used by the vanilla mount to compute the top-legend block when growing
|
|
120
|
+
// auto-height figures; exporting it keeps the gap math single-sourced.
|
|
121
|
+
export { legendGap } from './legend/wrap';
|
|
122
|
+
|
|
115
123
|
// ---------------------------------------------------------------------------
|
|
116
124
|
// Data transforms
|
|
117
125
|
// ---------------------------------------------------------------------------
|
package/src/layout/dimensions.ts
CHANGED
|
@@ -44,7 +44,7 @@ import { format as d3Format } from 'd3-format';
|
|
|
44
44
|
import type { NormalizedChartSpec } from '../compiler/types';
|
|
45
45
|
import { isEndsBoth, predictEndpointLabelsWidth } from '../endpoint-labels/predict';
|
|
46
46
|
import { countColorSeries, resolveSuppression } from '../legend/suppression';
|
|
47
|
-
import { legendGap } from '../legend/wrap';
|
|
47
|
+
import { legendGap, TOP_LEGEND_GAP_ABOVE } from '../legend/wrap';
|
|
48
48
|
import { yTickPositionIsInline } from './axes';
|
|
49
49
|
import { computeMetricBar, type MetricFontSizes, metricBarHeight } from './metrics';
|
|
50
50
|
import type { LayoutPlan } from './plan';
|
|
@@ -82,6 +82,14 @@ export interface LayoutDimensions {
|
|
|
82
82
|
* second legend pass) can position elements below the axis row.
|
|
83
83
|
*/
|
|
84
84
|
xAxisHeight: number;
|
|
85
|
+
/**
|
|
86
|
+
* The axis gap the margin stack placed between the top legend (or chrome,
|
|
87
|
+
* when there is no top legend) and the chart area. With a top legend this
|
|
88
|
+
* is the inline-tick overhang, and `placeLegend` must receive it as
|
|
89
|
+
* `axisGapBelowLegend` so the legend sits above the reserved tick zone
|
|
90
|
+
* instead of flush against `chartArea.y` (where inline y-tick labels draw).
|
|
91
|
+
*/
|
|
92
|
+
effectiveAxisGap: number;
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
/** Minimum chart area dimensions before guardrails kick in. */
|
|
@@ -254,7 +262,15 @@ export function computeDimensions(
|
|
|
254
262
|
height: Math.max(0, height - margins.top - margins.bottom),
|
|
255
263
|
};
|
|
256
264
|
|
|
257
|
-
return {
|
|
265
|
+
return {
|
|
266
|
+
total,
|
|
267
|
+
chrome,
|
|
268
|
+
chartArea,
|
|
269
|
+
margins,
|
|
270
|
+
theme,
|
|
271
|
+
xAxisHeight: xAxisSpace,
|
|
272
|
+
effectiveAxisGap: 0,
|
|
273
|
+
};
|
|
258
274
|
}
|
|
259
275
|
|
|
260
276
|
// Start with the total rect
|
|
@@ -664,18 +680,23 @@ export function computeDimensions(
|
|
|
664
680
|
if (legendPos === 'right' || legendPos === 'bottom-right') {
|
|
665
681
|
margins.right += legendBoundsWidth + 8;
|
|
666
682
|
} else if (legendPos === 'top') {
|
|
667
|
-
|
|
683
|
+
// TOP_LEGEND_GAP_ABOVE keeps the legend from sitting flush against the
|
|
684
|
+
// subtitle/metric bar now that the axis gap sits BELOW the legend.
|
|
685
|
+
margins.top += TOP_LEGEND_GAP_ABOVE + legendHeight + gap;
|
|
668
686
|
}
|
|
669
687
|
// 'bottom' is intentionally not handled here -- see bottomLegendReservation
|
|
670
688
|
// above.
|
|
671
689
|
}
|
|
672
690
|
|
|
673
|
-
//
|
|
674
|
-
// chart area. When a top legend is present, the legendGap already
|
|
675
|
-
// breathing room, so only the inlineTickOverhang is needed (the
|
|
676
|
-
// component would double up with legendGap). Without a top
|
|
677
|
-
// full topAxisGap (axisMargin + inlineTickOverhang) applies.
|
|
678
|
-
|
|
691
|
+
// effectiveAxisGap sits between the legend (or chrome, if no legend) and
|
|
692
|
+
// the chart area. When a top legend is present, the legendGap already
|
|
693
|
+
// provides breathing room, so only the inlineTickOverhang is needed (the
|
|
694
|
+
// axisMargin component would double up with legendGap). Without a top
|
|
695
|
+
// legend, the full topAxisGap (axisMargin + inlineTickOverhang) applies.
|
|
696
|
+
// The value is returned so placeLegend can position the top legend above
|
|
697
|
+
// this reserved zone (inline y-tick labels draw inside it).
|
|
698
|
+
const effectiveAxisGap = hasTopLegend ? inlineTickOverhang : topAxisGap;
|
|
699
|
+
margins.top += effectiveAxisGap;
|
|
679
700
|
|
|
680
701
|
// Chart area is what's left after margins
|
|
681
702
|
let chartArea: Rect = {
|
|
@@ -719,7 +740,10 @@ export function computeDimensions(
|
|
|
719
740
|
|
|
720
741
|
if (topDelta > 0 || bottomDelta > 0) {
|
|
721
742
|
const gap = legendGap(width);
|
|
722
|
-
margins.top =
|
|
743
|
+
margins.top =
|
|
744
|
+
newTop +
|
|
745
|
+
(hasTopLegend ? TOP_LEGEND_GAP_ABOVE + legendHeight + gap : 0) +
|
|
746
|
+
fallbackEffectiveAxisGap;
|
|
723
747
|
margins.bottom = newBottom;
|
|
724
748
|
|
|
725
749
|
chartArea = {
|
|
@@ -765,6 +789,7 @@ export function computeDimensions(
|
|
|
765
789
|
theme,
|
|
766
790
|
metrics: fallbackMetrics,
|
|
767
791
|
xAxisHeight,
|
|
792
|
+
effectiveAxisGap: fallbackEffectiveAxisGap,
|
|
768
793
|
};
|
|
769
794
|
}
|
|
770
795
|
}
|
|
@@ -773,8 +798,9 @@ export function computeDimensions(
|
|
|
773
798
|
// topPad
|
|
774
799
|
// chrome.topHeight (title / subtitle / eyebrow)
|
|
775
800
|
// tentativeMetricsHeight (KPI bar — placed here)
|
|
776
|
-
// [optional top legend band]
|
|
777
|
-
//
|
|
801
|
+
// [optional top legend band (TOP_LEGEND_GAP_ABOVE + legend + legendGap)]
|
|
802
|
+
// effectiveAxisGap (inlineTickOverhang with a top legend,
|
|
803
|
+
// else axisMargin + inlineTickOverhang)
|
|
778
804
|
// chartArea
|
|
779
805
|
// The metric bar belongs with chrome, above the legend, so its y is
|
|
780
806
|
// computed off chrome.topHeight only — not the full legend-inclusive
|
|
@@ -801,6 +827,7 @@ export function computeDimensions(
|
|
|
801
827
|
theme,
|
|
802
828
|
metrics: resolvedMetrics,
|
|
803
829
|
xAxisHeight,
|
|
830
|
+
effectiveAxisGap,
|
|
804
831
|
};
|
|
805
832
|
}
|
|
806
833
|
|
package/src/layout/plan.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { format as d3Format } from 'd3-format';
|
|
|
32
32
|
import type { NormalizedChartSpec } from '../compiler/types';
|
|
33
33
|
import { predictEndpointLabelsWidth } from '../endpoint-labels/predict';
|
|
34
34
|
import { computeLegendContent, type LegendContent } from '../legend/compute';
|
|
35
|
-
import { legendGap } from '../legend/wrap';
|
|
35
|
+
import { legendGap, TOP_LEGEND_GAP_ABOVE } from '../legend/wrap';
|
|
36
36
|
import { yTickPositionIsInline } from './axes';
|
|
37
37
|
import { resolveBandTickAngle } from './axes/rotation';
|
|
38
38
|
import { buildContinuousTicks, scaleSupportsTickCount, targetTickCount } from './axes/ticks';
|
|
@@ -395,7 +395,8 @@ export function resolveLayoutPlan(
|
|
|
395
395
|
const effectiveTopAxisGap = hasTopLegend ? inlineTickOverhang : topAxisGap;
|
|
396
396
|
|
|
397
397
|
let topMargin = topPad + chrome.topHeight;
|
|
398
|
-
|
|
398
|
+
// Mirrors the dimensions.ts margin stack: min gap above the top legend.
|
|
399
|
+
if (hasTopLegend) topMargin += TOP_LEGEND_GAP_ABOVE + legendContent.height + gap;
|
|
399
400
|
topMargin += effectiveTopAxisGap;
|
|
400
401
|
|
|
401
402
|
const bMargin = bottomMargin(chrome.bottomHeight, padding, xAxisExtent);
|
package/src/legend/compute.ts
CHANGED
|
@@ -494,6 +494,11 @@ export function computeLegendContent(
|
|
|
494
494
|
* @param containerWidth - Full container width, used for responsive legend gap.
|
|
495
495
|
* @param _theme - Resolved theme (reserved for future use).
|
|
496
496
|
* @param xAxisHeight - Height of the x-axis, used for bottom legend positioning.
|
|
497
|
+
* @param axisGapBelowLegend - The `effectiveAxisGap` the margin stack reserved
|
|
498
|
+
* between a top legend and the chart area (inline y-tick labels draw in that
|
|
499
|
+
* zone, above `chartArea.y`). The top legend is lifted above it so it
|
|
500
|
+
* occupies its reserved slot instead of colliding with the topmost tick
|
|
501
|
+
* label. Defaults to 0 so other callers/positions are unaffected.
|
|
497
502
|
* @returns Full LegendLayout with bounds.
|
|
498
503
|
*/
|
|
499
504
|
export function placeLegend(
|
|
@@ -502,6 +507,7 @@ export function placeLegend(
|
|
|
502
507
|
containerWidth: number,
|
|
503
508
|
_theme: ResolvedTheme,
|
|
504
509
|
xAxisHeight: number,
|
|
510
|
+
axisGapBelowLegend: number = 0,
|
|
505
511
|
): LegendLayout {
|
|
506
512
|
const { position, entries, labelStyle, legendWidth, height, offset } = content;
|
|
507
513
|
|
|
@@ -538,7 +544,7 @@ export function placeLegend(
|
|
|
538
544
|
} else {
|
|
539
545
|
// top
|
|
540
546
|
boundsX = chartArea.x + offsetDx;
|
|
541
|
-
boundsY = chartArea.y - gap - height + offsetDy;
|
|
547
|
+
boundsY = chartArea.y - axisGapBelowLegend - gap - height + offsetDy;
|
|
542
548
|
}
|
|
543
549
|
|
|
544
550
|
const bounds = { x: boundsX, y: boundsY, width: legendWidth, height };
|
package/src/legend/wrap.ts
CHANGED
|
@@ -34,6 +34,14 @@ export const ENTRY_GAP_COMPACT = 10;
|
|
|
34
34
|
/** Default gap between legend bounds and chart area. Zero on narrow viewports. */
|
|
35
35
|
export const LEGEND_GAP = 8;
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Minimum gap between the chrome/metrics block above and a top legend.
|
|
39
|
+
* Without it the legend sits flush against the subtitle (compact widths)
|
|
40
|
+
* or the metric bar (all widths) once the inline-tick overhang is placed
|
|
41
|
+
* below the legend where it belongs.
|
|
42
|
+
*/
|
|
43
|
+
export const TOP_LEGEND_GAP_ABOVE = 6;
|
|
44
|
+
|
|
37
45
|
/** Gap between legend and chart area, responsive to container width. */
|
|
38
46
|
export function legendGap(width: number): number {
|
|
39
47
|
return width < COMPACT_WIDTH ? 0 : LEGEND_GAP;
|