@rokkit/chart 1.0.0-next.99 → 1.0.2

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 (137) hide show
  1. package/README.md +150 -46
  2. package/package.json +40 -51
  3. package/src/AnimatedPlot.svelte +383 -0
  4. package/src/Chart.svelte +87 -59
  5. package/src/ChartExporter.svelte +125 -0
  6. package/src/ChartLegend.svelte +148 -0
  7. package/src/ChartProvider.svelte +10 -0
  8. package/src/FacetPlot/Panel.svelte +37 -0
  9. package/src/FacetPlot.svelte +114 -0
  10. package/src/Plot/Arc.svelte +29 -0
  11. package/src/Plot/Area.svelte +32 -0
  12. package/src/Plot/Axis.svelte +164 -0
  13. package/src/Plot/Bar.svelte +54 -0
  14. package/src/Plot/Grid.svelte +34 -0
  15. package/src/Plot/Legend.svelte +268 -0
  16. package/src/Plot/Line.svelte +37 -0
  17. package/src/Plot/Point.svelte +40 -0
  18. package/src/Plot/Root.svelte +62 -0
  19. package/src/Plot/Timeline.svelte +95 -0
  20. package/src/Plot/Tooltip.svelte +87 -0
  21. package/src/Plot/index.js +9 -0
  22. package/src/Plot.svelte +313 -0
  23. package/src/PlotState.svelte.js +375 -0
  24. package/src/Sparkline.svelte +108 -0
  25. package/src/Symbol.svelte +15 -11
  26. package/src/Texture.svelte +9 -7
  27. package/src/charts/AreaChart.svelte +30 -0
  28. package/src/charts/BarChart.svelte +31 -0
  29. package/src/charts/BoxPlot.svelte +24 -0
  30. package/src/charts/BubbleChart.svelte +28 -0
  31. package/src/charts/LineChart.svelte +31 -0
  32. package/src/charts/PieChart.svelte +35 -0
  33. package/src/charts/ScatterPlot.svelte +31 -0
  34. package/src/charts/ViolinPlot.svelte +24 -0
  35. package/src/crossfilter/CrossFilter.svelte +42 -0
  36. package/src/crossfilter/FilterBar.svelte +24 -0
  37. package/src/crossfilter/FilterHistogram.svelte +290 -0
  38. package/src/crossfilter/FilterSlider.svelte +83 -0
  39. package/src/crossfilter/createCrossFilter.svelte.js +124 -0
  40. package/src/elements/Bar.svelte +20 -16
  41. package/src/elements/ColorRamp.svelte +19 -19
  42. package/src/elements/ContinuousLegend.svelte +41 -19
  43. package/src/elements/DefinePatterns.svelte +9 -7
  44. package/src/elements/DiscreteLegend.svelte +13 -11
  45. package/src/elements/Label.svelte +4 -9
  46. package/src/elements/SymbolGrid.svelte +8 -9
  47. package/src/examples/BarChartExample.svelte +81 -0
  48. package/src/geoms/Arc.svelte +126 -0
  49. package/src/geoms/Area.svelte +78 -0
  50. package/src/geoms/Bar.svelte +200 -0
  51. package/src/geoms/Box.svelte +113 -0
  52. package/src/geoms/Candlestick.svelte +110 -0
  53. package/src/geoms/Heatmap.svelte +92 -0
  54. package/src/geoms/Hexbin.svelte +114 -0
  55. package/src/geoms/LabelPill.svelte +17 -0
  56. package/src/geoms/Line.svelte +123 -0
  57. package/src/geoms/Point.svelte +145 -0
  58. package/src/geoms/Ribbon.svelte +161 -0
  59. package/src/geoms/Violin.svelte +56 -0
  60. package/src/geoms/Waterfall.svelte +114 -0
  61. package/src/geoms/lib/areas.js +154 -0
  62. package/src/geoms/lib/bars.js +223 -0
  63. package/src/index.js +81 -2
  64. package/src/lib/brewing/BoxBrewer.svelte.js +14 -0
  65. package/src/lib/brewing/CartesianBrewer.svelte.js +21 -0
  66. package/src/lib/brewing/PieBrewer.svelte.js +14 -0
  67. package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
  68. package/src/lib/brewing/ViolinBrewer.svelte.js +14 -0
  69. package/src/lib/brewing/axes.svelte.js +270 -0
  70. package/src/lib/brewing/bars.svelte.js +201 -0
  71. package/src/lib/brewing/brewer.svelte.js +277 -0
  72. package/src/lib/brewing/colors.js +157 -0
  73. package/src/lib/brewing/dimensions.svelte.js +56 -0
  74. package/src/lib/brewing/index.svelte.js +205 -0
  75. package/src/lib/brewing/legends.svelte.js +137 -0
  76. package/src/lib/brewing/marks/arcs.js +43 -0
  77. package/src/lib/brewing/marks/areas.js +72 -0
  78. package/src/lib/brewing/marks/bars.js +49 -0
  79. package/src/lib/brewing/marks/boxes.js +75 -0
  80. package/src/lib/brewing/marks/lines.js +55 -0
  81. package/src/lib/brewing/marks/points.js +105 -0
  82. package/src/lib/brewing/marks/violins.js +90 -0
  83. package/src/lib/brewing/patterns.js +45 -0
  84. package/src/lib/brewing/scales.js +51 -0
  85. package/src/lib/brewing/scales.svelte.js +82 -0
  86. package/src/lib/brewing/stats.js +74 -0
  87. package/src/lib/brewing/symbols.js +10 -0
  88. package/src/lib/brewing/types.js +73 -0
  89. package/src/lib/chart.js +16 -8
  90. package/src/lib/context.js +131 -0
  91. package/src/lib/keyboard-nav.js +37 -0
  92. package/src/lib/plot/chartProps.js +76 -0
  93. package/src/lib/plot/crossfilter.js +16 -0
  94. package/src/lib/plot/facet.js +58 -0
  95. package/src/lib/plot/frames.js +81 -0
  96. package/src/lib/plot/helpers.js +14 -0
  97. package/src/lib/plot/preset.js +67 -0
  98. package/src/lib/plot/scales.js +81 -0
  99. package/src/lib/plot/stat.js +92 -0
  100. package/src/lib/plot/types.js +65 -0
  101. package/src/lib/preset.js +41 -0
  102. package/src/lib/scales.svelte.js +151 -0
  103. package/src/lib/swatch.js +4 -7
  104. package/src/lib/utils.js +142 -0
  105. package/src/lib/xscale.js +31 -0
  106. package/src/patterns/DefinePatterns.svelte +32 -0
  107. package/src/patterns/PatternDef.svelte +27 -0
  108. package/src/patterns/index.js +4 -14
  109. package/src/patterns/patterns.js +360 -0
  110. package/src/patterns/scale.js +116 -0
  111. package/src/spec/chart-spec.js +72 -0
  112. package/src/symbols/RoundedSquare.svelte +18 -12
  113. package/src/symbols/Shape.svelte +22 -16
  114. package/src/symbols/constants/index.js +1 -1
  115. package/LICENSE +0 -21
  116. package/src/lib/index.js +0 -4
  117. package/src/lib/plots.js +0 -27
  118. package/src/patterns/Brick.svelte +0 -17
  119. package/src/patterns/Circles.svelte +0 -18
  120. package/src/patterns/CrossHatch.svelte +0 -14
  121. package/src/patterns/CurvedWave.svelte +0 -9
  122. package/src/patterns/Dots.svelte +0 -19
  123. package/src/patterns/OutlineCircles.svelte +0 -15
  124. package/src/patterns/Tile.svelte +0 -17
  125. package/src/patterns/Triangles.svelte +0 -15
  126. package/src/patterns/Waves.svelte +0 -13
  127. package/src/patterns/paths/NamedPattern.svelte +0 -12
  128. package/src/patterns/paths/constants.js +0 -4
  129. package/src/plots/Plot.svelte +0 -40
  130. package/src/plots/index.js +0 -1
  131. package/src/template/Texture.svelte +0 -16
  132. package/src/template/constants.js +0 -43
  133. package/src/template/shapes/Circles.svelte +0 -16
  134. package/src/template/shapes/Lines.svelte +0 -17
  135. package/src/template/shapes/Path.svelte +0 -12
  136. package/src/template/shapes/Polygons.svelte +0 -18
  137. package/src/template/shapes/index.js +0 -4
