@nafr/echo-ui
Version:
A UI library born for WAA
212 lines (211 loc) • 8.26 kB
JavaScript
import { jsx as _jsx } from "react/jsx-runtime";
import * as d3 from 'd3';
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react';
import { useResizeObserver } from '../../../lib/hooks';
import { cn, validScaledNaN } from '../../../lib/utils';
import { useStyle } from './styles';
import { HEIGHT, LINE_COLOR, LINE_WIDTH, SHADOW_COLOR, WIDTH, SHADOW_DIRECTION, SHADOW_HEIGHT, FFT_SIZE, X_AXIS_TICKS, Y_AXIS_TICKS, SAMPLE_RATE, AMPLITUDE_RANGE, GRID_COLOR, AXIS_COLOR, } from './constants';
export const Spectrogram = forwardRef((props, ref) => {
const { data: _data = [], fftSize = FFT_SIZE, amplitudeRange = AMPLITUDE_RANGE, lineColor = LINE_COLOR, lineWidth = LINE_WIDTH, axis = false, axisColor = AXIS_COLOR, xAxisTicks = X_AXIS_TICKS, yAxisTicks = Y_AXIS_TICKS, grid = false, gridColor = GRID_COLOR, shadow = false, shadowColor = SHADOW_COLOR, shadowDirection = SHADOW_DIRECTION, shadowHeight = SHADOW_HEIGHT, ...restProps } = props;
useImperativeHandle(ref, () => spectrogramRef.current);
const spectrogramRef = useRef(null);
const svgRef = useRef(null);
const xScale = useRef(null);
const yScale = useRef(null);
const dimensions = useResizeObserver(spectrogramRef, WIDTH, HEIGHT, () => {
generateScales();
generateGrid();
generateAxis();
generateLine();
generateShadow();
});
useEffect(() => {
if (!spectrogramRef.current)
return;
initShadowGradient();
}, []);
useEffect(() => {
generateLine();
generateShadow();
}, [_data]);
// Create the scales
const generateScales = () => {
const { width, height } = dimensions.current;
xScale.current = d3
.scaleLog()
.domain([20, SAMPLE_RATE / 2])
.range([0, width]);
yScale.current = d3.scaleLinear().domain(amplitudeRange).range([height, 0]);
};
// Create the shadow gradient
const initShadowGradient = () => {
if (!shadow)
return;
const svg = d3.select(svgRef.current);
const gradient = svg
.append('defs')
.append('linearGradient')
.attr('id', 'echo-area-gradient')
.attr('x1', '0%')
.attr('x2', '0%')
.attr('y1', '0%')
.attr('y2', '100%');
gradient
.append('stop')
.attr('offset', `${shadowHeight}%`)
.attr('stop-color', shadowColor)
.attr('stop-opacity', 0.4);
gradient
.append('stop')
.attr('offset', '100%')
.attr('stop-color', 'var(--echo-background)')
.attr('stop-opacity', 0);
};
// Update the chart, executed every time the data is updated
const generateLine = () => {
const svg = d3.select(svgRef.current);
svg.selectAll('g.echo-g-line').remove();
if (!_data.length)
return;
const { width, height } = dimensions.current;
const g = svg
.append('g')
.attr('class', 'echo-g-line')
.attr('width', width)
.attr('height', height);
const frequencyResolution = SAMPLE_RATE / (fftSize * 2);
const updatedData = _data.map((point, i) => ({
...point,
frequency: i * frequencyResolution,
}));
// Update line generator
const lineGenerator = d3
.line()
.x((d) => validScaledNaN(xScale.current, d.frequency, -300))
.y((d) => validScaledNaN(yScale.current, d.amplitude, -300))
.curve(d3.curveNatural)
.curve(d3.curveCatmullRom.alpha(0.5));
// Bind new data and apply transitions
g.selectAll('path.echo-path-line')
.data([updatedData])
.join('path')
.attr('class', 'echo-path-line')
.attr('d', lineGenerator)
.attr('stroke', lineColor)
.attr('stroke-width', lineWidth)
.attr('fill', 'none');
};
// Update the shadow, executed every time the data is updated
const generateShadow = () => {
if (!shadow)
return;
const svg = d3.select(svgRef.current);
svg.selectAll('g.echo-g-shadow').remove();
if (!_data.length)
return;
const { width, height } = dimensions.current;
const g = svg
.append('g')
.attr('class', 'echo-g-shadow')
.attr('width', width)
.attr('height', height);
const frequencyResolution = SAMPLE_RATE / (fftSize * 2);
const updatedData = _data.map((point, i) => ({
frequency: i * frequencyResolution,
amplitude: point.amplitude === -Infinity ? -300 : point.amplitude,
}));
// Create the area generator, to be used for the shadow
const areaGenerator = d3
.area()
.x((d) => validScaledNaN(xScale.current, d.frequency, -300))
.y((d) => validScaledNaN(yScale.current, d.amplitude, -300))
.y1(shadowDirection === 'top' ? 0 : height)
.curve(d3.curveNatural)
.curve(d3.curveCatmullRom.alpha(0.5));
g.selectAll('path.echo-path-shadow')
.data([updatedData])
.join('path')
.attr('class', 'echo-path-shadow')
.attr('d', areaGenerator)
.attr('fill', 'url(#echo-area-gradient)');
};
// Create the grid lines on brackground
const generateGrid = () => {
if (!grid)
return;
const svg = d3.select(svgRef.current);
svg.select('g.echo-g-grid').remove();
const { width, height } = dimensions.current;
const g = svg
.append('g')
.attr('class', 'echo-g-grid')
.attr('width', width)
.attr('height', height);
const line = d3
.line()
.x((d) => d.x)
.y((d) => d.y);
xAxisTicks.forEach((tick) => {
const x = xScale.current(tick);
g.append('path')
.attr('d', line([
{ x, y: 0 },
{ x, y: height },
]))
.attr('stroke', gridColor)
.attr('stroke-width', 0.5)
.attr('fill', 'none');
});
yAxisTicks.forEach((tick) => {
const y = yScale.current(tick);
g.append('path')
.attr('d', line([
{ x: 0, y },
{ x: width, y },
]))
.attr('stroke', gridColor)
.attr('stroke-width', 0.5)
.attr('fill', 'none')
.attr('transform', 'translate(0, -3)');
});
};
// Create the axis
const generateAxis = () => {
if (!axis)
return;
const { width, height } = dimensions.current;
const svg = d3.select(svgRef.current);
svg.select('g.echo-g-x-axis').remove();
svg.select('g.echo-g-y-axis').remove();
const xAxis = d3
.axisBottom(d3.scaleLog([20, SAMPLE_RATE / 2], [0, width]))
.tickValues(xAxisTicks)
.tickFormat(d3.format('~s'))
.tickSize(0);
const yAxis = d3
.axisRight(d3.scaleLinear(amplitudeRange, [height, 0]))
.tickValues(yAxisTicks)
.tickSize(0);
svg
.append('g')
.attr('class', 'echo-g-x-axis')
.call(xAxis)
.attr('transform', `translate(0, ${height - 15})`)
.attr('color', axisColor);
svg
.append('g')
.attr('class', 'echo-g-y-axis')
.call(yAxis)
.attr('transform', `translate(0, ${-3})`)
.attr('color', axisColor);
svg.selectAll('.domain').style('display', 'none');
};
const { base, svg } = useStyle();
return (_jsx("div", { ref: spectrogramRef, className: cn(base(), restProps.className), style: {
...restProps.style,
padding: 0,
overflow: 'hidden',
pointerEvents: 'none',
userSelect: 'none',
}, children: _jsx("svg", { ref: svgRef, className: cn(svg()) }) }));
});