whylabs/whylogs-python

View on GitHub
python/whylogs/viz/html/templates/index-hbs-cdn-all-in-jupyter-bar-chart.html

Summary

Maintainability
Test Coverage
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="" />
    <meta name="author" content="" />

    <title>Profile Visualizer | whylogs</title>

    <link rel="icon" href="images/whylabs-favicon.png" type="image/png" sizes="16x16" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link href="https://fonts.googleapis.com/css2?family=Asap:wght@400;500;600;700&display=swap" rel="stylesheet" />
    <link rel="preconnect" href="https://fonts.gstatic.com" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" />

    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.7/handlebars.min.js"
      integrity="sha512-RNLkV3d+aLtfcpEyFG8jRbnWHxUqVZozacROI4J2F1sTaDqo1dPQYs01OMi1t1w9Y2FdbSCDSQ2ZVdAC8bzgAg=="
      crossorigin="anonymous"
      referrerpolicy="no-referrer"
    ></script>

    <style type="text/css">

      :root {
        /** Branded colors */
        --brandSecondary900: #4f595b;
        --secondaryLight1000: #313b3d;
        /** Purpose colors */
        --tealBackground: #eaf2f3;
      }

      /* RESET STYLE */
      *,
      *::after,
      *::before {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
        overflow-y: hidden;
      }

      /* Screen on smaller screens */
      .no-responsive {
        display: none;
        position: fixed;
        top: 0;
        left: 0;
        z-index: 1031;
        width: 100vw;
        height: 100vh;
        background-color: var(--tealBackground);
        display: flex;
        align-items: center;
        justify-content: center;
      }

      .desktop-content {
        display: flex;
        flex-direction: column;
      }

      .no-responsive__content {
        max-width: 600px;
        width: 100%;
        padding: 0 24px;
      }

      .no-responsive__title {
        font-size: 96px;
        font-weight: 300;
        color: var(--brandSecondary900);
        line-height: 1.167;
      }

      .no-responsive__text {
        margin: 0;
        font-size: 16px;
        font-weight: 400;
        color: var(--brandSecondary900);
        line-height: 1.5;
      }

      .svg-container {
        display: inline-block;
        position: relative;
        width: 100%;
        vertical-align: top;
        overflow: hidden;
      }

      .svg-content-responsive {
        display: inline-block;
        position: absolute;
        left: 0;
      }

      .circle-color {
       display: inline-block;
       padding: 5px;
       border-radius: 50px;
     }

     .colors-for-distingushing-charts {
       padding-right: 10px;
     }

     .display-flex{
       display: flex;
     }

     .flex-direction-column {
       flex-direction: column;
     }

     .align-items-flex-end {
       align-items: flex-end;
     }

     .chart-box-title {
       width: 98%;
       justify-content: space-between;
       margin: 10px;
       margin-top: 15px;
       bottom: 0;
     }

     .chart-box-title p{
       margin-bottom: 0;
       font-family: Asap;
       font-weight: bold;
       font-size: 18px;
       line-height: 16px;
       color: #4F595B;
     }

     .error-message {
       display: flex;
       justify-content: center;
       align-items: center;
       color: rgb(255, 114, 71);
       font-size: 30px;
       font-weight: 900;
     }

     @media screen and (min-width: 500px) {
       .desktop-content {
         display: block;
       }
       .no-responsive {
         display: none;
       }
     }
    </style>
  </head>

  <body id="generated-html"></body>
  <script id="entry-template" type="text/x-handlebars-template">
    {{{{raw}}}}
      <div class="desktop-content">
        {{#each this}}
              <div class="chart-box" id="chart-box">
                <div class="chart-box-title display-flex">
                  <p>{{@key}}</p>
                  <div class="display-flex">
                    <div class="colors-for-distingushing-charts">
                      <div class="circle-color" style="background: #44C0E7;"></div>
                      <text alignment-baseline="middle" style="font-size: 15px;">Target</text>
                    </div>
                    <div class="colors-for-distingushing-charts">
                      <div class="circle-color" style="background: #F5843C"></div>
                      <text alignment-baseline="middle" style="font-size: 15px;">Reference</text>
                    </div>
                  </div></div>
                <div class="svg-container">
                  {{{getDoubleHistogramChart this}}}
                </div>
              </div>
        {{/each}}
      </div>
      <div class="no-responsive">
        <div class="no-responsive__content">
          <h1 class="no-responsive__title">Hold on! :)</h1>
          <p class="no-responsive__text">
            It looks like your current screen size or device is not yet supported by the WhyLabs Sandbox. The Sandbox is
            best experienced on a desktop computer. Please try maximizing this window or switching to another device. We
            are working on adding support for a larger variety of devices.
          </p>
        </div>
      </div>
    {{{{/raw}}}}
  </script>

  <script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js" integrity="sha512-cd6CHE+XWDQ33ElJqsi0MdNte3S+bQY819f7p3NUHgwQQLXSKjE4cPZTeGNI+vaxZynk1wVU3hoHmow3m089wA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

  <script>
   function registerHandlebarHelperFunctions() {

    const findAndDeleteUndefined = (axisData) => {
      const undefinedAxisIndex = axisData.findIndex((axis) => axis === undefined)
      if (undefinedAxisIndex == -1) {
        return axisData;
      }

      const result = [...axisData.slice(0, undefinedAxisIndex), ...axisData.slice(undefinedAxisIndex + 1)]
      return result
    }

    const filterAndSortChartData = (overlappedHistogramData, histogramData) => {
        const filteredData = overlappedHistogramData
          .map((d) =>
            histogramData
            .filter((ref) => d.axisX === ref.axisX )[0])
            .sort((a, b) => {
              if (+a.axisY < +b.axisY) {
                return 1;
              }
              if (+a.axisY > +b.axisY) {
                return -1;
              }

              return 0;
            })
            .slice(0, 20)

        return findAndDeleteUndefined(filteredData)
      }

      class GenerateChartParams {
        constructor(height, width, data, referenceData, bottomMargin=30, topMargin=5) {
          this.MARGIN = {
            TOP: topMargin,
            RIGHT: 5,
            BOTTOM: bottomMargin,
            LEFT: 55,
          };
          this.SVG_WIDTH = width;
          this.SVG_HEIGHT = height;
          this.CHART_WIDTH = this.SVG_WIDTH - this.MARGIN.LEFT - this.MARGIN.RIGHT;
          this.CHART_HEIGHT = this.SVG_HEIGHT - this.MARGIN.TOP - this.MARGIN.BOTTOM;
          this.svgEl = d3.create("svg")
             .attr("preserveAspectRatio", "xMinYMin meet")
             .attr("viewBox", `0 0 ${$(window).width()} ${$(window).height()-30}`)
             .classed("svg-content-responsive", true)
          this.maxYValue = d3.max([...data, ...referenceData], (d) => Math.abs(d.axisY));
          this.xScale = d3
            .scaleBand()
            .domain(filterAndSortChartData(data, referenceData).map((sortedCounts) => sortedCounts?.axisX))
            .range([this.MARGIN.LEFT, this.MARGIN.LEFT + this.CHART_WIDTH]);
          this.yScale = d3
            .scaleLinear()
            .domain([0, this.maxYValue * 1.2])
            .range([this.CHART_HEIGHT, 0]);
        }
      }

      function chartData(column) {
        const data = [];
          if (column.frequentItems) {
            Object.entries(column.frequentItems).forEach(([key, {value, estimate}], index) => {
              data.push({
                axisY: estimate,
                axisX: value,
              });
            });
          } else {
            $(document).ready(() =>
              $(".desktop-content").html(`
                <p style="height: ${$(window).height()}px" class="error-message">
                  Something went wrong. Please try again.
                </p>
              `)
            )
          }

        return data
      }

      function generateBarChart(histogramData, overlappedHistogramData) {
        let yFormat,
            xFormat;
        const data = filterAndSortChartData(chartData(histogramData), chartData(overlappedHistogramData)).map((axis, index) => {
          if (axis) {
            const findIndex = chartData(histogramData).findIndex((value) => value.axisX === axis.axisX)
            return {
              group: axis.axisX,
              profile: axis.axisY,
              reference_profile: chartData(histogramData)[findIndex].axisY
            }
          }
          return 0;
        })

        const sizes = new GenerateChartParams($(window).height()-60, $(window).width(), chartData(histogramData), chartData(overlappedHistogramData))
        let {
          MARGIN,
          SVG_WIDTH,
          SVG_HEIGHT,
          CHART_WIDTH,
          CHART_HEIGHT,
          svgEl,
          xScale,
          yScale
        } = sizes

        const rectColors = ["#44C0E7", "#F5843C"]
        const subgroups = ['reference_profile', 'profile']

        xScale.padding([0.3])

        const xAxis = d3.axisBottom(xScale).ticks(SVG_WIDTH / 80, xFormat).tickSizeOuter(0);
        const yAxis = d3.axisLeft(yScale).ticks(SVG_HEIGHT / 40, yFormat);
        yFormat = yScale.tickFormat(100, yFormat);

        svgEl.append("g")
          .attr("transform", `translate(${MARGIN.LEFT}, 0)`)
          .attr("id", "g1")
          .call(yAxis)
          .call(g => g.select(".domain").remove())
          .call(g => g.selectAll(".tick line")
              .attr("x2", CHART_WIDTH)
              .attr("stroke-opacity", 0.1))
          .call(g => g.append("text")
              .attr("x", -MARGIN.LEFT)
              .attr("y", 10)
              .attr("fill", "currentColor")
              .attr("text-anchor", "start"));

        svgEl.append("text")
          .attr("transform",
                "translate(" + (CHART_WIDTH/2) + " ," +
                               (CHART_HEIGHT + MARGIN.TOP + 40) + ")")
          .style("text-anchor", "middle")
          .text("Values")
          .style("font-size", "15")
          .style("opacity", "0.6")

        svgEl.append("text")
          .attr("transform", "rotate(-90)")
          .attr("y", 0)
          .attr("x", 0 - (SVG_HEIGHT / 2))
          .attr("dy", "1em")
          .style("text-anchor", "middle")
          .text("Counts")
          .style("font-size", "15")
          .style("opacity", "0.6")

        svgEl.append("g")
            .attr("transform", `translate(0,${SVG_HEIGHT - MARGIN.BOTTOM})`)
            .attr("id", "g2")
            .call(xAxis)
            .call(g => g.select(".domain").remove())
            .call(g => g.selectAll(".tick line").remove())
            .call(g => g.append("text")
                .attr("x", SVG_WIDTH - MARGIN.RIGHT)
                .attr("y", 27)
                .attr("fill", "currentColor")
                .attr("text-anchor", "end"));

        const xSubgroup = d3.scaleBand()
          .domain(subgroups)
          .range([0, xScale.bandwidth()])

        const color = d3.scaleOrdinal()
          .domain(subgroups)
          .range(rectColors)

        svgEl.append("g")
           .attr("id", "g3")
           .selectAll("g")
           .data(data)
           .enter()
           .append("g")
             .attr("transform", function(d) { return "translate(" + xScale(d?.group) + ",0)"; })
             .attr("id", "g4")
           .selectAll("rect")
           .data(function(d) { return subgroups.map(function(key) { return {key: key, value: d && d[key]}; }); })
           .enter().append("rect")
             .attr("x", function(d) { return xSubgroup(d.key); })
             .attr("y", function(d) { return yScale(d.value); })
             .attr("width", xSubgroup.bandwidth())
             .attr("height", function(d) { return (CHART_HEIGHT - yScale(d.value)); })
             .attr("fill", function(d) { return color(d.key); })
             .style("opacity", "0.8");


         return svgEl._groups[0][0].outerHTML;
      }


      const profileFromCSVfile = {{{reference_profile_from_whylogs}}}

      Handlebars.registerHelper("getDoubleHistogramChart",(column,key) => {
        const columnKey = key.data.key
        try {
          if (profileFromCSVfile) {
            return generateBarChart(
                     column,
                     profileFromCSVfile[columnKey]
                   )
          }
        } catch (err) {
          $(document).ready(() =>
            $(".desktop-content").html(`
              <p style="height: ${$(window).height()}px" class="error-message">
                Something went wrong. Please try again.
              </p>
            `)
          )
        }
      });
    }

    function initWebsiteScripts() {
      $(".svg-container").css("height", $(window).height() - 32)
    }

    function initHandlebarsTemplate() {
      // Replace this context with JSON from .py file
      const context = {{{profile_from_whylogs}}};
      // Config handlebars and pass data to HBS template
      const source = document.getElementById("entry-template").innerHTML;
      const template = Handlebars.compile(source);
      const html = template(context);
      const target = document.getElementById("generated-html");
      target.innerHTML = html;
    }

    // Invoke functions -- keep in mind invokation order
    registerHandlebarHelperFunctions();
    initHandlebarsTemplate();
    initWebsiteScripts();
  </script>
</html>