@internetstiftelsen/charts 0.4.0 → 0.4.1

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/bar.js CHANGED
@@ -1,4 +1,4 @@
1
- import { sanitizeForCSS, mergeDeep } from './utils.js';
1
+ import { getContrastTextColor, sanitizeForCSS, mergeDeep } from './utils.js';
2
2
  export class Bar {
3
3
  constructor(config) {
4
4
  Object.defineProperty(this, "type", {
@@ -362,7 +362,7 @@ export class Bar {
362
362
  const fontSize = config.fontSize ?? theme.valueLabel.fontSize;
363
363
  const fontFamily = config.fontFamily ?? theme.valueLabel.fontFamily;
364
364
  const fontWeight = config.fontWeight ?? theme.valueLabel.fontWeight;
365
- const color = config.color ?? theme.valueLabel.color;
365
+ const defaultLabelColor = config.color ?? theme.valueLabel.color;
366
366
  const background = config.background ?? theme.valueLabel.background;
367
367
  const border = config.border ?? theme.valueLabel.border;
368
368
  const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
@@ -370,11 +370,14 @@ export class Bar {
370
370
  const labelGroup = plotGroup
371
371
  .append('g')
372
372
  .attr('class', `bar-value-labels-${sanitizeForCSS(this.dataKey)}`);
373
- data.forEach((d) => {
373
+ data.forEach((d, i) => {
374
374
  const categoryKey = String(d[xKey]);
375
375
  const value = parseValue(d[this.dataKey]);
376
376
  const valueText = String(value);
377
377
  const xPos = this.getScaledPosition(d, xKey, x, xScaleType);
378
+ const barColor = this.colorAdapter
379
+ ? this.colorAdapter(d, i)
380
+ : this.fill;
378
381
  // Calculate bar position based on stacking mode
379
382
  let barTop;
380
383
  let barBottom;
@@ -497,6 +500,9 @@ export class Bar {
497
500
  }
498
501
  tempText.remove();
499
502
  if (shouldRender) {
503
+ const labelColor = position === 'inside' && config.color === undefined
504
+ ? getContrastTextColor(barColor)
505
+ : defaultLabelColor;
500
506
  const group = labelGroup.append('g');
501
507
  if (position === 'outside') {
502
508
  // Draw rounded rectangle background
@@ -522,7 +528,7 @@ export class Bar {
522
528
  .style('font-size', `${fontSize}px`)
523
529
  .style('font-family', fontFamily)
524
530
  .style('font-weight', fontWeight)
525
- .style('fill', color)
531
+ .style('fill', labelColor)
526
532
  .style('pointer-events', 'none')
527
533
  .text(valueText);
528
534
  }
@@ -574,7 +580,7 @@ export class Bar {
574
580
  const fontSize = config.fontSize ?? theme.valueLabel.fontSize;
575
581
  const fontFamily = config.fontFamily ?? theme.valueLabel.fontFamily;
576
582
  const fontWeight = config.fontWeight ?? theme.valueLabel.fontWeight;
577
- const color = config.color ?? theme.valueLabel.color;
583
+ const defaultLabelColor = config.color ?? theme.valueLabel.color;
578
584
  const background = config.background ?? theme.valueLabel.background;
579
585
  const border = config.border ?? theme.valueLabel.border;
580
586
  const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
@@ -582,11 +588,14 @@ export class Bar {
582
588
  const labelGroup = plotGroup
583
589
  .append('g')
584
590
  .attr('class', `bar-value-labels-${sanitizeForCSS(this.dataKey)}`);
585
- data.forEach((d) => {
591
+ data.forEach((d, i) => {
586
592
  const categoryKey = String(d[xKey]);
587
593
  const value = parseValue(d[this.dataKey]);
588
594
  const valueText = String(value);
589
595
  const yPos = this.getScaledPosition(d, xKey, y, yScaleType);
596
+ const barColor = this.colorAdapter
597
+ ? this.colorAdapter(d, i)
598
+ : this.fill;
590
599
  // Calculate bar position based on stacking mode
591
600
  let barLeft;
592
601
  let barRight;
@@ -711,6 +720,9 @@ export class Bar {
711
720
  }
712
721
  tempText.remove();
713
722
  if (shouldRender) {
723
+ const labelColor = position === 'inside' && config.color === undefined
724
+ ? getContrastTextColor(barColor)
725
+ : defaultLabelColor;
714
726
  const group = labelGroup.append('g');
715
727
  if (position === 'outside') {
716
728
  // Draw rounded rectangle background
@@ -736,7 +748,7 @@ export class Bar {
736
748
  .style('font-size', `${fontSize}px`)
737
749
  .style('font-family', fontFamily)
738
750
  .style('font-weight', fontWeight)
739
- .style('fill', color)
751
+ .style('fill', labelColor)
740
752
  .style('pointer-events', 'none')
741
753
  .text(valueText);
742
754
  }
package/legend.d.ts CHANGED
@@ -16,7 +16,6 @@ export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
16
16
  setToggleCallback(callback: () => void): void;
17
17
  isSeriesVisible(dataKey: string): boolean;
18
18
  private getCheckmarkPath;
19
- private parseColor;
20
19
  /**
21
20
  * Returns the space required by the legend
22
21
  */
package/legend.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getSeriesColor } from './types.js';
2
- import { mergeDeep } from './utils.js';
2
+ import { getContrastTextColor, mergeDeep } from './utils.js';
3
3
  export class Legend {
4
4
  constructor(config) {
5
5
  Object.defineProperty(this, "type", {
@@ -83,29 +83,6 @@ export class Legend {
83
83
  const offsetY = size * 0.15;
84
84
  return `M ${4 * scale + offsetX} ${12 * scale + offsetY} L ${9 * scale + offsetX} ${17 * scale + offsetY} L ${20 * scale + offsetX} ${6 * scale + offsetY}`;
85
85
  }
86
- parseColor(color) {
87
- // Handle hex colors
88
- if (color.startsWith('#')) {
89
- const hex = color.slice(1);
90
- const r = parseInt(hex.slice(0, 2), 16);
91
- const g = parseInt(hex.slice(2, 4), 16);
92
- const b = parseInt(hex.slice(4, 6), 16);
93
- return { r, g, b };
94
- }
95
- // Handle rgb/rgba colors
96
- if (color.startsWith('rgb')) {
97
- const match = color.match(/\d+/g);
98
- if (match) {
99
- return {
100
- r: parseInt(match[0]),
101
- g: parseInt(match[1]),
102
- b: parseInt(match[2]),
103
- };
104
- }
105
- }
106
- // Default to black if we can't parse
107
- return { r: 0, g: 0, b: 0 };
108
- }
109
86
  /**
110
87
  * Returns the space required by the legend
111
88
  */
@@ -185,13 +162,7 @@ export class Legend {
185
162
  .append('path')
186
163
  .attr('d', this.getCheckmarkPath(boxSize))
187
164
  .attr('fill', 'none')
188
- .attr('stroke', (d) => {
189
- // Calculate luminance to determine if we need black or white checkmark
190
- const color = d.color;
191
- const rgb = this.parseColor(color);
192
- const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
193
- return luminance > 0.5 ? '#000' : '#fff';
194
- })
165
+ .attr('stroke', (d) => getContrastTextColor(d.color))
195
166
  .attr('stroke-width', 2)
196
167
  .attr('stroke-linecap', 'round')
197
168
  .attr('stroke-linejoin', 'round')
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.4.0",
2
+ "version": "0.4.1",
3
3
  "name": "@internetstiftelsen/charts",
4
4
  "type": "module",
5
5
  "sideEffects": false,
package/utils.d.ts CHANGED
@@ -60,4 +60,5 @@ export declare function breakWord(word: string, maxWidth: number, fontSize: stri
60
60
  * @returns Array of lines
61
61
  */
62
62
  export declare function wrapText(text: string, maxWidth: number, fontSize: string | number, fontFamily: string, fontWeight: string, svg: SVGSVGElement): string[];
63
+ export declare function getContrastTextColor(backgroundColor: string, lightTextColor?: string, darkTextColor?: string): string;
63
64
  export declare function mergeDeep<T extends Record<string, unknown>>(base: T, override?: Partial<T>): T;
package/utils.js CHANGED
@@ -168,6 +168,44 @@ export function wrapText(text, maxWidth, fontSize, fontFamily, fontWeight, svg)
168
168
  }
169
169
  return lines.length > 0 ? lines : [text];
170
170
  }
171
+ function parseColorToRGB(color) {
172
+ const normalizedColor = color.trim();
173
+ if (normalizedColor.startsWith('#')) {
174
+ const hex = normalizedColor.slice(1);
175
+ if (hex.length === 3) {
176
+ const r = parseInt(hex[0] + hex[0], 16);
177
+ const g = parseInt(hex[1] + hex[1], 16);
178
+ const b = parseInt(hex[2] + hex[2], 16);
179
+ return { r, g, b };
180
+ }
181
+ if (hex.length === 6) {
182
+ const r = parseInt(hex.slice(0, 2), 16);
183
+ const g = parseInt(hex.slice(2, 4), 16);
184
+ const b = parseInt(hex.slice(4, 6), 16);
185
+ return { r, g, b };
186
+ }
187
+ }
188
+ if (normalizedColor.startsWith('rgb')) {
189
+ const matches = normalizedColor.match(/\d+/g);
190
+ if (matches && matches.length >= 3) {
191
+ return {
192
+ r: parseInt(matches[0], 10),
193
+ g: parseInt(matches[1], 10),
194
+ b: parseInt(matches[2], 10),
195
+ };
196
+ }
197
+ }
198
+ // Fallback matches legend behavior: unknown colors are treated as dark.
199
+ return { r: 0, g: 0, b: 0 };
200
+ }
201
+ export function getContrastTextColor(backgroundColor, lightTextColor = '#fff', darkTextColor = '#000') {
202
+ const { r, g, b } = parseColorToRGB(backgroundColor);
203
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
204
+ if (luminance > 0.5) {
205
+ return darkTextColor;
206
+ }
207
+ return lightTextColor;
208
+ }
171
209
  export function mergeDeep(base, override) {
172
210
  if (!override) {
173
211
  return { ...base };