<div id="app"></div>
@import url("https://fonts.googleapis.com/css2?family=Alexandria&display=swap");
html,
body,
#app {
  height: 100vh;
}
body {
  margin: 0;
  font-family: "Alexandria", sans-serif;
  font-size: 1em;
}
h1 {
  font-size: 1.3em;
  text-align:center;
}
#app {
  display: grid;
  justify-items: center;
  align-content: center;
  grid-template-columns: 20% 10% 50% 20%;
  grid-template-rows: 60vh 10vh;
  grid-gap: 0.5em;
}
form {
  display: flex;
  flex-direction: column;
  justify-content: space-evenly;
  align-items: center;
  grid-row: 1/3;
  grid-column: 2/4;
  width: 90%;
  height: 70vh;
  background: linear-gradient(
      115deg,
      rgba(255, 234, 0, 0.5),
      rgba(255, 0, 0, 0) 70.71%
    ),
    linear-gradient(217deg, rgba(255, 0, 0, 0.5), rgba(255, 0, 0, 0) 70.71%),
    linear-gradient(127deg, rgba(0, 255, 0, 0.5), rgba(0, 255, 0, 0) 70.71%),
    linear-gradient(336deg, rgba(0, 0, 255, 0.5), rgba(0, 0, 255, 0) 70.71%);
  border-radius: 5px;
}
form label {
  display: flex;
  flex-direction: column;
}
input {
  padding: 0.5%;
  border-radius: 0.5em;
  border: none;
  text-align:center;
}
input.error {
  border: 1px solid red;
}
input[type="submit"] {
  background: rgb(9, 189, 90);
  border: rgb(9, 189, 90);
  color: white;
  cursor: pointer;
  font-weight: bold;
}
input[type="submit"]:hover {
  background: rgb(9, 196, 93);
  border: rgb(9, 196, 93);
}
input[type="submit"]:active {
  background: rgb(8, 158, 75);
  border: rgb(8, 158, 75);
}
input[type="submit"]:disabled {
  opacity: 0.5;
}
.box {
  width: 85%;
  height: 65vh;
  grid-row: 1/3;
  grid-column: 3/4;
  overflow-x: auto;
  overflow-y: clip;
  justify-self: left;
  border-left: 1px solid black;
}
.histogram {
  height: 50vh;
  border-bottom: 3px solid black;
  border-left: 3px solid black;
  display: flex;
  width: max-content;
  justify-content: space-around;
  align-items: flex-end;
}
.y-axis {
  display: flex;
  flex-direction: column-reverse;
  justify-content: space-evenly;
  align-items: center;
  height: 50vh;
  grid-row: 1/2;
  grid-column: 2/3;
  justify-self: right;
}

.column-label {
  display: flex;
  flex-direction: column-reverse;
  width: 2vw;
  height: inherit;
  margin: 0 1em;
}
.column {
  width: 2vw;
  height: 0px;
  border-top-left-radius: 5px;
  border-top-right-radius: 5px;
  cursor: pointer;
}
[class^="label"] {
  position: relative;
  width: 2em;
  height: 2em;
  background: white;
  color: white;
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: 5px;
  visibility: hidden;
  opacity: 0;
}
[class^="label"]::after {
  content: "";
  display: block;
  position: absolute;
  top: 25px;
  left: 7px;
  clip-path: polygon(49% 100%, 0 0, 100% 0);
  width: 1.05em;
  height: 1.05em;
  background: inherit;
  z-index: 0;
  font-weight: bolder;
}
.column-label span {
  position: relative;
  top: 100%;
  margin-bottom: auto;
}
.reset {
  padding: 0.5em;
  border: 1px solid black;
  border-radius: 5px;
  text-align: center;
  cursor: pointer;
  grid-column: 2/4;
  grid-row:3/4;
  justify-self: center;
  display: flex;
  justify-content: center;
  align-items: center;
}
.reset:hover {
  background: rgb(225, 225, 225);
}

@media (max-width: 650px) {
  #app {
    grid-template-columns: 5% 10% 80% 5%;
    grid-gap: 0;
  }
  form {
    width: 95%;
  }
  .box {
    width: 80%;
    justify-self: center;
  }
  .y-axis {
    width: 10%;
  }
  .column-label {
    width: 1.75em;
  }
  .column {
    width: 1.75em;
  }
  .reset {
    grid-column: 3/4;
    justify-self: center;
  }
}
const colors = [
  "#FFC313",
  "#FF9813",
  "#FB1223",
  "#E01093",
  "#6621D2",
  "#2B37D4",
  "#198DCC",
  "#10D66C",
  "#45EA11",
  "#BBF812",
];
let i = 0;
function getColor() {
  if (i > colors.length - 1) {
    i = 0;
  }
  let currentColor = colors[i];
  i++;
  return currentColor;
}

function Column(props) {
  const hover = (event) => {
    event.currentTarget.style.opacity = "0.5";
    props.labelRef.current.style.background = props.color;
    props.labelRef.current.style.visibility = "visible";
    props.labelRef.current.style.opacity = "1";
  };

  const hoverLeave = (event) => {
    event.currentTarget.style.opacity = "1";
    props.labelRef.current.style.visibility = "hidden";
    props.labelRef.current.style.opacity = "0";
  };

  return (
    <div
      className="column"
      style={{ height: props.height + "%", background: props.color }}
      onMouseEnter={hover}
      onMouseLeave={hoverLeave}
      onTouchStart={hover}
      onTouchEnd={hoverLeave}
    ></div>
  );
}

