@internetstiftelsen/charts 0.0.3 → 0.0.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.
package/bar.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Selection } from 'd3';
2
- import type { BarConfig, DataItem, ScaleType, Orientation } from './types.js';
2
+ import type { BarConfig, DataItem, ScaleType, Orientation, ChartTheme } from './types.js';
3
3
  import type { ChartComponent } from './chart-interface.js';
4
4
  export declare class Bar implements ChartComponent {
5
5
  readonly type: "bar";
@@ -7,7 +7,7 @@ export declare class Bar implements ChartComponent {
7
7
  readonly fill: string;
8
8
  readonly orientation: Orientation;
9
9
  constructor(config: BarConfig);
10
- render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: any, y: any, parseValue: (value: any) => number, xScaleType?: ScaleType): void;
10
+ render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: any, y: any, parseValue: (value: any) => number, xScaleType?: ScaleType, _theme?: ChartTheme): void;
11
11
  private renderVertical;
12
12
  private renderHorizontal;
13
13
  }
package/bar.js CHANGED
@@ -28,7 +28,7 @@ export class Bar {
28
28
  this.fill = config.fill || '#8884d8';
29
29
  this.orientation = config.orientation || 'vertical';
30
30
  }
31
- render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band') {
31
+ render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', _theme) {
32
32
  if (this.orientation === 'vertical') {
33
33
  this.renderVertical(plotGroup, data, xKey, x, y, parseValue, xScaleType);
34
34
  }
package/base-chart.js CHANGED
@@ -146,13 +146,13 @@ export class BaseChart {
146
146
  if (!this.container)
147
147
  return;
148
148
  // Calculate current width
149
- this.width = Math.min(this.container.getBoundingClientRect().width || this.theme.width, this.theme.width);
149
+ this.width =
150
+ this.container.getBoundingClientRect().width || this.theme.width;
150
151
  // Clear and setup SVG
151
152
  this.container.innerHTML = '';
152
153
  this.svg = create('svg')
153
154
  .attr('width', '100%')
154
155
  .attr('height', this.theme.height)
155
- .style('max-width', `${this.theme.width}px`)
156
156
  .style('display', 'block');
157
157
  this.container.appendChild(this.svg.node());
158
158
  // Calculate layout
package/grid.js CHANGED
@@ -39,8 +39,8 @@ export class Grid {
39
39
  .tickFormat(() => ''))
40
40
  .call((g) => g
41
41
  .selectAll('.tick line')
42
- .attr('stroke', theme.gridColor)
43
- .attr('stroke-opacity', 0.5))
42
+ .attr('stroke', theme.grid.color)
43
+ .attr('stroke-opacity', theme.grid.opacity))
44
44
  .selectAll('.domain')
45
45
  .remove();
46
46
  }
@@ -54,8 +54,8 @@ export class Grid {
54
54
  .tickFormat(() => ''))
55
55
  .call((g) => g
56
56
  .selectAll('.tick line')
57
- .attr('stroke', theme.gridColor)
58
- .attr('stroke-opacity', 0.5))
57
+ .attr('stroke', theme.grid.color)
58
+ .attr('stroke-opacity', theme.grid.opacity))
59
59
  .selectAll('.domain')
60
60
  .remove();
61
61
  }
