Skip to content

Instantly share code, notes, and snippets.

@sebinsua
Created November 4, 2024 22:39
Show Gist options
  • Save sebinsua/86c4a1a71cfa9adec62d7156722f5b5b to your computer and use it in GitHub Desktop.
Save sebinsua/86c4a1a71cfa9adec62d7156722f5b5b to your computer and use it in GitHub Desktop.
import React, { useEffect, useRef, useState } from 'react';
class VolumeGenerator {
public name: string;
private volume: number;
private delta: number;
constructor(name: string, initialVolume = 50) {
this.name = name;
this.volume = initialVolume;
this.delta = 0;
}
read() {
this.volume = Math.min(100, Math.max(0, this.volume + (Math.random() - 0.5) * 20));
this.delta = Math.min(1, Math.max(-1, this.delta + (Math.random() - 0.5) * 0.2));
return { volume: this.volume, delta: this.delta };
}
}
function getColor(delta: number): string {
const hue = (delta + 1) * 60; // Maps -1->0 and 1->120 (red through yellow to green)
return `hsl(${hue}, 80%, 50%)`;
}
// Get contrasting text color based on background hue
// For our scale: red(0°) -> yellow(60°) -> green(120°)
function getContrastingColor(delta: number): string {
const hue = (delta + 1) * 60;
// Yellow-ish hues (around 60°) need dark text
// We can extend this a bit on either side for yellow-green and orange
const needsDarkText = hue > 30 && hue < 90;
return needsDarkText ? '#000000' : '#ffffff';
}
export default function Demo() {
const canvasRef = useRef<HTMLCanvasElement>(null);
const [cellSize, setCellSize] = useState({ width: 0, height: 0 });
useEffect(() => {
const canvas = canvasRef.current;
const cell = canvas?.parentElement;
if (!canvas || !cell) return;
const resizeObserver = new ResizeObserver(() => {
const cellRect = cell.getBoundingClientRect();
setCellSize(prev =>
prev.height !== cellRect.height || prev.width !== cellRect.width
? { width: cellRect.width, height: cellRect.height }
: prev
);
});
resizeObserver.observe(cell);
return () => resizeObserver.disconnect();
}, []);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext('2d');
if (!ctx) return;
const PADDING = 4;
const SAMPLE_INTERVAL = 50;
const fontSize = Math.round(cellSize.height * 0.67);
const TEXT_FONT = `${fontSize}px monospace`;
const dataGenerator = new VolumeGenerator('volume');
let rafId: number;
let lastSampleTime = performance.now();
let previousValue = { volume: 0, delta: 0 };
function draw() {
const now = performance.now();
const deltaTime = now - lastSampleTime;
if (deltaTime >= SAMPLE_INTERVAL) {
const newValue = dataGenerator.read();
// Clear canvas
ctx.clearRect(0, 0, cellSize.width, cellSize.height);
// Draw background bar
ctx.fillStyle = '#f3f4f6';
ctx.fillRect(PADDING, PADDING, cellSize.width - PADDING * 2, cellSize.height - PADDING * 2);
// Draw volume bar
const barWidth = ((cellSize.width - PADDING * 2) * (newValue.volume / 100));
ctx.fillStyle = getColor(newValue.delta);
ctx.fillRect(
PADDING,
PADDING,
barWidth,
cellSize.height - PADDING * 2
);
// Set up text properties
ctx.font = TEXT_FONT;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const text = newValue.volume.toFixed(2);
const textX = cellSize.width / 2;
// Check if text is over the bar
const isOverBar = textX < (PADDING + barWidth);
// Choose text color based on position and background color
ctx.fillStyle = isOverBar ?
getContrastingColor(newValue.delta) :
'#000000';
// Only add shadow for light text on dark backgrounds
if (isOverBar && ctx.fillStyle === '#ffffff') {
ctx.shadowColor = 'rgba(0, 0, 0, 0.4)';
ctx.shadowBlur = 3;
} else {
ctx.shadowColor = 'transparent';
ctx.shadowBlur = 0;
}
ctx.fillText(
text,
textX,
cellSize.height / 2
);
previousValue = newValue;
lastSampleTime = now;
}
rafId = requestAnimationFrame(draw);
}
draw();
return () => {
cancelAnimationFrame(rafId);
ctx.clearRect(0, 0, cellSize.width, cellSize.height);
};
}, [cellSize]);
return (
<div className="h-24">
<canvas
ref={canvasRef}
style={{ display: 'block', verticalAlign: 'top' }}
width={cellSize.width}
height={cellSize.height}
/>
</div>
);
}
@sebinsua
Copy link
Author

