Render JSX-elements as multi-level pyramid

Viewed 408

I need to create a pyramid figure with 4 levels division something like this: enter image description here

Although I have achieved the same with the following code set:

const PyramidChart = () => {
  return (
    <div className="d-flex flex-column align-items-center pyramid_wrap">
      <div className="category_one">
        <h6>2</h6>
      </div>
      <div className="category_two">
        <h6>8</h6>
      </div>
      <div className="category_three">
        <h6>11</h6>
      </div>
      <div className="category_four">
        <h6>16</h6>
      </div>
    </div>
  );
};

ReactDOM.render(
  <PyramidChart />,
  document.getElementById('root')
);
.pyramid_wrap {
  height: 100%;
  text-align: center;
}

.category_one {
  width: 70px;
  height: 30px;
  border-left: 35px solid transparent;
  border-right: 35px solid transparent;
  border-bottom: 50px solid tomato;
}

.category_two {
  width: 116px;
  height: 30px;
  border-left: 22px solid transparent;
  border-right: 22px solid transparent;
  border-bottom: 28px solid orange;
}

.category_three {
  width: 162px;
  height: 30px;
  border-left: 22px solid transparent;
  border-right: 22px solid transparent;
  border-bottom: 28px solid cyan;
}

.category_four {
  width: 208px;
  height: 30px;
  border-left: 22px solid transparent;
  border-right: 22px solid transparent;
  border-bottom: 28px solid teal;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>


<div id="root"></div>

And got something like this:

enter image description here

But the issue I am unable to resolve is to align the text in top-level pyramid area to center. It is somehow always out of sync with the it is aligned in.

Any help to rectify this issue is appreciated :)

2 Answers

Let's try moving from CSS Triangle to :before and :after with skew transform property.

I haven't changed your HTML, just add a .value class to your <h6> to style them.

Basically all you have to do is to set to every <div> a :before with a negative degree angle and a :after with a positive degree angle and give them the same background color, in order to match.

Only for the top-level <div> I used a CSS Triangle, so you already know how it works.

In order to make the values horizontally and vertically aligned I used flexbox on the parent, combined with justify-content: center; (horizontal align) and align-items: center; (vertical align). Don't forget to always add a line-height:1em; and remove margin to the element you want to be vertical aligned. If the line-height is not equal to its actual height it will always be some pixels up or down the middle axe.

.pyramid_wrap {
  margin-top: 200px;
  height: 100%;
  text-align: center;
}

.category_one,
.category_two,
.category_three,
.category_four {
  position: relative;
  margin: 6px auto;
  display: flex;
  justify-content: center;
  align-items: center;
}

.category_one:before,
.category_one:after,
.category_two:before,
.category_two:after,
.category_three:before,
.category_three:after,
.category_four:before,
.category_four:after {
  position: absolute;
  top: 0;
  display: block;
  width: 30px;
  height: 100%;
  content: "";
}

.category_one:before,
.category_two:before,
.category_three:before,
.category_four:before {
  left: -15px;
  transform: skew(-25deg);
}

.category_one:after,
.category_two:after,
.category_three:after,
.category_four:after {
  right: -15px;
  transform: skew(25deg);
}

.category_one {
    width: 20px;
    height: 40px;
    background: tomato;

}

.category_one:before,
.category_one:after {
  width: 30px;
  height: 40px;
  background: tomato;
}

.category_one:before {
  left: -16px;
  top: 0;
}
.category_one:after {
  right: -16px;
  top: 0;
}

.category_one .value:after {
  position: absolute;
  z-index: -1;
  top: -55px;
  left: 50%;
  transform: translateX(-50%);
  content: "";
  display: block;
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 0 28px 70px 28px;
  border-color: transparent transparent tomato transparent;
}


.category_two {
    width: 70px;
    height: 40px;
    background: orange;
}

.category_two:before,
.category_two:after {
  width: 40px;
  background: orange;
}

.category_two:before {
  left: -12px;
}
.category_two:after {
  right: -12px;
}


.category_three {
  width: 120px;
  height: 40px;
  background: cyan;
}

.category_three:before,
.category_three:after {
  background: cyan;
}
.category_three:before {
  left: -9px;
}
.category_three:after {
  right: -9px;
}


.category_four {
    width: 150px;
    height: 40px;
    background: teal;
}

.category_four:before,
.category_four:after {
  background: teal;
}

.value {
  margin: 0;
  position: relative;
  z-index: 3;
  color: #fff;
  font-size: 16px;
  font-family: Arial, sans-serif;
  font-weight: normal;
  line-height: 1em;
}
 <div class="d-flex flex-column align-items-center pyramid_wrap">
        <div class="category_one">
            <h6 class="value">2</h6>
        </div>
        <div class="category_two">
          <h6 class="value">8</h6>
        </div>
        <div class="category_three">
          <h6 class="value">11</h6>
        </div>
        <div class="category_four">
          <h6 class="value">16</h6>
        </div>
      </div>

One of the possible solutions is to use CSS mask over the entire pyramid's wrapper. It may be not well supported as of now, yet provides genuine way of shaping your pyramid with SVG path (triangle):

enter image description here

In order not to let the mask shrink (hiding upper and lower layers), you may need to preserve the min-width of a wrapper (equal to number of layers multiplied by layer height - 3em).

To avoid layer labels falling outside of the mask, you may need to center the mask according to labels position (mask-position: center)

Live-demo, implementing that concept, might look as follows:

const { render } = ReactDOM,
      rootNode = document.getElementById('root')

const pyramidItems = [{label:'2', color: '#f47660'}, {label:'8', color:'#fcae60'}, {label:'11', color:'#a7e6db'}, {label:'16',color:'#79d4c5'}]

const Pyramid = ({items}) => (
  <div 
    className="wrapper"
    style={{minWidth:`${items.length}*3em`}}
  >
    {
      items.map(({label,color},key) => (
        <div 
          key={key}
          className="layer"
          style={{backgroundColor:color}}
        >
          {label}
        </div>
      ))
    }
  </div>
)

render (
  <Pyramid items={pyramidItems} />,
  rootNode
)
.wrapper {
  --triangle-shape: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAwIDEwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNTAsMCBMMTAwLDEwMCBMMCwxMDAgeiIvPjwvc3ZnPg==");
  -webkit-mask-image: var(--triangle-shape);
  -webkit-mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-image: var(--triangle-shape);
  mask-repeat: no-repeat;
  mask-position: center;
  display: flex;
  flex-direction: column;
}

.layer {
  height: 3em;
  color: #fff;
  font-size: 15px;
  display: flex;
  justify-content: center;
  align-items: center;
  border: .2em solid #fff;
  margin: -.2em 0px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script><script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script><div id="root"></div>

Related