package/legend.d.ts CHANGED
@@ -15,6 +15,7 @@ export declare class Legend implements LayoutAwareComponent {
15
15
  setToggleCallback(callback: () => void): void;
16
16
  isSeriesVisible(dataKey: string): boolean;
17
17
  private getCheckmarkPath;
18
+ private parseColor;
18
19
  /**
19
20
  * Returns the space required by the legend
20
21
  */
package/legend.js CHANGED
@@ -57,6 +57,29 @@ export class Legend {
57
57
  const scale = size / 24;
58
58
  return `M ${4 * scale} ${12 * scale} L ${9 * scale} ${17 * scale} L ${20 * scale} ${6 * scale}`;
59
59
  }
60
+ parseColor(color) {
61
+ // Handle hex colors
62
+ if (color.startsWith('#')) {
63
+ const hex = color.slice(1);
64
+ const r = parseInt(hex.slice(0, 2), 16);
65
+ const g = parseInt(hex.slice(2, 4), 16);
66
+ const b = parseInt(hex.slice(4, 6), 16);
67
+ return { r, g, b };
68
+ }
69
+ // Handle rgb/rgba colors
70
+ if (color.startsWith('rgb')) {
71
+ const match = color.match(/\d+/g);
72
+ if (match) {
73
+ return {
74
+ r: parseInt(match[0]),
75
+ g: parseInt(match[1]),
76
+ b: parseInt(match[2]),
77
+ };
78
+ }
79
+ }
80
+ // Default to black if we can't parse
81
+ return { r: 0, g: 0, b: 0 };
82
+ }
60
83
  /**
61
84
  * Returns the space required by the legend
62
85
  */
@@ -136,7 +159,13 @@ export class Legend {
136
159
  .append('path')
137
160
  .attr('d', this.getCheckmarkPath(boxSize))
138
161
  .attr('fill', 'none')
139
- .attr('stroke', '#000')
162
+ .attr('stroke', (d) => {
163
+ // Calculate luminance to determine if we need black or white checkmark
164
+ const color = d.color;
165
+ const rgb = this.parseColor(color);
166
+ const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
167
+ return luminance > 0.5 ? '#000' : '#fff';
168
+ })
140
169
  .attr('stroke-width', 2)
141
170
  .attr('stroke-linecap', 'round')
142
171
  .attr('stroke-linejoin', 'round')
package/line.d.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  import { type Selection } from 'd3';
2
- import type { LineConfig, DataItem, ScaleType } from './types.js';
2
+ import type { LineConfig, DataItem, ScaleType, ChartTheme } from './types.js';
3
3
  import type { ChartComponent } from './chart-interface.js';
4
4
  export declare class Line implements ChartComponent {
5
5
  readonly type: "line";
6
6
  readonly dataKey: string;
7
7
  readonly stroke: string;
8
- readonly strokeWidth: number;
8
+ readonly strokeWidth?: number;
9
9
  constructor(config: LineConfig);
10
- render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: any, y: any, parseValue: (value: any) => number, xScaleType?: ScaleType): void;
10
+ render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: any, y: any, parseValue: (value: any) => number, xScaleType: ScaleType | undefined, theme: ChartTheme): void;
11
11
  }
package/line.js CHANGED
@@ -27,9 +27,9 @@ export class Line {
27
27
  });
28
28
  this.dataKey = config.dataKey;
29
29
  this.stroke = config.stroke || '#8884d8';
30
- this.strokeWidth = config.strokeWidth || 2;
30
+ this.strokeWidth = config.strokeWidth;
31
31
  }
