Aren Trot
Aren Trot

Reputation: 479

React with Svg vertical stacked barchart (no third party library)

I have below piece of code. I am using React and svg for bar charts. I am not using any third party library for charts. With this below piece of code, i am able to get the bar charts. But the issue is that my bar charts show horizontally, I want to show it vertically. I am not able to figure out how to get this same piece of code working for a vertical bar chart.

I saw one video online https://egghead.io/lessons/javascript-build-a-bar-chart-with-svg-from-scratch-with-react This guy is able to achieve the bar chart vertically. I am not sure where i am going wrong in displaying it. Any changes i do or try, it always show horizontal bar chart.


export const ReleaseScopeCharts = () => {
const data = [
    {
        "name": "Transit",
        "passed": 2,
        "skipped": 5,
        "failed": 22,
    },
    {
        "name": "Access",
        "passed": 7,
        "skipped": 2,
        "failed": 11,
    }
]
const fontSize=14
const width=1000
const rowHeight=40
const colors = ["#30D158", "#005EA7", "#FF453A"];

const entries = data.map((d) => ({
    name: d.name,
    total: d.total,
    bars: ["passed", "skipped", "failed"].map((key, i) => ({
        value: d[key],
        portion: d[key] / 29,
        color: colors[i]
    }))
        .filter((bar) => bar.value)
}))
const heightPerRow = rowHeight;
const canvasHeight = entries.length * heightPerRow;
const canvasWidth = width;
const labelWidth = canvasWidth / 4;
const plotWidth = canvasWidth - labelWidth;
const verticalPadding = heightPerRow / 2;
const barHeight = heightPerRow - verticalPadding;
const horizontalPadding = 0;
const rows = entries.map((entry, i) => {
    const widths = entry.bars.map((bar) => plotWidth * bar.portion)
    const offsets = entry.bars.map((bar, i, array) => {
        const previous = array.slice(0, i);
        const offset = previous.map((bar) => bar.portion)
            .reduce((a, b) => a + b, 0)

        return offset + bar.portion / 2
    })

    const bars = entry.bars.map((bar, i) => {
        const barWidth = widths[i] - horizontalPadding;
        return (<g key={i} transform={`translate(${plotWidth * offsets[i]}, ${heightPerRow / 2})`}>
            <rect 
            rx={barHeight / 2} ry={barHeight / 2} width={barWidth} height={barHeight} fill={bar.color} x={-barWidth / 2} y={-barHeight / 2} />
            <text fill={"#fff"} fontSize={fontSize} textAnchor={"middle"} alignmentBaseline={"middle"}>{bar.value}</text>
        </g>)
    })
    return (
        <g key={i} transform={`translate(${labelWidth},${heightPerRow * i})`}>
            <g transform={`translate(${-labelWidth}, ${heightPerRow / 2})`}>
                <text
                    fontSize={fontSize}
                    textAnchor={"start"}
                    alignmentBaseline={"middle"}>{entry.name}</text>
            </g>
            {bars}
        </g>
    )
})
return (
    <div className="new-card">
        <div>
        </div>
        <svg viewBox={`0, 0, ${canvasWidth}, ${canvasHeight}`}
        height={canvasHeight}
        width={canvasWidth}
        >
            {rows}
        </svg>
    </div>
)}

Can someone please help me figure out where i went wrong.

Upvotes: 2

Views: 1260

Answers (2)

Mark Schultheiss
Mark Schultheiss

Reputation: 34158

I am going to take this slightly out of context to show a bar result - more "bars" can be added in the SVG, will leave that to you.

Note the important parts are the <rect width="20" height="75" y="25"></rect> where the width and height show the "size" of the bar.

For fun I added some mouse-over CSS.

.bar {
  fill: lime;
  /* changes the background */
  height: 10rem;
  transition: fill .3s ease;
  cursor: pointer;
  font-family: Helvetica, sans-serif;
}

.bar text {
  color: black;
  font-size: 0.75rem;
}

.bar:hover,
.bar:focus {
  fill: blue;
}

.bar:hover text,
.bar:focus text {
  fill: red;
}
<svg class="chart" width="200" height="200" aria-labelledby="title desc" role="img">
  <title id="title">A single bar chart</title>
  <desc id="desc">FunBar day</desc>
  <g class="bar">
    <rect width="20" height="75" y="25"></rect>
    <text x="0" y="115" dy=".25em">Fun</text>
  </g>
   <g class="bar">
    <rect width="20" height="25" y="75" x="30"></rect>
    <text x="30" y="115" dy=".25em">Goat</text>
  </g>
 </svg>

Upvotes: 0

Yadab
Yadab

Reputation: 1883

I just figured this out for you. You can do the rest calculation.

export const ReleaseScopeCharts = () => {
    const data = [
        {
            name: 'Transit',
            passed: 2,
            skipped: 5,
            failed: 22,
        },
        {
            name: 'Access',
            passed: 7,
            skipped: 2,
            failed: 11,
        },
    ];
    const width = 500;
    const colors = ['#30D158', '#005EA7', '#FF453A'];

    const entries = data.map((d) => ({
        name: d.name,
        total: ['passed', 'skipped', 'failed'].reduce((acc, key) => acc + d[key], 0),
        bars: ['passed', 'skipped', 'failed'].map((key, i) => ({
            value: d[key],
            color: colors[i],
        }))
            .filter((bar) => bar.value),
    }));

    const rows = (entry) => entry.bars.map((bar, index) => {
        const height = (bar.value / entry.total) * 100;
        return (
            <g key={index}>
                <rect
                    width={50}
                    height={`${height}%`}
                    fill={bar.color}
                    x={index * 60} // multiply with the width (50) + 10 for space
                />
            </g>
        );
    });

    return (
        <div className="new-card">
            <div />
            {entries.map((entry) => (
                <>
                    {entry.name}
                    <svg viewBox={`0, 0, ${width}, ${500}`}
                        height={500}
                        width={width}
                        style={{ transform: `rotateX(180deg)` }}
                    >
                        {rows(entry)}
                    </svg>
                </>
            ))}
        </div>
    );
};

Upvotes: 1

Related Questions