Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ranged bar chart #7

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions samples/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import NavBar from './utils/NavBar';
import NormalBarChart from './pages/NormalBarChart';
import StackedBarChart from './pages/StackedBarChart';
import RangedBarChart from './pages/RangedBarChart';

const App = () => {
return (
Expand All @@ -12,6 +13,7 @@ const App = () => {
<Routes>
<Route path="/" element={<NormalBarChart />} />
<Route path="/stacked" element={<StackedBarChart />} />
<Route path="/ranged" element={<RangedBarChart />} />
</Routes>
</div>
</Router>
Expand Down
76 changes: 76 additions & 0 deletions samples/pages/RangedBarChart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useState } from 'react';
import { BarChart, CartesianGrid, XAxis, YAxis, Tooltip, Legend, Bar } from '../../src';
import BarChartControls from '../utils/BarChartControls';

const RangedBarChart = () => {
const [width, setWidth] = useState(600);
const [height, setHeight] = useState(400);
const [barCategoryGap, setBarCategoryGap] = useState('10%');
const [barGap, setBarGap] = useState(4);
const [layout, setLayout] = useState<'horizontal' | 'vertical'>('horizontal');
const [margin, setMargin] = useState({ top: 5, right: 5, bottom: 5, left: 5 });
const [showXAxis, setShowXAxis] = useState(true);
const [showYAxis, setShowYAxis] = useState(true);
const [showCartesianGrid, setShowCartesianGrid] = useState(true);
const [showTooltip, setShowTooltip] = useState(true);
const [showLegend, setShowLegend] = useState(true);

const data = [
{ name: '05-01', temperature: [-6, 12] },
{ name: '05-02', temperature: [6, 12] },
{ name: '05-03', temperature: [3, 12] },
{ name: '05-04', temperature: [4, 12] },
{ name: '05-05', temperature: [12, 15] },
{ name: '05-06', temperature: [5, 15] },
{ name: '05-07', temperature: [3, 12] },
{ name: '05-08', temperature: [0, 8] },
{ name: '05-09', temperature: [-3, 5] },
];

return (
<div>
<BarChartControls
width={width}
setWidth={setWidth}
height={height}
setHeight={setHeight}
barCategoryGap={barCategoryGap}
setBarCategoryGap={setBarCategoryGap}
barGap={barGap}
setBarGap={setBarGap}
layout={layout}
setLayout={setLayout}
margin={margin}
setMargin={setMargin}
showXAxis={showXAxis}
setShowXAxis={setShowXAxis}
showYAxis={showYAxis}
setShowYAxis={setShowYAxis}
showCartesianGrid={showCartesianGrid}
setShowCartesianGrid={setShowCartesianGrid}
showTooltip={showTooltip}
setShowTooltip={setShowTooltip}
showLegend={showLegend}
setShowLegend={setShowLegend}
/>
<BarChart
data={data}
width={width}
height={height}
margin={margin}
barCategoryGap={barCategoryGap}
barGap={barGap}
layout={layout}
>
{showCartesianGrid && <CartesianGrid strokeDasharray="3 3" />}
{showXAxis && <XAxis dataKey="day" />}
{showYAxis && <YAxis />}
{showTooltip && <Tooltip />}
{showLegend && <Legend />}
<Bar dataKey="temperature" fill="#8884d8" />
</BarChart>
</div>
);
};

export default RangedBarChart;
13 changes: 8 additions & 5 deletions samples/utils/NavBar.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import React from 'react';
import { Link } from 'react-router-dom';

const NavBar: React.FC = () => {
const NavBar = () => {
return (
<nav className="navbar flex justify-between items-center p-4 mb-2">
<div className="flex space-x-4">
<Link className="nav-link text-lg font-semibold" to="/">
<nav className="bg-gray-800 p-4">
<div className="container mx-auto flex space-x-4">
<Link to="/" className="text-white hover:underline">
Normal BarChart
</Link>
<Link className="nav-link text-lg font-semibold" to="/stacked">
<Link to="/stacked" className="text-white hover:underline">
Stacked BarChart
</Link>
<Link to="/ranged" className="text-white hover:underline">
Ranged BarChart
</Link>
</div>
</nav>
);
Expand Down
47 changes: 47 additions & 0 deletions src/Bar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ interface BarProps {
width?: number;
height?: number;
maxValue?: number;
minValue?: number;
layout?: 'horizontal' | 'vertical';
barIndex?: number;
totalBars?: number;
Expand All @@ -26,6 +27,7 @@ const Bar = ({
width = 0,
height = 0,
maxValue = 0,
minValue = 0,
layout = 'horizontal',
barIndex = 0,
totalBars = 1,
Expand All @@ -39,6 +41,51 @@ const Bar = ({
<g>
{data.map((entry) => {
const value = entry[dataKey];

if (Array.isArray(value)) {
const [minValueRange, maxValueRange] = value;

if (layout === 'horizontal') {
const barHeight = ((maxValueRange - minValueRange) / (maxValue - minValue)) * height;
return (
<rect
key={uuidv4()}
x={stackIdPos * (width + barGap)}
y={
height -
((maxValueRange - minValue) / (maxValue - minValue)) * height -
accumulatedHeight
}
width={width}
height={barHeight}
fill={fill}
onMouseOver={(event) => {
const { name, ...rest } = entry;
onMouseOver(event, { name, ...rest });
}}
onMouseOut={onMouseOut}
/>
);
} else {
const barWidth = ((maxValueRange - minValueRange) / (maxValue - minValue)) * width;
return (
<rect
key={uuidv4()}
x={((minValueRange - minValue) / (maxValue - minValue)) * width + accumulatedHeight}
y={stackIdPos * (height + barGap)}
width={barWidth}
height={height}
fill={fill}
onMouseOver={(event) => {
const { name, ...rest } = entry;
onMouseOver(event, { name, ...rest });
}}
onMouseOut={onMouseOut}
/>
);
}
}

const barHeight = (value / maxValue) * height;
const barWidth = (value / maxValue) * width;

Expand Down
27 changes: 18 additions & 9 deletions src/BarChart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ const BarChart = ({
const hasStackedBars = React.Children.toArray(children).some(
(child) => (child as React.ReactElement).props.stackId,
);
const maxValue = roundMaxValue(data, hasStackedBars);
const { maxValue, minValue } = roundMaxValue(data, hasStackedBars);

// Asignar un stackId único a cada Bar que no lo tenga
const barComponents = React.Children.toArray(children).map((child) => {
Expand Down Expand Up @@ -126,7 +126,6 @@ const BarChart = ({
})
.filter((val) => val !== null);

// Actualizar el tooltip solo si los valores son válidos
if (values.length > 0) {
setTooltipData({ name: entry.name, values });
const svgRect = svgRef.current?.getBoundingClientRect();
Expand All @@ -140,12 +139,9 @@ const BarChart = ({
setTooltipData(null);
};

const categoryGap = parseGap(
barCategoryGap,
layout === 'horizontal'
? width - (margin.left ?? DEFAULT_MARGIN) - rightMargin - leftMargin
: height - (margin.top ?? DEFAULT_MARGIN) - (margin.bottom ?? DEFAULT_MARGIN),
);
const handleMouseLeave = () => {
setTooltipData(null);
};

const totalGroups = data.length;
const totalBars = Object.keys(groupedBarComponents).length;
Expand Down Expand Up @@ -183,6 +179,7 @@ const BarChart = ({
? height - (margin.top ?? DEFAULT_MARGIN) - (margin.bottom ?? DEFAULT_MARGIN)
: barSize - adjustedBarGap,
maxValue,
minValue,
barIndex,
totalBars,
barGap: adjustedBarGap,
Expand Down Expand Up @@ -212,7 +209,13 @@ const BarChart = ({
margin: `${margin.top ?? DEFAULT_MARGIN}px ${margin.right ?? DEFAULT_MARGIN}px ${margin.bottom ?? DEFAULT_MARGIN}px ${margin.left ?? DEFAULT_MARGIN}px`,
}}
>
<svg ref={svgRef} width={width} height={height + height * 0.1} className='border border-gray-300'>
<svg
ref={svgRef}
width={width}
height={height + height * 0.1}
className='border border-gray-300'
onMouseLeave={handleMouseLeave} // Aquí agregas el manejador de evento
>
<g
transform={`translate(${(margin.left ?? DEFAULT_MARGIN) + leftMargin}, ${
(margin.top ?? DEFAULT_MARGIN) + height * 0.025
Expand All @@ -224,6 +227,7 @@ const BarChart = ({
<YAxis
height={height - (margin.top ?? DEFAULT_MARGIN) - (margin.bottom ?? DEFAULT_MARGIN)}
maxValue={maxValue}
minValue={minValue}
layout={layout}
/>
)}
Expand All @@ -240,6 +244,8 @@ const BarChart = ({
width={width - (margin.left ?? DEFAULT_MARGIN) - rightMargin - leftMargin}
height={height - (margin.top ?? DEFAULT_MARGIN) - (margin.bottom ?? DEFAULT_MARGIN)}
dataKey='name'
maxValue={maxValue}
minValue={minValue}
layout={layout}
/>
)}
Expand All @@ -254,6 +260,7 @@ const BarChart = ({
height={height - (margin.top ?? DEFAULT_MARGIN) - (margin.bottom ?? DEFAULT_MARGIN)}
dataKey='name'
maxValue={maxValue}
minValue={minValue}
layout={layout}
/>
)}
Expand All @@ -269,6 +276,8 @@ const BarChart = ({
data={data}
width={width - (margin.left ?? DEFAULT_MARGIN) - rightMargin}
height={height - (margin.top ?? DEFAULT_MARGIN) - (margin.bottom ?? DEFAULT_MARGIN)}
maxValue={maxValue}
minValue={minValue}
layout={layout}
/>
)}
Expand Down
62 changes: 56 additions & 6 deletions src/BarChart/utils.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,60 @@
import { ChartData } from '../constants';

export const roundMaxValue = (data: ChartData, stacked: boolean = false): number => {
export const findMinValue = (data: ChartData): number => {
let minValue: number;

minValue = Math.min(
...data.map((d) =>
Math.min(
...Object.values(d).map((v) => {
if (typeof v === 'number') {
return v;
} else if (Array.isArray(v)) {
return Math.min(...v); // Usar el valor mínimo del rango
}
return Infinity; // Valor por defecto si no es ni número ni array
}),
),
),
);

return Math.floor(minValue);
};

export const roundMaxValue = (data: ChartData, stacked: boolean = false): { maxValue: number; minValue: number } => {
let maxValue: number;
let minValue: number = findMinValue(data);

if (stacked) {
const stackedSums = data.map((d) =>
Object.values(d).reduce((sum, value) => (typeof value === 'number' ? sum + value : sum), 0),
Object.values(d).reduce((sum, value) => {
if (typeof value === 'number') {
return sum + value;
} else if (Array.isArray(value)) {
return sum + Math.max(...value); // Sumar el valor máximo del rango
}
return sum;
}, 0),
);
maxValue = Math.max(...stackedSums);
} else {
maxValue = Math.max(...data.map((d) => Math.max(...Object.values(d).filter((v) => typeof v === 'number'))));
maxValue = Math.max(
...data.map((d) =>
Math.max(
...Object.values(d).map((v) => {
if (typeof v === 'number') {
return v;
} else if (Array.isArray(v)) {
return Math.max(...v); // Usar el valor máximo del rango
}
return -Infinity; // Valor por defecto si no es ni número ni array
}),
),
),
);
}

const magnitude = Math.pow(10, Math.floor(Math.log10(maxValue)));

// Ajuste para redondeo más preciso
const factor = maxValue / magnitude;

if (factor <= 1.5) {
Expand All @@ -27,7 +67,17 @@ export const roundMaxValue = (data: ChartData, stacked: boolean = false): number
maxValue = 10 * magnitude;
}

return Math.ceil(maxValue);
maxValue = Math.ceil(maxValue);

// Asegurar que el valor mínimo y máximo sean simétricos
if (minValue < 0) {
minValue = -maxValue;
}

return {
maxValue: maxValue,
minValue: minValue,
};
};

export const parseGap = (gap: string | number, totalSize: number): number => {
Expand Down
Loading
Loading