32
- render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band') {
32
+ render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme) {
33
33
  const getXPosition = (d) => {
34
34
  const xValue = d[xKey];
35
35
  // Handle different scale types appropriately
@@ -58,13 +58,18 @@ export class Line {
58
58
  const lineGenerator = line()
59
59
  .x(getXPosition)
60
60
  .y((d) => y(parseValue(d[this.dataKey])) || 0);
61
+ const lineStrokeWidth = this.strokeWidth ?? theme.line.strokeWidth;
62
+ const pointSize = theme.line.point.size;
63
+ const pointStrokeWidth = theme.line.point.strokeWidth;
64
+ const pointStrokeColor = theme.line.point.strokeColor || this.stroke;
65
+ const pointColor = theme.line.point.color || this.stroke;
61
66
  // Add line path
62
67
  plotGroup
63
68
  .append('path')
64
69
  .datum(data)
65
70
  .attr('fill', 'none')
66
71
  .attr('stroke', this.stroke)
67
- .attr('stroke-width', this.strokeWidth)
72
+ .attr('stroke-width', lineStrokeWidth)
68
73
  .attr('d', lineGenerator);
69
74
  // Add data point circles
70
75
  plotGroup
@@ -74,9 +79,9 @@ export class Line {
74
79
  .attr('class', `circle-${this.dataKey.replace(/\s+/g, '-')}`)
75
80
  .attr('cx', getXPosition)
76
81
  .attr('cy', (d) => y(parseValue(d[this.dataKey])) || 0)
77
- .attr('r', 4)
78
- .attr('fill', this.stroke)
79
- .attr('stroke', 'white')
80
- .attr('stroke-width', 2);
82
+ .attr('r', pointSize)
83
+ .attr('fill', pointColor)
84
+ .attr('stroke', pointStrokeColor)
85
+ .attr('stroke-width', pointStrokeWidth);
81
86
  }
82
87
  }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.3",
2
+ "version": "0.0.4",
3
3
  "name": "@internetstiftelsen/charts",
4
4
  "type": "module",
5
5
  "sideEffects": false,
package/theme.d.ts CHANGED
@@ -1,2 +1,7 @@
1
1
  import { type ChartTheme } from './types.js';
2
2
  export declare const defaultTheme: ChartTheme;
3
+ export declare const newspaperTheme: ChartTheme;
4
+ export declare const themes: {
5
+ default: ChartTheme;
6
+ newspaper: ChartTheme;
7
+ };
package/theme.js CHANGED
@@ -1,13 +1,17 @@
1
1
  export const defaultTheme = {
2
2
  width: 928,
3
3
  height: 600,
4
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
4
5
  margins: {
5
6
  top: 20,
6
7
  right: 20,
7
8
  bottom: 30,
8
9
  left: 40,
9
10
  },
10
- gridColor: '#e0e0e0',
11
+ grid: {
12
+ color: '#e0e0e0',
13
+ opacity: 0.5,
14
+ },
11
15
  colorPalette: [
12
16
  '#50b2fc', // ocean
13
17
  '#ff4069', // ruby
@@ -21,10 +25,67 @@ export const defaultTheme = {
21
25
  axis: {
22
26
  fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"',
23
27
  fontSize: '14',
28
+ fontWeight: 'normal',
24
29
  },
25
30
  legend: {
26
31
  boxSize: 20,
27
32
  uncheckedColor: '#d0d0d0',
28
33
  fontSize: 14,
29
34
  },
35
+ line: {
36
+ strokeWidth: 4,
37
+ point: {
38
+ strokeWidth: 3,
39
+ color: 'white',
40
+ size: 5,
41
+ },
42
+ },
43
+ };
44
+ export const newspaperTheme = {
45
+ width: 928,
46
+ height: 600,
47
+ fontFamily: 'Georgia, "Times New Roman", Times, serif',
48
+ margins: {
49
+ top: 20,
50
+ right: 20,
51
+ bottom: 30,
52
+ left: 40,
53
+ },
54
+ grid: {
55
+ color: '#2c2c2c',
56
+ opacity: 0.15,
57
+ },
58
+ colorPalette: [
59
+ '#1a1a1a', // ink black
60
+ '#8b0000', // dark red
61
+ '#4a4a4a', // charcoal
62
+ '#6b4423', // sepia brown
63
+ '#2f4f4f', // dark slate
64
+ '#556b2f', // dark olive
65
+ '#8b4513', // saddle brown
66
+ '#708090', // slate gray
67
+ ],
68
+ axis: {
69
+ fontFamily: 'Georgia, "Times New Roman", Times, serif',
70
+ fontSize: '13',
71
+ fontWeight: '600',
72
+ },
73
+ legend: {
74
+ boxSize: 18,
75
+ uncheckedColor: '#d3d3d3',
76
+ fontSize: 13,
77
+ },
78
+ line: {
79
+ strokeWidth: 2.5,
80
+ point: {
81
+ strokeWidth: 1.5,
82
+ strokeColor: '#1a1a1a',
83
+ color: '#f5f5dc',
84
+ size: 5,
85
+ },
86
+ },
87
+ };
88
+ export const themes = {
89
+ default: defaultTheme,
90
+ newspaper: newspaperTheme,
30
91
  };
package/title.d.ts CHANGED
@@ -6,6 +6,7 @@ export declare class Title implements LayoutAwareComponent {
6
6
  readonly text: string;
7
7
  private readonly fontSize;
8
8
  private readonly fontWeight;
9
+ private readonly fontFamily?;
9
10
  private readonly align;
10
11
  private readonly marginTop;
11
12
  private readonly marginBottom;
package/title.js CHANGED
@@ -24,6 +24,12 @@ export class Title {
24
24
  writable: true,
25
25
  value: void 0
26
26
  });
27
+ Object.defineProperty(this, "fontFamily", {
28
+ enumerable: true,
29
+ configurable: true,
30
+ writable: true,
31
+ value: void 0
32
+ });
27
33
  Object.defineProperty(this, "align", {
28
34
  enumerable: true,
29
35
  configurable: true,
@@ -45,6 +51,7 @@ export class Title {
45
51
  this.text = config.text;
46
52
  this.fontSize = config.fontSize ?? 18;
47
53
  this.fontWeight = config.fontWeight ?? 'bold';
54
+ this.fontFamily = config.fontFamily;
48
55
  this.align = config.align ?? 'center';
49
56
  this.marginTop = config.marginTop ?? 10;
50
57
  this.marginBottom = config.marginBottom ?? 15;
@@ -81,7 +88,7 @@ export class Title {
81
88
  .attr('text-anchor', textAnchor)
82
89
  .attr('font-size', `${this.fontSize}px`)
83
90
  .attr('font-weight', this.fontWeight)
84
- .attr('font-family', theme.axis.fontFamily)
91
+ .attr('font-family', this.fontFamily || theme.fontFamily)
85
92
  .text(this.text);
86
93
  }
87
94
  }
package/tooltip.d.ts CHANGED
@@ -10,6 +10,6 @@ export declare class Tooltip implements ChartComponent {
10
10
  private tooltipDiv;
11
11
  constructor(config?: TooltipConfig);
12
12
  initialize(theme: ChartTheme): void;
13
- attachToArea(svg: Selection<SVGSVGElement, undefined, null, undefined>, data: DataItem[], series: (Line | Bar)[], xKey: string, x: any, y: any, _theme: ChartTheme, plotArea: PlotAreaBounds, parseValue: (value: any) => number): void;
13
+ attachToArea(svg: Selection<SVGSVGElement, undefined, null, undefined>, data: DataItem[], series: (Line | Bar)[], xKey: string, x: any, y: any, theme: ChartTheme, plotArea: PlotAreaBounds, parseValue: (value: any) => number): void;
14
14
  cleanup(): void;
15
15
  }
package/tooltip.js CHANGED
@@ -38,7 +38,7 @@ export class Tooltip {
38
38
  .style('pointer-events', 'none')
39
39
  .style('z-index', '1000');
40
40
  }
41
- attachToArea(svg, data, series, xKey, x, y, _theme, plotArea, parseValue) {
41
+ attachToArea(svg, data, series, xKey, x, y, theme, plotArea, parseValue) {
42
42
  if (!this.tooltipDiv)
43
43
  return;
44
44
  const tooltip = this.tooltipDiv;
@@ -65,13 +65,14 @@ export class Tooltip {
65
65
  .style('pointer-events', 'all');
66
66
  // Create focus circles for each series
67
67
  const focusCircles = series.map((s) => {
68
+ const seriesColor = getSeriesColor(s);
68
69
  return svg
69
70
  .append('circle')
70
71
  .attr('class', `focus-circle-${s.dataKey.replace(/\s+/g, '-')}`)
71
- .attr('r', 5)
72
- .attr('fill', getSeriesColor(s))
73
- .attr('stroke', 'white')
74
- .attr('stroke-width', 2)
72
+ .attr('r', theme.line.point.size + 1)
73
+ .attr('fill', theme.line.point.color || seriesColor)
74
+ .attr('stroke', theme.line.point.strokeColor || seriesColor)
75
+ .attr('stroke-width', theme.line.point.strokeWidth)
75
76
  .style('opacity', 0)
76
77
  .style('pointer-events', 'none');
77
78
  });
@@ -114,8 +115,9 @@ export class Tooltip {
114
115
  });
115
116
  // Position tooltip relative to the data point
116
117
  const svgRect = svg.node().getBoundingClientRect();
117
- const tooltipX = svgRect.left + xPos + 10;
118
+ const tooltipX = svgRect.left + window.scrollX + xPos + 10;
118
119
  const tooltipY = svgRect.top +
120
+ window.scrollY +
119
121
  y(parseValue(dataPoint[series[0].dataKey])) -
120
122
  10;
121
123
  tooltip
package/types.d.ts CHANGED
@@ -5,23 +5,37 @@ export type ColorPalette = string[];
5
5
  export type ChartTheme = {
6
6
  width: number;
7
7
  height: number;
8
+ fontFamily: string;
8
9
  margins: {
9
10
  top: number;
10
11
  right: number;
11
12
  bottom: number;
12
13
  left: number;
13
14
  };
14
- gridColor: string;
15
+ grid: {
16
+ color: string;
17
+ opacity: number;
18
+ };
15
19
  colorPalette: ColorPalette;
16
20
  axis: {
17
21
  fontFamily: string;
18
22
  fontSize: string;
23
+ fontWeight?: string;
19
24
  };
20
25
  legend: {
21
26
  boxSize: number;
22
27
  uncheckedColor: string;
23
28
  fontSize: number;
24
29
  };
30
+ line: {
31
+ strokeWidth: number;
32
+ point: {
33
+ strokeWidth: number;
34
+ strokeColor?: string;
35
+ color?: string;
36
+ size: number;
37
+ };
38
+ };
25
39
  };
26
40
  export type LineConfig = {
27
41
  dataKey: string;
@@ -60,6 +74,7 @@ export type TitleConfig = {
60
74
  text: string;
61
75
  fontSize?: number;
62
76
  fontWeight?: string;
77
+ fontFamily?: string;
63
78
  align?: 'left' | 'center' | 'right';
64
79
  marginTop?: number;
65
80
  marginBottom?: number;
@@ -75,6 +90,8 @@ export type ScaleConfig = {
75
90
  range?: any[];
76
91
  padding?: number;
77
92
  nice?: boolean;
93
+ min?: number;
94
+ max?: number;
78
95
  };
79
96
  export type AxisScaleConfig = {
80
97
  x?: Partial<ScaleConfig>;
package/x-axis.js CHANGED
@@ -58,6 +58,7 @@ export class XAxis {
58
58
  .tickPadding(this.tickPadding))
59
59
  .attr('font-size', theme.axis.fontSize)
60
60
  .attr('font-family', theme.axis.fontFamily)
61
+ .attr('font-weight', theme.axis.fontWeight || 'normal')
61
62
  .attr('stroke', 'none');
62
63
  // Apply rotation to labels if enabled
63
64
  if (this.rotatedLabels) {
package/xy-chart.js CHANGED
@@ -189,8 +189,8 @@ export class XYChart extends BaseChart {
189
189
  else if ((scaleType === 'linear' || scaleType === 'log') && dataKey) {
190
190
  // For linear and log scales with a dataKey, calculate from that key
191
191
  const values = this.data.map((d) => this.parseValue(d[dataKey]));
192
- const minVal = min(values) ?? 0;
193
- const maxVal = max(values) ?? 100;
192
+ const minVal = config.min ?? (min(values) ?? 0);
193
+ const maxVal = config.max ?? (max(values) ?? 100);
194
194
  domain =
195
195
  scaleType === 'log' && minVal <= 0
196
196
  ? [1, maxVal]
@@ -199,8 +199,8 @@ export class XYChart extends BaseChart {
199
199
  else {
200
200
  // Calculate from series data (for value axes without explicit dataKey)
201
201
  const values = this.data.flatMap((d) => this.series.map((s) => this.parseValue(d[s.dataKey])));
202
- const minVal = min(values) ?? 0;
203
- const maxVal = max(values) ?? 100;
202
+ const minVal = config.min ?? (min(values) ?? 0);
203
+ const maxVal = config.max ?? (max(values) ?? 100);
204
204
  domain =
205
205
  scaleType === 'log' && minVal <= 0
206
206
  ? [1, maxVal]
@@ -266,7 +266,7 @@ export class XYChart extends BaseChart {
266
266
  ? this.series.filter((series) => this.legend.isSeriesVisible(series.dataKey))
267
267
  : this.series;
268
268
  visibleSeries.forEach((series) => {
269
- series.render(this.plotGroup, sortedData, xKey, this.x, this.y, this.parseValue, categoryScaleType);
269
+ series.render(this.plotGroup, sortedData, xKey, this.x, this.y, this.parseValue, categoryScaleType, this.theme);
270
270
  });
271
271
  }
272
272
  }
package/y-axis.js CHANGED
@@ -52,6 +52,7 @@ export class YAxis {
52
52
  .call(axis)
53
53
  .attr('font-size', theme.axis.fontSize)
54
54
  .attr('font-family', theme.axis.fontFamily)
55
+ .attr('font-weight', theme.axis.fontWeight || 'normal')
55
56
  .selectAll('.domain')
56
57
  .remove();
57
58
  }