sebinsua commented Nov 4, 2024

import React, { useState, useEffect, useRef } from 'react';

class DataGenerator {
  private value = 50;
  private delta = 0;
  private askValue = 50;
  private bidValue = 50;

  read() {
    // For simple bar
    this.value += (Math.random() - 0.5) * 10;
    this.value = Math.max(0, Math.min(100, this.value));
    this.delta += (Math.random() - 0.5) * 0.2;
    this.delta = Math.max(-1, Math.min(1, this.delta));

    // For mirrored bars
    this.bidValue += (Math.random() - 0.5) * 8;
    this.bidValue = Math.max(0, Math.min(100, this.bidValue));
    this.askValue += (Math.random() - 0.5) * 8;
    this.askValue = Math.max(0, Math.min(100, this.askValue));

    return {
      value: this.value,
      delta: this.delta,
      bidValue: this.bidValue,
      askValue: this.askValue
    };
  }
}

const BarComponent = ({ type }: { type: 'mirrored' | 'skewed' | 'winged' }) => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [cellSize, setCellSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    const canvas = canvasRef.current;
    const cell = canvas?.parentElement;
    if (!canvas || !cell) return;

    const resizeObserver = new ResizeObserver(() => {
      const cellRect = cell.getBoundingClientRect();
      setCellSize(prev => 
        prev.height !== cellRect.height || prev.width !== cellRect.width
          ? { width: cellRect.width, height: cellRect.height }
          : prev
      );
    });
    resizeObserver.observe(cell);
    return () => resizeObserver.disconnect();
  }, []);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const generator = new DataGenerator();
    const SAMPLE_INTERVAL = 50;
    let rafId: number;
    let lastSampleTime = performance.now();
    let minValue = 50;
    let maxValue = 50;

    function draw() {
      const now = performance.now();
      const deltaTime = now - lastSampleTime;
      
      if (deltaTime >= SAMPLE_INTERVAL) {
        const { value, delta, bidValue, askValue } = generator.read();
        
        ctx.clearRect(0, 0, cellSize.width, cellSize.height);
        const PADDING = 4;

        if (type === 'mirrored') {
          // Mirrored bars
          const centerX = cellSize.width / 2;
          const barHeight = cellSize.height - 2 * PADDING;

          // Bid bar (left)
          const bidWidth = (centerX - PADDING) * (bidValue / 100);
          ctx.fillStyle = '#22c55e';
          ctx.fillRect(centerX - bidWidth, PADDING, bidWidth, barHeight);

          // Ask bar (right)
          const askWidth = (centerX - PADDING) * (askValue / 100);
          ctx.fillStyle = '#ef4444';
          ctx.fillRect(centerX, PADDING, askWidth, barHeight);

          // Center line
          ctx.fillStyle = '#000';
          ctx.fillRect(centerX - 1, PADDING, 2, barHeight);

          // Values
          ctx.font = `${Math.round(cellSize.height * 0.5)}px monospace`;
          ctx.textAlign = 'right';
          ctx.textBaseline = 'middle';
          ctx.fillStyle = '#000';
          ctx.fillText(bidValue.toFixed(2), centerX - 8, cellSize.height / 2);
          ctx.textAlign = 'left';
          ctx.fillText(askValue.toFixed(2), centerX + 8, cellSize.height / 2);

        } else if (type === 'skewed') {
          // Skewed bar
          const barWidth = (cellSize.width - 2 * PADDING) * (value / 100);
          const skewAmount = delta * 10;

          ctx.save();
          ctx.translate(PADDING, cellSize.height / 2);
          ctx.transform(1, Math.tan(skewAmount * Math.PI / 180), 0, 1, 0, 0);
          
          ctx.fillStyle = delta >= 0 ? '#22c55e' : '#ef4444';
          ctx.fillRect(0, -cellSize.height/3, barWidth, cellSize.height/1.5);
          
          ctx.restore();

          // Value
          ctx.font = `${Math.round(cellSize.height * 0.5)}px monospace`;
          ctx.textAlign = 'center';
          ctx.textBaseline = 'middle';
          ctx.fillStyle = '#000';
          ctx.fillText(value.toFixed(2), cellSize.width / 2, cellSize.height / 2);

        } else if (type === 'winged') {
          // Winged bar
          const barHeight = cellSize.height - 2 * PADDING;
          
          // Update min/max with decay
          minValue = Math.min(value, minValue + 0.1);
          maxValue = Math.max(value, maxValue - 0.1);
          
          // Main bar
          const barWidth = (cellSize.width - 2 * PADDING) * (value / 100);
          ctx.fillStyle = delta >= 0 ? '#22c55e' : '#ef4444';
          ctx.fillRect(PADDING, PADDING, barWidth, barHeight);

          // Wings
          const minX = (cellSize.width - 2 * PADDING) * (minValue / 100) + PADDING;
          const maxX = (cellSize.width - 2 * PADDING) * (maxValue / 100) + PADDING;
          
          ctx.strokeStyle = '#666';
          ctx.lineWidth = 2;
          
          ctx.beginPath();
          ctx.moveTo(minX, PADDING);
          ctx.lineTo(minX, cellSize.height - PADDING);
          ctx.stroke();
          
          ctx.beginPath();
          ctx.moveTo(maxX, PADDING);
          ctx.lineTo(maxX, cellSize.height - PADDING);
          ctx.stroke();

          // Value
          ctx.font = `${Math.round(cellSize.height * 0.5)}px monospace`;
          ctx.textAlign = 'center';
          ctx.textBaseline = 'middle';
          ctx.fillStyle = '#000';
          ctx.fillText(value.toFixed(2), cellSize.width / 2, cellSize.height / 2);
        }

        lastSampleTime = now;
      }

      rafId = requestAnimationFrame(draw);
    }

    draw();
    return () => cancelAnimationFrame(rafId);
  }, [cellSize, type]);

  return (
    <canvas
      ref={canvasRef}
      style={{ display: 'block', width: '100%', height: '100%' }}
      width={cellSize.width}
      height={cellSize.height}
    />
  );
};

