@coreusa/final-barline 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Coreus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # Final Barline
2
+
3
+ A high-performance SVG Chart library for Svelte 5. Supports bar and line type of graphs with option to export the graph and a wide support for customization. Responsive by default.
4
+
5
+ <img width="630" height="496" alt="image" src="https://github.com/user-attachments/assets/ffe674c0-9fbc-4799-87ec-9bf275faa361" />
6
+
7
+ <img width="630" height="496" alt="image" src="https://github.com/user-attachments/assets/2d67a102-5b6b-42f7-a49d-f5fcac708da7" />
8
+
9
+ ## General use
10
+
11
+ ```sh
12
+ # install Final Barline
13
+ #pnpm
14
+ pnpm add @coreusa/final-barline
15
+
16
+ # or npm
17
+ npm install @coreusa/final-barline
18
+
19
+ # or yarn
20
+ yarn add @coreusa/final-barline
21
+ ```
22
+
23
+ ```sh
24
+ # import the component
25
+ import Barline from '@coreusa/final-barline';
26
+ ```
27
+
28
+ ```sh
29
+ # Configure and setup the component
30
+
31
+ <Barline
32
+ data={data}
33
+ type="line"
34
+ height={400}
35
+ width={600}
36
+ title="Chart title"
37
+ lineWidth={2}
38
+ yMaxValuePadding={1}
39
+ />
40
+
41
+ ```
@@ -0,0 +1,624 @@
1
+ <!--
2
+ Basic SVG Chart component that supports lines and bar type of charts.
3
+ Charts can be customized through various properties like padding, line width and much more.
4
+ -->
5
+ <script lang="ts">
6
+ import type { DataSeriesInterface } from './types/DataSeriesInterface.js';
7
+
8
+ let {
9
+ data = [
10
+ {
11
+ label: '',
12
+ values: [0]
13
+ }
14
+ ],
15
+ title = '',
16
+ type = 'line',
17
+ width = 400,
18
+ height = 200,
19
+ xMin = undefined,
20
+ xMax = undefined,
21
+ yMin = undefined,
22
+ yMax = undefined,
23
+ yMaxValuePadding = 0,
24
+ timeFormatting = false,
25
+ showLegend = true,
26
+ lineWidth = 3,
27
+ palette = ['#4BEF8F', '#d62728', '#FFC83B', '#17becf', '#dbdb8d', '#D4484B', '#9edae5'],
28
+ xValueSuffix = '',
29
+ yValueSuffix = '',
30
+ xValueCulling = 10,
31
+ xValuePrecision = 1,
32
+ yValuePrecision = 1,
33
+ xValues = [],
34
+ paddingSides = {
35
+ top: 20,
36
+ bottom: 0,
37
+ right: 10,
38
+ left: 20
39
+ },
40
+ showHorizontalGridLines = true,
41
+ showVerticalGridLines = true,
42
+ yValueCulling = 6,
43
+ responsive = true
44
+ }: {
45
+ data: DataSeriesInterface[];
46
+ type: 'line' | 'bar';
47
+ title?: string;
48
+ width?: number;
49
+ height?: number;
50
+ xMin?: number;
51
+ xMax?: number;
52
+ yMin?: number;
53
+ yMax?: number;
54
+ yMaxValuePadding?: number; // How much extra space to add on top of the computed max y value
55
+ timeFormatting?: boolean;
56
+ showLegend?: boolean;
57
+ lineWidth?: number;
58
+ palette?: string[];
59
+ xValueSuffix?: string;
60
+ yValueSuffix?: string;
61
+ // Number of x axis values to display (max)
62
+ xValueCulling?: number;
63
+ // Number of y axis values to display (max)
64
+ yValueCulling?: number;
65
+ // Number precision for x values
66
+ xValuePrecision?: number;
67
+ // Number precision for y values
68
+ yValuePrecision?: number;
69
+ // Optionally provide the x values corresponding to each data point, otherwise assume that x values are 0, 1, 2, etc.)
70
+ xValues?: number[];
71
+ paddingSides?: {
72
+ top: number;
73
+ bottom: number;
74
+ left: number;
75
+ right: number;
76
+ };
77
+ showHorizontalGridLines?: boolean;
78
+ showVerticalGridLines?: boolean;
79
+ // Whether the chart resizes to parent container responsively or not (default true)
80
+ responsive?: boolean;
81
+ } = $props();
82
+
83
+ let chartContainerWidth = $state(0);
84
+ let showGraphGrid = $state(true);
85
+ let svgChartElement: SVGSVGElement;
86
+ // Start position of the X/Y axis, to the right of the Y axis
87
+ const chartSafetyMarginX = 50;
88
+ const chartSafetyMarginBottom = 20;
89
+
90
+ let chartWidth = $derived.by(() => {
91
+ if (responsive) {
92
+ return chartContainerWidth;
93
+ } else {
94
+ return width;
95
+ }
96
+ });
97
+
98
+ // The inner width of the chart (where X starts (min, to the left) and ends (max, to the right)
99
+ let innerWidth = $derived.by(() => {
100
+ return chartWidth - (paddingSides.left + paddingSides.right + chartSafetyMarginX);
101
+ });
102
+
103
+ // The inner height of the chart (where Y starts (min, bottom) and ends (max, top). Additionally adds a safety margin at the bottom so x values can be shown
104
+ const innerHeight = $derived.by(
105
+ () => height - (paddingSides.top + paddingSides.bottom) - chartSafetyMarginBottom
106
+ );
107
+
108
+ let isEnoughDataForPresentation = $derived.by(() => {
109
+ // Ensure there is at least one data series and that series has at least 1 data point
110
+ return data?.length > 0 && data[0]?.values?.length > 1;
111
+ });
112
+
113
+ /**
114
+ * Scales an x value based on the width of the chart and the min/max x values observed in the data set
115
+ * @param x
116
+ */
117
+ const xScale = (x: number) => {
118
+ // TODO: Add padding to computedXMax if need for padding on the right side
119
+ // NOTE: Added padding to the right side to ensure scale factor has enough room
120
+ const ratio = (x - computedXMin) / (computedXMax + 1 - computedXMin);
121
+ const res =
122
+ chartSafetyMarginX / 2 + paddingSides.left + paddingSides.right + ratio * chartWidth;
123
+ // Clamp to the drawable area
124
+ return Math.max(paddingSides.left + paddingSides.right, Math.min(chartWidth, res));
125
+ };
126
+
127
+ const yScale = (y: number) => {
128
+ const ratio = (y - computedYMin) / (computedYMax - computedYMin);
129
+ const res = innerHeight + (paddingSides.top + paddingSides.bottom) - ratio * innerHeight;
130
+ return isNaN(res) ? 0 : res;
131
+ };
132
+
133
+ // All numeric values across every series – used for autoscaling y‑axis
134
+ const allValues = $derived.by(() => {
135
+ if (data?.length && data[0].values?.length > 2) {
136
+ return data.flatMap((d) => {
137
+ return d.values;
138
+ });
139
+ } else {
140
+ return [0];
141
+ }
142
+ });
143
+
144
+ // X‑axis is based on the number of points (assumes all series share the same length)
145
+ const pointCount = $derived.by(() => {
146
+ // Test if there are x values present (custom x ticks provided)
147
+ if (xValues?.length > 0) {
148
+ return xValues.length;
149
+ } else if (data?.length && data[0]?.values?.length > 0) {
150
+ // Sample only the first data series. Point count must be the same across all data series
151
+ return data[0]?.values?.length;
152
+ } else {
153
+ return 0;
154
+ }
155
+ });
156
+
157
+ // What data index is currently under the mouse cursor
158
+ let dataHoverIndex = $state(-1);
159
+
160
+ /**
161
+ * Variables related to tooltip position and mouse hovering the chart
162
+ */
163
+ let mouseHoverX = $derived(dataHoverIndex >= 0 ? xScale(dataHoverIndex) : 0);
164
+
165
+ let tooltipX = $derived(mouseHoverX + 10);
166
+ let tooltipY = $state(0);
167
+
168
+ let isHoveringChart = $state(false);
169
+
170
+ // Handle all values if they are undefined
171
+ const computedXMin = $derived(xMin ?? 0);
172
+ const computedXMax = $derived(xMax ?? pointCount - 1);
173
+ const computedYMin = $derived(yMin ?? Math.min(...allValues));
174
+ const computedYMax = $derived(yMax ?? Math.max(...allValues) + yMaxValuePadding);
175
+
176
+ /*
177
+ /**
178
+ * Returns an array of SVG path strings, one per series
179
+ */
180
+ const graphLines = $derived.by(() => {
181
+ // Iterate each data series and create paths
182
+ const paths = data.map((series) => {
183
+ const points = series.values;
184
+ if (points?.length < 2) {
185
+ // Empty line if there's not enough data
186
+ return 'M 0 0';
187
+ }
188
+ // Create a line for each data series and concatinate to a SVG path string
189
+ return points
190
+ .map((value, index) => {
191
+ const x = xScale(index);
192
+ const y = yScale(value);
193
+ return `${index === 0 ? 'M' : 'L'} ${x} ${y}`;
194
+ })
195
+ .join(' ');
196
+ });
197
+ return paths;
198
+ });
199
+
200
+ /**
201
+ * Mouse handler for when the user hovers the chart (to show tooltip and vertical index/position line)
202
+ * @param event
203
+ */
204
+ const onMouseMove = (event: MouseEvent) => {
205
+ const rect = (event.currentTarget as SVGRectElement).getBoundingClientRect();
206
+ const mouseX = event.clientX - rect.left - chartSafetyMarginX + paddingSides.left;
207
+ // Convert from the dimensions of the image to a corresponding data index in the chart
208
+ const relative = mouseX / chartWidth;
209
+ const index = Math.round(computedXMin + relative * (computedXMax - computedXMin));
210
+ tooltipY = event.clientY - rect.top + 10; // 10px below the mouse
211
+ dataHoverIndex = Math.max(0, Math.min(pointCount - 1, index));
212
+ isHoveringChart = true;
213
+ };
214
+
215
+ const onMouseLeave = () => {
216
+ isHoveringChart = false;
217
+ dataHoverIndex = -1;
218
+ };
219
+
220
+ /**
221
+ * Bar chart groups. Do not calculate them if the chart type is not bar
222
+ */
223
+ const graphBars = $derived.by(() => {
224
+ if (type !== 'bar' || !isEnoughDataForPresentation) {
225
+ return [];
226
+ }
227
+
228
+ const barWidth = innerWidth / pointCount;
229
+
230
+ const groups = Array.from({ length: pointCount }, (_, dataSeriesIndex) => {
231
+ const xBaseLine = xScale(dataSeriesIndex);
232
+ return data.map((series, seriesIndex) => {
233
+ const v = series.values[dataSeriesIndex];
234
+ const x = xBaseLine + seriesIndex * barWidth - barWidth / 2;
235
+ const y = Math.abs(yScale(v));
236
+ const h = innerHeight + (paddingSides.top + paddingSides.bottom) - y;
237
+ return {
238
+ x,
239
+ y,
240
+ w: barWidth,
241
+ h
242
+ };
243
+ });
244
+ });
245
+ return groups;
246
+ });
247
+
248
+ /**
249
+ * Creates values for X, taking into account any x culling settings
250
+ */
251
+ const xTicks = $derived.by(() => {
252
+ const range = computedXMax - computedXMin;
253
+ const step = Math.max(1, Math.floor(range / xValueCulling));
254
+
255
+ const indexes = Array.from({ length: xValueCulling + 1 }, (_, i) => computedXMin + i * step);
256
+
257
+ // Use a set to ensure unique values
258
+ const unique = Array.from(new Set(indexes));
259
+
260
+ return unique.map((index) => {
261
+ // TODO: Using base index as fallback will cause issues when adding a xValuePadding and using custom xValues
262
+ const value = xValues?.[index] !== undefined ? xValues[index] : index;
263
+ return {
264
+ value:
265
+ timeFormatting && value === 0
266
+ ? 'Now'
267
+ : `${value.toFixed(xValuePrecision)}${xValueSuffix}`,
268
+ position: xScale(index)
269
+ };
270
+ });
271
+ });
272
+
273
+ /**
274
+ * Creates values for Y, taking into account any y culling settings
275
+ */
276
+ const yTicks = $derived.by(() => {
277
+ return Array.from({ length: yValueCulling }, (_, i) => {
278
+ const value = computedYMin + (i / (yValueCulling - 1)) * (computedYMax - computedYMin);
279
+ return {
280
+ value,
281
+ y: yScale(value)
282
+ };
283
+ });
284
+ });
285
+
286
+ /**
287
+ * Map legend data series labels and corresponding color in the palette
288
+ */
289
+ const legend = $derived.by(() => {
290
+ return data.map((series, index) => {
291
+ return {
292
+ label: series.label,
293
+ color: palette[index % palette.length]
294
+ };
295
+ });
296
+ });
297
+
298
+ const legendWidth = $derived.by(() => {
299
+ if (legend.length > 0) {
300
+ // Determine how long each legend can be based on the number of data series and the chart width (minus padding on each side)
301
+ return Math.min(
302
+ 100,
303
+ (innerWidth - 2 * paddingSides.left + paddingSides.right) / legend.length
304
+ );
305
+ } else {
306
+ return 0;
307
+ }
308
+ });
309
+
310
+ /**
311
+ * Determine where to start placing the legend (far end of chart)
312
+ */
313
+ const legendStartX = $derived.by(() => {
314
+ if (legend.length > 0) {
315
+ // Center the legend in the remaining space after taking into account the legend width and padding on each side
316
+ return paddingSides.left + paddingSides.right + (innerWidth - legendWidth * legend.length);
317
+ } else {
318
+ return 0;
319
+ }
320
+ });
321
+
322
+ let chartFilename = $derived.by(() => {
323
+ const timeNow = Date.now();
324
+ let fileNamePrefix = 'chart-export-';
325
+ if (title?.length) {
326
+ fileNamePrefix = title.toLowerCase().replace(/\s+/g, '-');
327
+ }
328
+ return `${fileNamePrefix}-${timeNow}.svg`;
329
+ });
330
+
331
+ const exportSvg = (svgElement: SVGSVGElement) => {
332
+ const clone = svgElement.cloneNode(true) as SVGSVGElement;
333
+ const serializer = new XMLSerializer();
334
+
335
+ /**
336
+ * TODO: Consider adding a toggle whether or not to default coloring of exported
337
+ * chart to avoid removing custom user profiling / styles
338
+ */
339
+ const darkColour = '#222';
340
+ // Change all text to dark so the chart can be read properly
341
+ clone.querySelectorAll('text').forEach((txt) => {
342
+ // If the element already has a fill, overwrite it; otherwise just set it
343
+ txt.setAttribute('fill', darkColour);
344
+ txt.setAttribute('font-family', 'Open Sans');
345
+ // Some charts use stroke for text – set that as well just in case
346
+ txt.setAttribute('stroke', darkColour);
347
+ });
348
+
349
+ const svgString = serializer.serializeToString(clone);
350
+ // Add XML declaration + optional DOCTYPE for better compatibility
351
+ const svgBlob = new Blob(
352
+ [
353
+ '<?xml version="1.0" encoding="UTF-8"?>\n',
354
+ '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n',
355
+ ' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n',
356
+ svgString
357
+ ],
358
+ { type: 'image/svg+xml;charset=utf-8' }
359
+ );
360
+
361
+ const url = URL.createObjectURL(svgBlob);
362
+ const link = document.createElement('a');
363
+ link.href = url;
364
+ link.download = chartFilename;
365
+ link.click();
366
+ URL.revokeObjectURL(url);
367
+ };
368
+ const horizontalPadding = $derived.by(() => paddingSides.left + paddingSides.right);
369
+ const verticalPadding = $derived.by(() => paddingSides.top + paddingSides.bottom);
370
+ </script>
371
+
372
+ <div bind:clientWidth={chartContainerWidth} class="barline-chart">
373
+ <svg
374
+ bind:this={svgChartElement}
375
+ width={chartWidth}
376
+ {height}
377
+ viewBox={`0 0 ${chartWidth} ${height}`}
378
+ xmlns="http://www.w3.org/2000/svg"
379
+ >
380
+ <!-- Title -->
381
+ <text
382
+ x={chartWidth / 2}
383
+ y={paddingSides.top / 2}
384
+ text-anchor="middle"
385
+ font-size="14"
386
+ class="barline-chart-title fw-bold"
387
+ fill="#222">{title}</text
388
+ >
389
+
390
+ <!-- Only plot graph if there is at least 1 data series with 1 data point -->
391
+ {#if isEnoughDataForPresentation}
392
+ <!-- Axis labels and grid lines -->
393
+ <!-- X-axis -->
394
+ {#each xTicks as { value, position }, index (`x-axis-label-${index}`)}
395
+ {#if showGraphGrid}
396
+ <!-- LINES - Vertical -->
397
+ <line
398
+ x1={position}
399
+ x2={position}
400
+ y1={showVerticalGridLines ? verticalPadding : height - verticalPadding + 5}
401
+ y2={height - paddingSides.bottom - chartSafetyMarginBottom}
402
+ stroke="#e0e0e0"
403
+ stroke-width="1"
404
+ />
405
+ {/if}
406
+ <!-- LABELS - X axis -->
407
+ <text
408
+ x={position}
409
+ y={innerHeight + verticalPadding + 15}
410
+ text-anchor="middle"
411
+ font-size="12"
412
+ transform={`rotate(0, ${position + 5}, ${height})`}
413
+ fill="#222"
414
+ >
415
+ {value}
416
+ </text>
417
+ {/each}
418
+
419
+ <!-- Y‑axis -->
420
+ {#each yTicks as { value, y }, index (`y-axis-label-${index}`)}
421
+ {#if showGraphGrid}
422
+ <!-- LINES - Horizontal -->
423
+ <line
424
+ x1={showHorizontalGridLines ? chartSafetyMarginX / 2 + 20 : verticalPadding}
425
+ x2={showHorizontalGridLines ? chartWidth + chartSafetyMarginX : horizontalPadding - 5}
426
+ y1={y}
427
+ y2={y}
428
+ stroke="#ccc"
429
+ stroke-width="1"
430
+ />
431
+ {/if}
432
+ <!-- LABELS - Y axis. NOTE: Add margin for last (top Y value) in case no top padding is in place -->
433
+ <text
434
+ x={chartSafetyMarginX - 10}
435
+ y={index === yTicks.length - 1 ? y + 10 : y + 5}
436
+ text-anchor="end"
437
+ font-size="12"
438
+ font-family="open sans"
439
+ fill="#222"
440
+ >
441
+ {value.toFixed(1)}
442
+ {yValueSuffix}
443
+ </text>
444
+ {/each}
445
+
446
+ {#if type === 'line'}
447
+ {#each graphLines as path, lineIndex (`line-${lineIndex}`)}
448
+ <path
449
+ d={path}
450
+ fill="none"
451
+ stroke={palette[lineIndex % palette.length]}
452
+ stroke-width={lineWidth}
453
+ stroke-linejoin="round"
454
+ />
455
+ {/each}
456
+ {:else if type === 'bar'}
457
+ {#each graphBars as group, groupIndex (`bar-group-${groupIndex}`)}}
458
+ {#each group as { x, y, w, h }, barIndex (`bar-group-${barIndex}-bar-${barIndex}`)}
459
+ <rect {x} {y} width={w} height={h} fill={palette[barIndex % palette.length]} />
460
+ {/each}
461
+ {/each}
462
+ {/if}
463
+ {:else}
464
+ <!-- Not enough data, show message -->
465
+ Please provide at least 1 data series with at least 1 data point in them.
466
+ {/if}
467
+
468
+ <!-- Add data series legend (if enabled) -->
469
+ {#if showLegend}
470
+ <!-- Background area behind legend for better eligibility -->
471
+ <rect
472
+ width={legendWidth * legend.length}
473
+ height="40"
474
+ x={legendStartX - (paddingSides.left + paddingSides.right) / 2}
475
+ y={paddingSides.top + 8}
476
+ fill="#222"
477
+ opacity="0.9"
478
+ />
479
+ {#each legend as { label, color }, index (`data-series-legend-${index}`)}
480
+ <g transform={`translate(${legendStartX + legendWidth * index}, ${paddingSides.top + 20})`}>
481
+ <!-- Colored square matching the color of the corresponding data series -->
482
+ <rect width="14" height="14" fill={color} />
483
+ <!-- Name of the data series -->
484
+ <text x="20" y="13" font-size="16" class="fw-bold" fill="#fff">
485
+ {label}
486
+ </text>
487
+ </g>
488
+ {/each}
489
+ {/if}
490
+
491
+ <!-- Vertical and horizontal line upon mouse hover -->
492
+ {#if mouseHoverX > 0}
493
+ <!-- Vertical hover line -->
494
+ <line
495
+ x1={mouseHoverX}
496
+ x2={mouseHoverX}
497
+ y1={verticalPadding}
498
+ y2={height - verticalPadding}
499
+ stroke="#2B9C6A"
500
+ stroke-width="1"
501
+ stroke-dasharray="4 4"
502
+ />
503
+ <!-- Horizontal hover line -->
504
+ <line
505
+ x1={0}
506
+ x2={chartContainerWidth}
507
+ y1={tooltipY - 10}
508
+ y2={tooltipY - 10}
509
+ stroke="#2B9C6A"
510
+ stroke-width="1"
511
+ stroke-dasharray="4 4"
512
+ />
513
+ {/if}
514
+
515
+ <!-- Transparent overlay to capture mouse events -->
516
+ <rect
517
+ x={0}
518
+ y={0}
519
+ width={chartContainerWidth}
520
+ {height}
521
+ fill="transparent"
522
+ role="presentation"
523
+ onmousemove={onMouseMove}
524
+ onmouseleave={onMouseLeave}
525
+ />
526
+ </svg>
527
+ <!-- Tooltip for the graph -->
528
+ {#if isHoveringChart && dataHoverIndex >= 0}
529
+ <div
530
+ class="barline-tooltip"
531
+ style="
532
+ left: {tooltipX}px;
533
+ top: {tooltipY}px;
534
+ pointer-events: none;
535
+ "
536
+ >
537
+ <h6 class="barline-tooltip-header">
538
+ <!-- If custom X Values have been provided (and there's no x max), use the hover index and lookup the value in that array. Otherwise use the data series index -->
539
+ {xValues?.length > 0 && xMax === undefined
540
+ ? xValues[dataHoverIndex].toFixed(xValuePrecision)
541
+ : dataHoverIndex.toFixed(xValuePrecision)}
542
+ {xValueSuffix}
543
+ </h6>
544
+ <div class="barline-tooltip-content">
545
+ <dd>
546
+ {#each data as dataSeries, seriesIndex (`data-series-tooltip-${seriesIndex}`)}
547
+ <dl>
548
+ <span style="color:{palette[seriesIndex % palette.length]}">&#9632; </span>
549
+ <strong>
550
+ {dataSeries.label}
551
+ {dataSeries.values[dataHoverIndex].toFixed(yValuePrecision)}
552
+ {yValueSuffix}
553
+ </strong>
554
+ </dl>
555
+ {/each}
556
+ </dd>
557
+ </div>
558
+ </div>
559
+ {/if}
560
+ <div class="d-flex justify-content-center">
561
+ <button
562
+ class="barline-export-chart-button"
563
+ onclick={() => {
564
+ exportSvg(svgChartElement);
565
+ }}
566
+ >
567
+ Export graph
568
+ </button>
569
+ </div>
570
+ </div>
571
+
572
+ <style>
573
+ .barline-chart {
574
+ font-family: 'Open Sans', 'Lato', 'Helvetica', 'Ubuntu', sans-serif;
575
+ }
576
+
577
+ .barline-export-chart-button {
578
+ background-color: #222;
579
+ color: #fff;
580
+ border: none;
581
+ padding: 8px 16px;
582
+ border-radius: 4px;
583
+ font-size: 14px;
584
+ cursor: pointer;
585
+ }
586
+
587
+ .barline-export-chart-button:hover {
588
+ background-color: #3f3f3f;
589
+ color: #ffac11;
590
+ transition: all 0.25s ease-in-out;
591
+ }
592
+
593
+ .barline-tooltip {
594
+ position: absolute;
595
+ background-color: #fff;
596
+ color: #222;
597
+ text-wrap: nowrap;
598
+ box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.4);
599
+ border-radius: 4px;
600
+ z-index: 500;
601
+ }
602
+
603
+ .barline-chart-title {
604
+ font-weight: bold;
605
+ }
606
+
607
+ .barline-tooltip-header {
608
+ /*bg-dark-blue-horizontal-gradient text-white p-2*/
609
+ background-color: #222;
610
+ color: #fff;
611
+ font-size: 1.1rem;
612
+ padding: 5px 10px;
613
+ margin: 0;
614
+ text-align: center;
615
+ }
616
+
617
+ .barline-tooltip-content {
618
+ padding: 10px;
619
+ }
620
+
621
+ .barline-tooltip-content dd {
622
+ margin: 0;
623
+ }
624
+ </style>
@@ -0,0 +1,36 @@
1
+ import type { DataSeriesInterface } from './types/DataSeriesInterface.ts';
2
+ type $$ComponentProps = {
3
+ data: DataSeriesInterface[];
4
+ type: 'line' | 'bar';
5
+ title?: string;
6
+ width?: number;
7
+ height?: number;
8
+ xMin?: number;
9
+ xMax?: number;
10
+ yMin?: number;
11
+ yMax?: number;
12
+ yMaxValuePadding?: number;
13
+ timeFormatting?: boolean;
14
+ showLegend?: boolean;
15
+ lineWidth?: number;
16
+ palette?: string[];
17
+ xValueSuffix?: string;
18
+ yValueSuffix?: string;
19
+ xValueCulling?: number;
20
+ yValueCulling?: number;
21
+ xValuePrecision?: number;
22
+ yValuePrecision?: number;
23
+ xValues?: number[];
24
+ paddingSides?: {
25
+ top: number;
26
+ bottom: number;
27
+ left: number;
28
+ right: number;
29
+ };
30
+ showHorizontalGridLines?: boolean;
31
+ showVerticalGridLines?: boolean;
32
+ responsive?: boolean;
33
+ };
34
+ declare const Barline: import("svelte").Component<$$ComponentProps, {}, "">;
35
+ type Barline = ReturnType<typeof Barline>;
36
+ export default Barline;
@@ -0,0 +1,2 @@
1
+ import Barline from './Barline.svelte';
2
+ export { Barline };
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ // src/lib/index.ts
2
+ import Barline from './Barline.svelte';
3
+ export { Barline };
@@ -0,0 +1,4 @@
1
+ export interface DataSeriesInterface {
2
+ label: string;
3
+ values: number[];
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,39 @@
1
+ import prettier from 'eslint-config-prettier';
2
+ import path from 'node:path';
3
+ import { includeIgnoreFile } from '@eslint/compat';
4
+ import js from '@eslint/js';
5
+ import svelte from 'eslint-plugin-svelte';
6
+ import { defineConfig } from 'eslint/config';
7
+ import globals from 'globals';
8
+ import ts from 'typescript-eslint';
9
+ import svelteConfig from './svelte.config.js';
10
+
11
+ const gitignorePath = path.resolve(import.meta.dirname, '.gitignore');
12
+
13
+ export default defineConfig(
14
+ includeIgnoreFile(gitignorePath),
15
+ js.configs.recommended,
16
+ ts.configs.recommended,
17
+ svelte.configs.recommended,
18
+ prettier,
19
+ svelte.configs.prettier,
20
+ {
21
+ languageOptions: { globals: { ...globals.browser, ...globals.node } },
22
+ rules: {
23
+ // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
24
+ // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
25
+ 'no-undef': 'off'
26
+ }
27
+ },
28
+ {
29
+ files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
30
+ languageOptions: {
31
+ parserOptions: {
32
+ projectService: true,
33
+ extraFileExtensions: ['.svelte'],
34
+ parser: ts.parser,
35
+ svelteConfig
36
+ }
37
+ }
38
+ }
39
+ );
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "@coreusa/final-barline",
3
+ "version": "0.1.0",
4
+ "description": "Final Barline - The lightweight, high performance SVG Chart Library",
5
+ "keywords": [
6
+ "SVG Chart",
7
+ "High performance chart",
8
+ "Svelte 5 Chart"
9
+ ],
10
+ "homepage": "https://github.com/Coreusa/final-barline#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/Coreusa/final-barline/issues"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/Coreusa/final-barline.git"
17
+ },
18
+ "license": "MIT",
19
+ "author": "Coreus",
20
+ "type": "module",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./dist/index.d.ts",
24
+ "svelte": "./dist/index.js"
25
+ }
26
+ },
27
+ "main": "eslint.config.js",
28
+ "types": "./dist/index.d.ts",
29
+ "files": [
30
+ "dist",
31
+ "!dist/**/*.test.*",
32
+ "!dist/**/*.spec.*"
33
+ ],
34
+ "scripts": {
35
+ "dev": "vite dev",
36
+ "build": "vite build && npm run prepack",
37
+ "preview": "vite preview",
38
+ "prepare": "svelte-kit sync || echo ''",
39
+ "prepack": "svelte-kit sync && svelte-package && publint",
40
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
41
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
42
+ "lint": "prettier --check . && eslint .",
43
+ "format": "prettier --write .",
44
+ "test:unit": "vitest",
45
+ "test": "npm run test:unit -- --run"
46
+ },
47
+ "devDependencies": {
48
+ "@eslint/compat": "^2.0.2",
49
+ "@eslint/js": "^9.39.2",
50
+ "@sveltejs/adapter-auto": "^7.0.0",
51
+ "@sveltejs/kit": "^2.50.2",
52
+ "@sveltejs/package": "^2.5.7",
53
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
54
+ "@types/node": "^24",
55
+ "@vitest/browser-playwright": "^4.1.0",
56
+ "eslint": "^9.39.2",
57
+ "eslint-config-prettier": "^10.1.8",
58
+ "eslint-plugin-svelte": "^3.14.0",
59
+ "globals": "^17.3.0",
60
+ "playwright": "^1.58.2",
61
+ "prettier": "^3.8.1",
62
+ "prettier-plugin-svelte": "^3.4.1",
63
+ "publint": "^0.3.17",
64
+ "svelte": "^5.51.0",
65
+ "svelte-check": "^4.4.2",
66
+ "typescript": "^5.9.3",
67
+ "typescript-eslint": "^8.54.0",
68
+ "vite": "^7.3.1",
69
+ "vitest": "^4.1.0",
70
+ "vitest-browser-svelte": "^2.0.2"
71
+ },
72
+ "peerDependencies": {
73
+ "svelte": "^5.0.0"
74
+ },
75
+ "sideEffects": [
76
+ "**/*.css"
77
+ ],
78
+ "svelte": "./dist/index.js"
79
+ }