@@ -0,0 +1,148 @@
1
+ <script>
2
+ import { buildSymbolPath } from './lib/brewing/marks/points.js'
3
+
4
+ /**
5
+ * Standalone chart legend — HTML-based, usable outside of Plot context.
6
+ * Accepts `groups` from a chart's brewer or from multiple charts for shared legends.
7
+ *
8
+ * @type {{
9
+ * groups: Array<{
10
+ * type?: 'color' | 'pattern' | 'symbol',
11
+ * label?: string,
12
+ * items: Array<{
13
+ * key: string,
14
+ * label: string,
15
+ * fill?: string,
16
+ * stroke?: string,
17
+ * patternId?: string,
18
+ * shape?: string,
19
+ * }>
20
+ * }>,
21
+ * orientation?: 'horizontal' | 'vertical' | 'wrap',
22
+ * gradient?: { style: string, min: string, max: string } | null,
23
+ * swatchStyle?: 'fill' | 'line' | 'point',
24
+ * }}
25
+ */
26
+ let {
27
+ groups = [],
28
+ orientation = 'horizontal',
29
+ gradient = null,
30
+ swatchStyle = 'fill'
31
+ } = $props()
32
+ </script>
33
+
34
+ <div
35
+ class="chart-legend"
36
+ class:vertical={orientation === 'vertical'}
37
+ class:horizontal={orientation === 'horizontal' || orientation === 'wrap'}
38
+ data-chart-legend
39
+ >
40
+ {#if gradient}
41
+ <div class="gradient-section">
42
+ <div class="gradient-bar" style={gradient.style} data-chart-legend-gradient></div>
43
+ <div class="gradient-labels">
44
+ <span>{gradient.min}</span>
45
+ <span>{gradient.max}</span>
46
+ </div>
47
+ </div>
48
+ {:else}
49
+ {#each groups as group, gi (gi)}
50
+ {#if group.label}
51
+ <div class="group-label" data-chart-legend-group-label>{group.label}</div>
52
+ {/if}
53
+ <div class="legend-items" class:vertical={orientation === 'vertical'}>
54
+ {#each group.items as item (item.key)}
55
+ <div class="legend-item" data-chart-legend-item>
56
+ {#if swatchStyle === 'line'}
57
+ <svg width="24" height="14" data-chart-legend-swatch>
58
+ <line
59
+ x1="2" y1="7" x2="22" y2="7"
60
+ stroke={item.stroke ?? item.fill}
61
+ stroke-width="2"
62
+ stroke-linecap="round"
63
+ />
64
+ {#if item.shape}
65
+ <path
66
+ transform="translate(12,7)"
67
+ d={buildSymbolPath(item.shape, 4)}
68
+ fill={item.stroke ?? item.fill}
69
+ />
70
+ {/if}
71
+ </svg>
72
+ {:else if swatchStyle === 'point' && item.shape}
73
+ <svg width="14" height="14" data-chart-legend-swatch>
74
+ <path
75
+ transform="translate(7,7)"
76
+ d={buildSymbolPath(item.shape, 5)}
77
+ fill={item.fill}
78
+ />
79
+ </svg>
80
+ {:else if item.patternId}
81
+ <svg width="14" height="14" data-chart-legend-swatch>
82
+ <rect width="14" height="14" fill={item.fill} />
83
+ <rect width="14" height="14" fill="url(#{item.patternId})" />
84
+ </svg>
85
+ {:else}
86
+ <span class="swatch" style:background-color={item.fill} data-chart-legend-swatch></span>
87
+ {/if}
88
+ <span class="label" data-chart-legend-label>{item.label}</span>
89
+ </div>
90
+ {/each}
91
+ </div>
92
+ {/each}
93
+ {/if}
94
+ </div>
95
+
96
+ <style>
97
+ .chart-legend {
98
+ display: flex;
99
+ flex-direction: column;
100
+ gap: 6px;
101
+ font-size: 12px;
102
+ overflow-y: auto;
103
+ }
104
+ .legend-items {
105
+ display: flex;
106
+ flex-wrap: wrap;
107
+ gap: 8px;
108
+ }
109
+ .legend-items.vertical {
110
+ flex-direction: column;
111
+ }
112
+ .legend-item {
113
+ display: flex;
114
+ align-items: center;
115
+ gap: 4px;
116
+ }
117
+ .swatch {
118
+ display: inline-block;
119
+ width: 14px;
120
+ height: 14px;
121
+ border-radius: 2px;
122
+ flex-shrink: 0;
123
+ }
124
+ .group-label {
125
+ font-weight: 600;
126
+ font-size: 11px;
127
+ opacity: 0.7;
128
+ text-transform: uppercase;
129
+ letter-spacing: 0.02em;
130
+ }
131
+ .gradient-section {
132
+ display: flex;
133
+ flex-direction: column;
134
+ gap: 2px;
135
+ }
136
+ .gradient-bar {
137
+ width: 180px;
138
+ height: 14px;
139
+ border-radius: 2px;
140
+ }
141
+ .gradient-labels {
142
+ display: flex;
143
+ justify-content: space-between;
144
+ width: 180px;
145
+ font-size: 11px;
146
+ opacity: 0.8;
147
+ }
148
+ </style>
@@ -0,0 +1,10 @@
1
+ <script>
2
+ import { setContext } from 'svelte'
3
+ import { createChartPreset } from './lib/preset.js'
4
+
5
+ let { preset = createChartPreset(), children } = $props()
6
+
7
+ setContext('chart-preset', { get current() { return preset } })
8
+ </script>
9
+
10
+ {@render children?.()}
@@ -0,0 +1,37 @@
1
+ <script>
2
+ import PlotChart from '../Plot.svelte'
3
+
4
+ let {
5
+ data,
6
+ x,
7
+ y,
8
+ color,
9
+ geoms = [],
10
+ helpers = {},
11
+ width,
12
+ height,
13
+ mode,
14
+ grid,
15
+ legend,
16
+ xDomain,
17
+ yDomain,
18
+ colorDomain,
19
+ children
20
+ } = $props()
21
+
22
+ // Build spec with domain overrides so PlotState uses them
23
+ const spec = $derived({
24
+ data,
25
+ x,
26
+ y,
27
+ color,
28
+ geoms,
29
+ xDomain,
30
+ yDomain,
31
+ colorDomain
32
+ })
33
+ </script>
34
+
35
+ <PlotChart {spec} {helpers} {width} {height} {mode} {grid} {legend}>
36
+ {@render children?.()}
37
+ </PlotChart>
@@ -0,0 +1,114 @@
1
+ <script>
2
+ import { splitByField, getFacetDomains } from './lib/plot/facet.js'
3
+ import { distinct } from './lib/brewing/colors.js'
4
+ import PlotPanel from './FacetPlot/Panel.svelte'
5
+
6
+ /**
7
+ * @type {{
8
+ * data: Object[],
9
+ * facet: { by: string, cols?: number, scales?: 'fixed'|'free'|'free_x'|'free_y' },
10
+ * x?: string,
11
+ * y?: string,
12
+ * color?: string,
13
+ * fill?: string,
14
+ * pattern?: string,
15
+ * symbol?: string,
16
+ * geom?: string,
17
+ * stat?: string,
18
+ * geoms?: import('./lib/plot/types.js').GeomSpec[],
19
+ * helpers?: import('./lib/plot/types.js').PlotHelpers,
20
+ * panelWidth?: number,
21
+ * panelHeight?: number,
22
+ * width?: number,
23
+ * height?: number,
24
+ * mode?: 'light' | 'dark',
25
+ * grid?: boolean,
26
+ * legend?: boolean,
27
+ * children?: import('svelte').Snippet
28
+ * }}
29
+ */
30
+ let {
31
+ data = [],
32
+ facet,
33
+ x,
34
+ y,
35
+ fill = undefined,
36
+ color = undefined,
37
+ pattern = undefined,
38
+ symbol = undefined,
39
+ geom = 'bar',
40
+ stat = 'identity',
41
+ geoms = [],
42
+ helpers = {},
43
+ panelWidth,
44
+ panelHeight,
45
+ width = 900,
46
+ height = 300,
47
+ mode = 'light',
48
+ grid = true,
49
+ legend = false,
50
+ children
51
+ } = $props()
52
+
53
+ // `fill` is accepted as an alias for `color` (bar/area semantics vs line/point)
54
+ const colorChannel = $derived(fill ?? color)
55
+
56
+ // Effective geom list: explicit array takes precedence; otherwise build from shorthand props
57
+ const effectiveGeoms = $derived(
58
+ geoms.length > 0
59
+ ? geoms
60
+ : [
61
+ {
62
+ type: geom,
63
+ stat,
64
+ ...(pattern !== undefined && { pattern }),
65
+ ...(symbol !== undefined && { symbol })
66
+ }
67
+ ]
68
+ )
69
+
70
+ const panels = $derived(splitByField(data, facet.by))
71
+ const scales = $derived(facet.scales ?? 'fixed')
72
+ const domains = $derived(x && y ? getFacetDomains(panels, { x, y }, scales) : new Map())
73
+
74
+ // Global color domain ensures the same value maps to the same color in every panel.
75
+ const colorDomain = $derived(colorChannel ? distinct(data, colorChannel) : undefined)
76
+
77
+ const cols = $derived(facet.cols ?? Math.min(panels.size, 3))
78
+ const pw = $derived(panelWidth ?? Math.floor(width / cols))
79
+ const ph = $derived(panelHeight ?? height)
80
+ </script>
81
+
82
+ <div data-facet-grid style:--facet-cols={cols}>
83
+ {#each [...panels.entries()] as [facetValue, panelData] (`${facetValue}`)}
84
+ <div data-facet-panel data-facet-value={facetValue}>
85
+ <div data-facet-title>{facetValue}</div>
86
+ <PlotPanel
87
+ data={panelData}
88
+ {x}
89
+ {y}
90
+ color={colorChannel}
91
+ geoms={effectiveGeoms}
92
+ {helpers}
93
+ width={pw}
94
+ height={ph}
95
+ {mode}
96
+ {grid}
97
+ legend={false}
98
+ xDomain={domains.get(facetValue)?.xDomain}
99
+ yDomain={domains.get(facetValue)?.yDomain}
100
+ {colorDomain}
101
+ >
102
+ <!-- Render caller-supplied geoms inside every panel (each gets its own PlotState context) -->
103
+ {@render children?.()}
104
+ </PlotPanel>
105
+ </div>
106
+ {/each}
107
+ </div>
108
+
109
+ <!-- Single shared legend outside the grid -->
110
+ {#if legend}
111
+ <div data-facet-legend>
112
+ <!-- Legend content rendered by first panel; simplified for now -->
113
+ </div>
114
+ {/if}
@@ -0,0 +1,29 @@
1
+ <script>
2
+ import { getContext } from 'svelte'
3
+
4
+ const brewer = getContext('chart-brewer')
5
+ </script>
6
+
7
+ {#if brewer && brewer.arcs && brewer.arcs.length > 0}
8
+ <g
9
+ class="chart-arcs"
10
+ data-plot-type="arc"
11
+ transform="translate({brewer.width / 2}, {brewer.height / 2})"
12
+ >
13
+ {#each brewer.arcs as arc (arc.key)}
14
+ <path
15
+ d={arc.d}
16
+ fill={arc.fill}
17
+ stroke={arc.stroke}
18
+ stroke-width="1"
19
+ data-plot-element="arc"
20
+ />
21
+ {/each}
22
+ </g>
23
+ {/if}
24
+
25
+ <style>
26
+ .chart-arcs {
27
+ pointer-events: none;
28
+ }
29
+ </style>
@@ -0,0 +1,32 @@
1
+ <script>
2
+ import { getContext } from 'svelte'
3
+ import { area as d3Area } from 'd3-shape'
4
+
5
+ let {
6
+ data = [],
7
+ x = undefined,
8
+ y = undefined,
9
+ y0 = undefined,
10
+ fill = 'steelblue',
11
+ opacity = 0.7,
12
+ curve = undefined
13
+ } = $props()
14
+
15
+ const state = getContext('plot-state')
16
+
17
+ const path = $derived.by(() => {
18
+ if (!state?.xScale || !state?.yScale || !data?.length) return null
19
+ const innerHeight = state.innerHeight
20
+ const areaGen = d3Area()
21
+ .x((d) => state.xScale(x ? d[x] : d) ?? 0)
22
+ .y1((d) => state.yScale(y ? d[y] : d) ?? 0)
23
+ .y0((d) => (y0 !== undefined ? state.yScale(d[y0] ?? y0) : innerHeight) ?? innerHeight)
24
+ .defined((d) => d != null)
25
+ if (curve) areaGen.curve(curve)
26
+ return areaGen(data)
27
+ })
28
+ </script>
29
+
30
+ {#if path}
31
+ <path d={path} {fill} {opacity} stroke="none" data-plot-element="area" />
32
+ {/if}
@@ -0,0 +1,164 @@
1
+ <script>
2
+ import { getContext } from 'svelte'
3
+
4
+ /**
5
+ * @type {{
6
+ * type?: 'x' | 'y',
7
+ * label?: string,
8
+ * format?: (v: unknown) => string,
9
+ * ticks?: number,
10
+ * minorTicks?: boolean,
11
+ * showLine?: boolean,
12
+ * showTicks?: boolean,
13
+ * showLabels?: boolean,
14
+ * }}
15
+ */
16
+ let {
17
+ type = 'x',
18
+ label = '',
19
+ format = undefined,
20
+ ticks: tickCount = 6,
21
+ minorTicks = false,
22
+ showLine = true,
23
+ showTicks = true,
24
+ showLabels = true
25
+ } = $props()
26
+
27
+ const state = getContext('plot-state')
28
+
29
+ const MINOR_DIVISIONS = 4
30
+
31
+ const xTicks = $derived.by(() => {
32
+ const s = state.xScale
33
+ if (!s) return []
34
+ if (typeof s.bandwidth === 'function') {
35
+ return s.domain().map((val) => ({
36
+ value: val,
37
+ pos: (s(val) ?? 0) + s.bandwidth() / 2,
38
+ minor: false
39
+ }))
40
+ }
41
+ const major = s.ticks(tickCount).map((val) => ({ value: val, pos: s(val), minor: false }))
42
+ if (!minorTicks || major.length < 2) return major
43
+ const result = []
44
+ for (let i = 0; i < major.length; i++) {
45
+ result.push(major[i])
46
+ if (i < major.length - 1) {
47
+ const step = (major[i + 1].pos - major[i].pos) / MINOR_DIVISIONS
48
+ for (let j = 1; j < MINOR_DIVISIONS; j++) {
49
+ result.push({ value: null, pos: major[i].pos + step * j, minor: true })
50
+ }
51
+ }
52
+ }
53
+ return result
54
+ })
55
+
56
+ const yTicks = $derived.by(() => {
57
+ const s = state.yScale
58
+ if (!s) return []
59
+ if (typeof s.bandwidth === 'function') {
60
+ return s.domain().map((val) => ({
61
+ value: val,
62
+ pos: (s(val) ?? 0) + s.bandwidth() / 2,
63
+ minor: false
64
+ }))
65
+ }
66
+ const major = s.ticks(tickCount).map((val) => ({ value: val, pos: s(val), minor: false }))
67
+ if (!minorTicks || major.length < 2) return major
68
+ const result = []
69
+ for (let i = 0; i < major.length; i++) {
70
+ result.push(major[i])
71
+ if (i < major.length - 1) {
72
+ const step = (major[i + 1].pos - major[i].pos) / MINOR_DIVISIONS
73
+ for (let j = 1; j < MINOR_DIVISIONS; j++) {
74
+ result.push({ value: null, pos: major[i].pos + step * j, minor: true })
75
+ }
76
+ }
77
+ }
78
+ return result
79
+ })
80
+
81
+ const xTransform = $derived(`translate(0, ${state.xAxisY ?? state.innerHeight})`)
82
+ const yTransform = $derived(`translate(${state.yAxisX ?? 0}, 0)`)
83
+ </script>
84
+
85
+ {#if type === 'x'}
86
+ <g class="axis x-axis" transform={xTransform} data-plot-axis="x">
87
+ {#if showLine}
88
+ <line x1="0" y1="0" x2={state.innerWidth} y2="0" data-plot-axis-line />
89
+ {/if}
90
+ {#each xTicks as tick (tick.minor ? `minor-${tick.pos}` : tick.value)}
91
+ <g transform="translate({tick.pos}, 0)" data-plot-tick>
92
+ {#if showTicks}
93
+ <line
94
+ x1="0" y1="0" x2="0"
95
+ y2={tick.minor ? 3 : 6}
96
+ stroke="currentColor"
97
+ opacity={tick.minor ? 0.4 : 1}
98
+ />
99
+ {/if}
100
+ {#if showLabels && !tick.minor}
101
+ <text x="0" y="9" text-anchor="middle" dominant-baseline="hanging" data-plot-tick-label>
102
+ {format ? format(tick.value) : tick.value}
103
+ </text>
104
+ {/if}
105
+ </g>
106
+ {/each}
107
+ {#if label}
108
+ <text
109
+ x={state.innerWidth / 2}
110
+ y="36"
111
+ text-anchor="middle"
112
+ class="axis-label"
113
+ data-plot-axis-label>{label}</text
114
+ >
115
+ {/if}
116
+ </g>
117
+ {:else}
118
+ <g class="axis y-axis" transform={yTransform} data-plot-axis="y">
119
+ {#if showLine}
120
+ <line x1="0" y1="0" x2="0" y2={state.innerHeight} data-plot-axis-line />
121
+ {/if}
122
+ {#each yTicks as tick (tick.minor ? `minor-${tick.pos}` : tick.value)}
123
+ <g transform="translate(0, {tick.pos})" data-plot-tick>
124
+ {#if showTicks}
125
+ <line
126
+ x1={tick.minor ? -3 : -6} y1="0" x2="0" y2="0"
127
+ stroke="currentColor"
128
+ opacity={tick.minor ? 0.4 : 1}
129
+ />
130
+ {/if}
131
+ {#if showLabels && !tick.minor}
132
+ <text x="-9" y="0" text-anchor="end" dominant-baseline="middle" data-plot-tick-label>
133
+ {format ? format(tick.value) : tick.value}
134
+ </text>
135
+ {/if}
136
+ </g>
137
+ {/each}
138
+ {#if label}
139
+ <text
140
+ transform="rotate(-90)"
141
+ x={-(state.innerHeight / 2)}
142
+ y="-40"
143
+ text-anchor="middle"
144
+ class="axis-label"
145
+ data-plot-axis-label>{label}</text
146
+ >
147
+ {/if}
148
+ </g>
149
+ {/if}
150
+
151
+ <style>
152
+ .axis {
153
+ font-size: 11px;
154
+ fill: currentColor;
155
+ stroke: currentColor;
156
+ }
157
+ .axis-label {
158
+ font-size: 13px;
159
+ font-weight: 500;
160
+ }
161
+ [data-plot-axis-line] {
162
+ stroke: currentColor;
163
+ }
164
+ </style>
@@ -0,0 +1,54 @@
1
+ <script>
2
+ import { getContext } from 'svelte'
3
+
4
+ let { data = undefined, x = undefined, y = undefined, fill = 'steelblue', opacity = 1 } = $props()
5
+
6
+ const state = getContext('plot-state')
7
+
8
+ const bars = $derived.by(() => {
9
+ if (!state?.xScale || !state?.yScale) return []
10
+ const xScale = state.xScale
11
+ const yScale = state.yScale
12
+ const innerHeight = state.innerHeight
13
+ const src = data ?? []
14
+ if (!src.length) return []
15
+
16
+ const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 20
17
+ const padding = typeof xScale.bandwidth === 'function' ? bw * 0.05 : 0
18
+
19
+ // Baseline at y=0 when domain spans zero (supports negative bars)
20
+ const yDomain = typeof yScale.bandwidth !== 'function' ? yScale.domain?.() : null
21
+ const baseline =
22
+ yDomain && yDomain[0] <= 0 && yDomain[yDomain.length - 1] >= 0
23
+ ? (yScale(0) ?? innerHeight)
24
+ : innerHeight
25
+
26
+ return src.map((d) => {
27
+ const xVal = x ? d[x] : d
28
+ const yVal = y ? d[y] : d
29
+ const xPos = (xScale(xVal) ?? 0) + padding
30
+ const yPos = yScale(yVal) ?? baseline
31
+ return {
32
+ x: xPos,
33
+ y: Math.min(yPos, baseline),
34
+ width: bw * 0.9,
35
+ height: Math.abs(baseline - yPos),
36
+ label: `${xVal}: ${yVal}`
37
+ }
38
+ })
39
+ })
40
+ </script>
41
+
42
+ {#each bars as bar, i (i)}
43
+ <rect
44
+ x={bar.x}
45
+ y={bar.y}
46
+ width={bar.width}
47
+ height={bar.height}
48
+ {fill}
49
+ {opacity}
50
+ data-plot-element="bar"
51
+ role="graphics-symbol"
52
+ aria-label={bar.label}
53
+ />
54
+ {/each}
@@ -0,0 +1,34 @@
1
+ <script>
2
+ import { getContext } from 'svelte'
3
+
4
+ const state = getContext('plot-state')
5
+
6
+ const xGridLines = $derived.by(() => {
7
+ const s = state.xScale
8
+ if (!s || typeof s.bandwidth !== 'function') return []
9
+ return s.domain().map((val) => ({ pos: (s(val) ?? 0) + s.bandwidth() / 2 }))
10
+ })
11
+
12
+ const yGridLines = $derived.by(() => {
13
+ const s = state.yScale
14
+ if (!s || typeof s.ticks !== 'function') return []
15
+ return s.ticks(6).map((val) => ({ pos: s(val) }))
16
+ })
17
+ </script>
18
+
19
+ <g class="grid" data-plot-grid>
20
+ {#each yGridLines as line (line.pos)}
21
+ <line x1="0" y1={line.pos} x2={state.innerWidth} y2={line.pos} data-plot-grid-line />
22
+ {/each}
23
+ {#each xGridLines as line (line.pos)}
24
+ <line x1={line.pos} y1="0" x2={line.pos} y2={state.innerHeight} data-plot-grid-line="x" />
25
+ {/each}
26
+ </g>
27
+
28
+ <style>
29
+ [data-plot-grid-line] {
30
+ stroke: var(--chart-grid-color, currentColor);
31
+ opacity: 0.15;
32
+ stroke-dasharray: 2 4;
33
+ }
34
+ </style>