export default function Demo() {
  return (
    <div style={{ display: 'grid', gap: '16px', gridTemplateRows: 'repeat(3, 48px)' }}>
      <div style={{ border: '1px solid #eee', height: '48px' }}>
        <BarComponent type="mirrored" />
      </div>
      <div style={{ border: '1px solid #eee', height: '48px' }}>
        <BarComponent type="skewed" />
      </div>
      <div style={{ border: '1px solid #eee', height: '48px' }}>
        <BarComponent type="winged" />
      </div>
    </div>
  );
}

@sebinsua
Copy link
Author

sebinsua commented Nov 4, 2024

import React, { useEffect, useRef, useState } from 'react';

interface VolumeBarProps {
  mirror?: boolean;
  className?: string;
}

function VolumeBar({ mirror = false, className = "" }: VolumeBarProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [cellSize, setCellSize] = useState({ width: 0, height: 0 });
  
  useEffect(() => {
    const canvas = canvasRef.current;
    const cell = canvas?.parentElement;
    if (!canvas || !cell) return;

    const resizeObserver = new ResizeObserver(() => {
      const cellRect = cell.getBoundingClientRect();
      setCellSize(prev => 
        prev.height !== cellRect.height || prev.width !== cellRect.width
          ? { width: cellRect.width, height: cellRect.height }
          : prev
      );
    });
    resizeObserver.observe(cell);
    return () => resizeObserver.disconnect();
  }, []);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const PADDING = 4;
    const SAMPLE_INTERVAL = 50;
    const fontSize = Math.round(cellSize.height * 0.67);
    const TEXT_FONT = `${fontSize}px monospace`;
    
    const dataGenerator = new VolumeGenerator('volume');
    let rafId: number;
    let lastSampleTime = performance.now();

    function draw() {
      const now = performance.now();
      const deltaTime = now - lastSampleTime;
      
      if (deltaTime >= SAMPLE_INTERVAL) {
        const newValue = dataGenerator.read();
        
        ctx.clearRect(0, 0, cellSize.width, cellSize.height);

        // Draw background
        ctx.fillStyle = '#f3f4f6';
        ctx.fillRect(PADDING, PADDING, cellSize.width - PADDING * 2, cellSize.height - PADDING * 2);

        // Calculate bar dimensions
        const barWidth = ((cellSize.width - PADDING * 2) * (newValue.volume / 100));
        
        // Draw volume bar (from left or right depending on mirror)
        ctx.fillStyle = getColor(newValue.delta);
        if (mirror) {
          ctx.fillRect(
            cellSize.width - PADDING - barWidth,
            PADDING,
            barWidth,
            cellSize.height - PADDING * 2
          );
        } else {
          ctx.fillRect(
            PADDING,
            PADDING,
            barWidth,
            cellSize.height - PADDING * 2
          );
        }

        // Setup text
        ctx.font = TEXT_FONT;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        
        const text = newValue.volume.toFixed(2);
        const textX = cellSize.width / 2;
        
        // Check if text is over the bar (accounting for mirror)
        const isOverBar = mirror ? 
          textX > (cellSize.width - PADDING - barWidth) : 
          textX < (PADDING + barWidth);
        
        // Set text color
        ctx.fillStyle = isOverBar ? 
          getContrastingColor(newValue.delta) : 
          '#000000';
        
        // Shadow for contrast
        if (isOverBar && ctx.fillStyle === '#ffffff') {
          ctx.shadowColor = 'rgba(0, 0, 0, 0.4)';
          ctx.shadowBlur = 3;
        } else {
          ctx.shadowColor = 'transparent';
          ctx.shadowBlur = 0;
        }
        
        ctx.fillText(
          text,
          textX,
          cellSize.height / 2
        );
        
        lastSampleTime = now;
      }

      rafId = requestAnimationFrame(draw);
    }

    draw();

    return () => {
      cancelAnimationFrame(rafId);
      ctx.clearRect(0, 0, cellSize.width, cellSize.height);
    };
  }, [cellSize, mirror]);

  return (
    <canvas
      ref={canvasRef}
      className={className}
      style={{ display: 'block', width: '100%', height: '100%' }}
      width={cellSize.width}
      height={cellSize.height}
    />
  );
}

