Skip to content

Commit

Permalink
✨ feat: add responsive design to ASCII bar charts
Browse files Browse the repository at this point in the history
The commit enhances the ASCII bar charts with responsive features:
- Add mobile-first styles for chart containers
- Implement dynamic bar width based on screen size
- Add data sampling for better display on smaller screens
- Improve label and date alignment for various screen sizes
  • Loading branch information
watzon committed Nov 14, 2024
1 parent 6407579 commit 72ed186
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 19 deletions.
21 changes: 21 additions & 0 deletions public/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,27 @@ tr:nth-child(even) {
border-radius: var(--border-radius);
}

/* Charts
========================================================================== */
.chart-container {
width: 100%;
overflow-x: auto;
margin: var(--space-md) 0;
font-size: 14px;
}

@media screen and (max-width: 480px) {
.chart-container {
font-size: 12px;
}

.chart-bar,
.chart-label,
.chart-spacer {
font-size: inherit;
}
}

/* Mobile Responsiveness */
@media screen and (max-width: 768px) {
.container {
Expand Down
118 changes: 99 additions & 19 deletions public/js/charts/bar.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import Chart from './base.js';

class AsciiBarChart extends Chart {
constructor(data, options = {}) {
// Calculate responsive bar width
const defaultBarWidth = typeof window !== 'undefined' && window.innerWidth < 480 ? 8 : 12;

const mergedOptions = {
barChar: options.barChar || '█',
emptyChar: options.emptyChar || '░',
height: options.height || 5,
barWidth: options.barWidth || 12,
barWidth: options.barWidth || defaultBarWidth,
showLabels: options.showLabels !== undefined ? options.showLabels : true,
showDates: options.showDates !== undefined ? options.showDates : true,
showScale: options.showScale !== undefined ? options.showScale : true,
Expand All @@ -20,6 +23,9 @@ class AsciiBarChart extends Chart {

super(data, mergedOptions);

// Process values with data sampling if needed
this.processDataSampling();

this.formattedNumbers = this.data.map(d => {
if (typeof d.value === 'string' && d.value.includes('B')) {
return d.value.padStart(7);
Expand All @@ -29,6 +35,27 @@ class AsciiBarChart extends Chart {
this.maxNumberWidth = Math.max(...this.formattedNumbers.map(n => n.length));
}

processDataSampling() {
if (typeof window === 'undefined') return;

// Calculate maximum bars that can fit on screen
// Account for scale (6 chars), spacing between bars (1 char), and some padding (40px)
const scaleWidth = this.options.showScale ? 6 : 0;
const maxBars = Math.floor((window.innerWidth - 40 - scaleWidth) / (this.options.barWidth + 1));

if (this.values.length > maxBars) {
// Sample data points to fit screen
const step = Math.ceil(this.values.length / maxBars);
this.values = this.values.filter((_, i) => i % step === 0).slice(0, maxBars);
this.data = this.data.filter((_, i) => i % step === 0).slice(0, maxBars);

// Recalculate min/max after sampling
this.max = Math.max(...this.values);
this.min = Math.min(...this.values);
this.valueRange = this.max - this.min;
}
}

render() {
const rows = [];
if (this.options.showLabels) {
Expand All @@ -43,16 +70,44 @@ class AsciiBarChart extends Chart {
}

renderLabels() {
const labels = this.formattedNumbers.map((num, i) => {
const padding = Math.max(0, this.options.barWidth - num.length);
const leftPad = Math.floor(padding / 2);
const rightPad = padding - leftPad;
let labels = '';
for (let i = 0; i < this.formattedNumbers.length; i++) {
const num = this.formattedNumbers[i];
const barWidth = this.options.barWidth;
const numWidth = num.length;

// Calculate exact center position with a slight right offset
const leftPad = Math.floor((barWidth - numWidth) / 2) + 1;
const rightPad = barWidth - numWidth - leftPad;
const intensity = this.getIntensity(this.values[i]);
return `<span class="chart-label" style="--value-intensity: ${intensity}">${' '.repeat(leftPad) + num + ' '.repeat(rightPad)}</span>`;
}).join('');

// Add left padding spaces
for (let x = 0; x < leftPad; x++) {
labels += `<span class="chart-label" style="--value-intensity: ${intensity}"> </span>`;
}

// Add each digit of the number
for (let x = 0; x < num.length; x++) {
labels += `<span class="chart-label" style="--value-intensity: ${intensity}">${num[x]}</span>`;
}

// Add right padding spaces
for (let x = 0; x < rightPad; x++) {
labels += `<span class="chart-label" style="--value-intensity: ${intensity}"> </span>`;
}

// Add spacing between bars (except for the last label)
if (i < this.formattedNumbers.length - 1) {
labels += `<span class="chart-spacer"> </span>`;
}
}

const emptyLine = ' '.repeat(this.options.showScale ? 6 : 0) +
' '.repeat((this.formattedNumbers.length * this.options.barWidth) + (this.formattedNumbers.length - 1));

return [
' '.repeat(this.options.showScale ? 6 : 0) + labels,
' '.repeat(this.options.showScale ? 6 : 0) + ' '.repeat(labels.length)
emptyLine
];
}

Expand All @@ -65,13 +120,23 @@ class AsciiBarChart extends Chart {
}

renderBarRow(row) {
const rowContent = this.values.map((value, i) => {
let rowContent = '';
for (let i = 0; i < this.values.length; i++) {
const value = this.values[i];
const filled = Math.round((value / this.max) * this.options.height);
const char = row < filled ? this.options.barChar : this.options.emptyChar;
const intensity = this.getIntensity(value);
const barContent = char.repeat(this.options.barWidth - 1).padEnd(this.options.barWidth);
return `<span class="chart-bar" style="--value-intensity: ${intensity}">${barContent}</span>`;
}).join('');

// Render each character individually
for (let x = 0; x < this.options.barWidth; x++) {
rowContent += `<span class="chart-bar" style="--value-intensity: ${intensity}">${char}</span>`;
}

// Add spacing between bars (except for the last bar)
if (i < this.values.length - 1) {
rowContent += `<span class="chart-spacer"> </span>`;
}
}

if (this.options.showScale) {
const scaleValue = this.max > 0 ? Math.round((this.max / this.options.height) * (this.options.height - row)) : 0;
Expand All @@ -82,14 +147,29 @@ class AsciiBarChart extends Chart {
}

renderDates() {
const dates = this.data.map(d => {
const date = new Date(d.date);
let dates = '';
for (let i = 0; i < this.data.length; i++) {
const date = new Date(this.data[i].date);
const dateStr = date.toLocaleDateString('en-US', this.options.dateFormat);
const padding = Math.max(0, this.options.barWidth - dateStr.length);
const leftPad = Math.floor(padding / 2);
const rightPad = padding - leftPad;
return ' '.repeat(leftPad) + dateStr + ' '.repeat(rightPad);
}).join('');
const barWidth = this.options.barWidth;
const dateWidth = dateStr.length;

// Calculate exact center position with extra left padding
const leftPad = Math.floor((barWidth - dateWidth) / 2) + 1;
const rightPad = barWidth - dateWidth - leftPad;

// Add left padding
dates += ' '.repeat(leftPad);
// Add the date string
dates += dateStr;
// Add right padding
dates += ' '.repeat(rightPad);

// Add spacing between dates (except for the last date)
if (i < this.data.length - 1) {
dates += ' ';
}
}
return [' '.repeat(this.options.showScale ? 6 : 0) + dates];
}

Expand Down

0 comments on commit 72ed186

Please sign in to comment.