Ascii Chart

ascii-chart.svelte

A multi-level ASCII / block chart for sparklines and time-series data.

Demo

Usage
001 <script lang="ts">
002 import AsciiChart from '$lib/components/ascii-chart.svelte';
003
004 const sparkData = [12, 18, 30, 45, 22, 58, 80, 95, 65, 42, 70, 85, 90, 60, 40, 75, 99];
005 const waveData = [
006 10, 20, 35, 50, 70, 85, 95, 100, 95, 80, 60, 40, 25, 15, 10, 15, 30, 55, 75, 90, 98, 92, 78, 55,
007 30, 15, 10, 20, 40, 65, 85, 95
008 ];
009 </script>
010
011 <div class="space-y-6">
012 <div>
013 <AsciiChart data={sparkData} label="CPU Load" showValues height={1} />
014 </div>
015
016 <div>
017 <AsciiChart data={waveData} label="Network Traffic (4-line graph)" showValues height={4} />
018 </div>
019 </div>
020
ascii-chart.svelte
001 <script lang="ts" module>
002 import type { HTMLAttributes } from 'svelte/elements';
003
004 export type AsciiChartProps = HTMLAttributes<HTMLElement> & {
005 ref?: HTMLElement | null;
006 data?: number[];
007 height?: number; // in integer --line units (default: 1)
008 min?: number;
009 max?: number;
010 label?: string;
011 showValues?: boolean;
012 };
013
014 const BLOCKS = [' ', ' ', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
015 </script>
016
017 <script lang="ts">
018 let {
019 ref = $bindable(null),
020 data = [],
021 height = 1,
022 min: minProp,
023 max: maxProp,
024 label,
025 showValues = false,
026 class: className = '',
027 ...restProps
028 }: AsciiChartProps = $props();
029
030 const validHeight = $derived(Math.max(1, Math.floor(height)));
031
032 const minVal = $derived(
033 minProp !== undefined ? minProp : data.length > 0 ? Math.min(...data) : 0
034 );
035 const maxVal = $derived(
036 maxProp !== undefined ? maxProp : data.length > 0 ? Math.max(...data) : 100
037 );
038 const currentVal = $derived(data.length > 0 ? data[data.length - 1] : 0);
039
040 // Generate multi-line ascii grid
041 const chartRows = $derived.by(() => {
042 if (data.length === 0) return [];
043 const rows: string[] = [];
044 const range = maxVal === minVal ? 1 : maxVal - minVal;
045
046 for (let r = validHeight - 1; r >= 0; r--) {
047 let lineStr = '';
048 const rowMin = minVal + (r / validHeight) * range;
049 const rowMax = minVal + ((r + 1) / validHeight) * range;
050 const rowHeight = rowMax - rowMin;
051
052 for (const val of data) {
053 if (val <= rowMin) {
054 lineStr += ' ';
055 } else if (val >= rowMax) {
056 lineStr += '█';
057 } else {
058 const fraction = (val - rowMin) / rowHeight;
059 const idx = Math.min(BLOCKS.length - 1, Math.floor(fraction * (BLOCKS.length - 1)) + 1);
060 lineStr += BLOCKS[idx] || ' ';
061 }
062 }
063 rows.push(lineStr);
064 }
065 return rows;
066 });
067 </script>
068
069 <figure
070 bind:this={ref}
071 class="chart-wrap {className}"
072 style:--chart-rows={validHeight}
073 role="img"
074 aria-label={label ?? 'ASCII data chart'}
075 {...restProps}
076 >
077 {#if label || showValues}
078 <div class="chart-header">
079 {#if label}<span class="chart-label">{label}</span>{/if}
080 {#if showValues}
081 <span class="chart-stats">
082 <span class="stat-cur">{currentVal}</span>
083 <span class="stat-minmax">[{minVal}..{maxVal}]</span>
084 </span>
085 {/if}
086 </div>
087 {/if}
088
089 <div class="chart-body">
090 {#each chartRows as row, i (i)}
091 <div class="chart-row">{row}</div>
092 {/each}
093 </div>
094 </figure>
095
096 <style>
097 .chart-wrap {
098 margin: 0;
099 display: flex;
100 flex-direction: column;
101 font-family: var(--font-family-mono);
102 font-size: var(--font-size);
103 line-height: var(--line);
104 color: var(--text-primary);
105 box-sizing: border-box;
106 }
107
108 .chart-header {
109 display: flex;
110 justify-content: space-between;
111 align-items: center;
112 height: var(--line);
113 line-height: var(--line);
114 user-select: none;
115 }
116
117 .chart-label {
118 font-weight: bold;
119 opacity: 0.85;
120 }
121
122 .chart-stats {
123 display: flex;
124 gap: 1ch;
125 }
126
127 .stat-cur {
128 color: var(--focus-ring);
129 font-weight: bold;
130 }
131
132 .stat-minmax {
133 opacity: 0.5;
134 }
135
136 .chart-body {
137 display: flex;
138 flex-direction: column;
139 height: calc(var(--line) * var(--chart-rows, 1));
140 user-select: none;
141 color: var(--focus-ring);
142 }
143
144 .chart-row {
145 height: var(--line);
146 line-height: var(--line);
147 white-space: pre;
148 overflow: hidden;
149 }
150 </style>
151