// Demo showing two bars side by side
export default function Demo() {
  return (
    <div className="flex gap-1">
      <div className="h-24 flex-1">
        <VolumeBar mirror={true} />
      </div>
      <div className="h-24 flex-1">
        <VolumeBar />
      </div>
    </div>
  );
}

// Helper functions
class VolumeGenerator {
  public name: string;
  private volume: number;
  private delta: number;

  constructor(name: string, initialVolume = 50) {
    this.name = name;
    this.volume = initialVolume;
    this.delta = 0;
  }

  read() {
    this.volume = Math.min(100, Math.max(0, this.volume + (Math.random() - 0.5) * 20));
    this.delta = Math.min(1, Math.max(-1, this.delta + (Math.random() - 0.5) * 0.2));
    return { volume: this.volume, delta: this.delta };
  }
}

function getColor(delta: number): string {
  const hue = (delta + 1) * 60;
  return `hsl(${hue}, 80%, 50%)`;
}

function getContrastingColor(delta: number): string {
  const hue = (delta + 1) * 60;
  const needsDarkText = hue > 30 && hue < 90;
  return needsDarkText ? '#000000' : '#ffffff';
}

@sebinsua
Copy link
Author

sebinsua commented Nov 4, 2024

import React, { useEffect, useRef, useState } from 'react';

// Same helper classes...
class VolumeGenerator {
  private volume: number;
  private delta: number;

  constructor(name: string, initialVolume = 50) {
    this.volume = initialVolume;
    this.delta = 0;
  }

  read() {
    this.volume = Math.min(100, Math.max(0, this.volume + (Math.random() - 0.5) * 20));
    this.delta = Math.min(1, Math.max(-1, this.delta + (Math.random() - 0.5) * 0.2));
    return { volume: this.volume, delta: this.delta };
  }
}

function getColor(delta: number): string {
  const hue = (delta + 1) * 60;
  return `hsl(${hue}, 80%, 50%)`;
}

function getContrastingColor(delta: number): string {
  const hue = (delta + 1) * 60;
  const needsDarkText = hue > 30 && hue < 90;
  return needsDarkText ? '#000000' : '#ffffff';
}

interface VolumeBarProps {
  mirror?: boolean;
  showWings?: boolean;
  wingDecayRate?: number;
}