function ColumnAndLabel(props) {
  return (
    <div className="column-label">
      <Column
        labelRef={props.labelRef}
        color={getColor()}
        height={100 * (props.frequency / props.maxValue)}
      />
      <div ref={props.labelRef} className="label">
        {/**frequency label visible on hover */}
        {props.frequency}
      </div>
      <span>{props.num}</span> {/**x-axis */}
    </div>
  );
}


function Box(props) {
  return (
    <div className="box">
      <div className="histogram">
        {Object.keys(props.data)
          .sort((a, b) => a - b)
          .map((num, i) => (
            <ColumnAndLabel
              key={"column-label" + num}
              num={num}
              frequency={props.data[num]}
              labelRef={props.labelsRef[i]}
              maxValue={props.maxValue}
            />
          ))}
      </div>
    </div>
  );
}

function Form(props) {
  return (
    <form>
      <h1>Data fetching and visualization</h1>
      <label htmlFor="amount">
        Amount of numbers
        <input
          type="text"
          name="amount"
          id="amount"
          defaultValue="200"
          onInput={props.onInput}
          className={
            props.formInput.amount &&
            !isNaN(props.formInput.amount - "") &&
            props.formInput.amount - "" <= 950
              ? null
              : "error"
          }
        />
      </label>

      <label htmlFor="min">
        Min number
        <input
          type="text"
          name="min"
          id="min-num"
          defaultValue="1"
          onInput={props.onInput}
          className={
            props.formInput.min && !isNaN(props.formInput.min - "")
              ? null
              : "error"
          }
        />
      </label>

      <label htmlFor="max">
        Max number
        <input
          type="text"
          name="max"
          id="max-num"
          defaultValue="10"
          onInput={props.onInput}
          className={
            props.formInput.max &&
            !isNaN(props.formInput.max - "") &&
            props.formInput.max !== props.formInput.min
              ? null
              : "error"
          }
        />
      </label>

      <input
        type="submit"
        value="Submit"
        id="submit"
        onClick={props.onSubmit}
        disabled={
          !props.formInput.amount.length ||
          !props.formInput.min.length ||
          !props.formInput.max.length ||
          isNaN(props.formInput.max - "") ||
          isNaN(props.formInput.min - "") ||
          isNaN(props.formInput.amount - "") ||
          props.formInput.max === props.formInput.min ||
          props.formInput.amount - "" > 950
        }
      />
    </form>
  );
}

class App extends React.Component {
  state = {
    data: {},
    labelsRef: [],
    formInput: {
      min: "1",
      max: "10",
      amount: "200",
    },
  };

  getData = (e) => {
    e.preventDefault();

    const amount = this.state.formInput.amount;
    const min = this.state.formInput.min;
    const max = this.state.formInput.max;
    const url = `https://www.random.org/integers/?num=${amount}&min=${min}&max=${max}&col=1&base=10&format=plain&rnd=new`;

    fetch(url)
      .then((response) => {
        if (!response.ok) {
          throw new Error(`HTTP error: ${response.status}`);
        }
        return response.text();
      })
      .then((result) => {
        let data = this.state.data;
        let labelsRef = this.state.labelsRef;
        data = {};
        labelsRef = [];

        result
          .split("\n")
          .filter((num) => num !== "")
          .forEach((num) => {
            if (num in data) {
              data[num]++;
            } else {
              data[num] = 1;
            }
          });

        for (let i = 0; i < Object.keys(data).length; i++) {
          labelsRef.push(React.createRef());
        }

        this.setState({ data, labelsRef });
      })
      .catch((error) => console.log("Error: " + error));
  };

  formInputs = (event) => {
    const value = event.currentTarget.value;
    let formInput = this.state.formInput;

    if (event.currentTarget.id === "amount") {
      formInput.amount = value;
    } else if (event.currentTarget.id === "min-num") {
      formInput.min = value;
    } else {
      formInput.max = value;
    }
    this.setState({ formInput });
  };
  reset = () => {
    this.setState({
      data: {},
      formInput: {
        min: "1",
        max: "10",
        amount: "200",
      },
    });
  };

  render() {
    if (Object.keys(this.state.data).length) {
      let maxValue = Math.max(...Object.values(this.state.data));
      let yAxis = [];
      maxValue = Number.isInteger(maxValue / 10)
        ? maxValue
        : Math.trunc((maxValue + 10) / 10) * 10;

      if (maxValue > 10) {
        for (let i = 10; i < maxValue; i += 10) {
          yAxis.push(<span key={i}>{i}</span>);
        }
      } else {
        yAxis.push(<span key="5">5</span>);
      }

      return [
        <div key="y-axis" className="y-axis">
          {yAxis}
        </div>,
        <Box
          key="box-histogram"
          data={this.state.data}
          labelsRef={this.state.labelsRef}
          maxValue={maxValue}
        />,
        <div key="reset" className="reset" onClick={this.reset}>
          <img src="https://img.icons8.com/ios-glyphs/30/null/reboot.png" />
        </div>,
      ];
    } else {
      return (
        <Form
          formInput={this.state.formInput}
          onInput={this.formInputs}
          onSubmit={this.getData}
        />
      );
    }
  }
}

ReactDOM.render(<App />, document.getElementById("app"));
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js