function VolumeBar({ mirror = false, showWings = false, wingDecayRate = 0.995 }: VolumeBarProps) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [cellSize, setCellSize] = useState({ width: 0, height: 0 });
  
  const wingsRef = useRef({
    min: 50,
    max: 50,
    lastValue: 50
  });

  useEffect(() => {
    const canvas = canvasRef.current;
    const cell = canvas?.parentElement;
    if (!canvas || !cell) return;

    const resizeObserver = new ResizeObserver(() => {
      const cellRect = cell.getBoundingClientRect();
      setCellSize(prev => 
        prev.height !== cellRect.height || prev.width !== cellRect.width
          ? { width: cellRect.width, height: cellRect.height }
          : prev
      );
    });
    resizeObserver.observe(cell);
    return () => resizeObserver.disconnect();
  }, []);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const PADDING = 4;
    const SAMPLE_INTERVAL = 50;
    const WING_WIDTH = 2;
    const fontSize = Math.round(cellSize.height * 0.67);
    const TEXT_FONT = `${fontSize}px monospace`;
    
    const dataGenerator = new VolumeGenerator('volume');
    let rafId: number;
    let lastSampleTime = performance.now();

    function updateWings(value: number) {
      const wings = wingsRef.current;
      
      if (value < wings.min) wings.min = value;
      if (value > wings.max) wings.max = value;
      
      wings.min = wings.min + (value - wings.min) * (1 - wingDecayRate);
      wings.max = wings.max + (value - wings.max) * (1 - wingDecayRate);
      
      wings.lastValue = value;
    }

    function draw() {
      const now = performance.now();
      const deltaTime = now - lastSampleTime;
      
      if (deltaTime >= SAMPLE_INTERVAL) {
        const newValue = dataGenerator.read();
        
        ctx.clearRect(0, 0, cellSize.width, cellSize.height);

        // Draw background
        ctx.fillStyle = '#f3f4f6';
        ctx.fillRect(PADDING, PADDING, cellSize.width - PADDING * 2, cellSize.height - PADDING * 2);

        const barWidth = ((cellSize.width - PADDING * 2) * (newValue.volume / 100));
        
        // Draw volume bar
        ctx.fillStyle = getColor(newValue.delta);
        if (mirror) {
          // Right bar: starts from left edge
          ctx.fillRect(
            PADDING,
            PADDING,
            barWidth,
            cellSize.height - PADDING * 2
          );
        } else {
          // Left bar: starts from right edge
          ctx.fillRect(
            cellSize.width - PADDING - barWidth,
            PADDING,
            barWidth,
            cellSize.height - PADDING * 2
          );
        }

        // Draw wings if enabled
        if (showWings) {
          updateWings(newValue.volume);
          const wings = wingsRef.current;
          
          const minWidth = ((cellSize.width - PADDING * 2) * (wings.min / 100));
          const maxWidth = ((cellSize.width - PADDING * 2) * (wings.max / 100));
          
          ctx.strokeStyle = '#666';
          ctx.lineWidth = WING_WIDTH;
          
          if (mirror) {
            // Right side wings from left
            ctx.beginPath();
            ctx.moveTo(PADDING + minWidth, PADDING);
            ctx.lineTo(PADDING + minWidth, cellSize.height - PADDING);
            ctx.stroke();
            
            ctx.beginPath();
            ctx.moveTo(PADDING + maxWidth, PADDING);
            ctx.lineTo(PADDING + maxWidth, cellSize.height - PADDING);
            ctx.stroke();
          } else {
            // Left side wings from right
            ctx.beginPath();
            ctx.moveTo(cellSize.width - PADDING - minWidth, PADDING);
            ctx.lineTo(cellSize.width - PADDING - minWidth, cellSize.height - PADDING);
            ctx.stroke();
            
            ctx.beginPath();
            ctx.moveTo(cellSize.width - PADDING - maxWidth, PADDING);
            ctx.lineTo(cellSize.width - PADDING - maxWidth, cellSize.height - PADDING);
            ctx.stroke();
          }
        }

        // Setup text
        ctx.font = TEXT_FONT;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        
        const text = newValue.volume.toFixed(2);
        const textX = cellSize.width / 2;
        
        const isOverBar = mirror ? 
          textX < PADDING + barWidth : 
          textX > cellSize.width - PADDING - barWidth;
        
        ctx.fillStyle = isOverBar ? 
          getContrastingColor(newValue.delta) : 
          '#000000';
        
        if (isOverBar && ctx.fillStyle === '#ffffff') {
          ctx.shadowColor = 'rgba(0, 0, 0, 0.4)';
          ctx.shadowBlur = 3;
        } else {
          ctx.shadowColor = 'transparent';
          ctx.shadowBlur = 0;
        }
        
        ctx.fillText(
          text,
          textX,
          cellSize.height / 2
        );
        
        lastSampleTime = now;
      }

      rafId = requestAnimationFrame(draw);
    }

    draw();

    return () => cancelAnimationFrame(rafId);
  }, [cellSize, mirror, showWings, wingDecayRate]);

  return (
    <canvas
      ref={canvasRef}
      style={{ display: 'block', width: '100%', height: '100%' }}
      width={cellSize.width}
      height={cellSize.height}
    />
  );
}

export default function Demo() {
  return (
    <div className="flex gap-4">
      <div className="flex-1 h-24">
        <VolumeBar showWings={true} />
      </div>
      <div className="flex-1 h-24">
        <VolumeBar showWings={true} mirror={true} />
      </div>
    </div>
  );
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment