whylabs/whylogs-python

View on GitHub
python/whylogs/viz/html/templates/index-hbs-cdn-all-in-jupyter-distribution-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;
     }

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

     .chart-box-title {
       width: 88%;
       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;
     }

    .bar {
      font: 10px sans-serif;
    }

    .bar path,
    .bar line {
      fill: none;
      stroke: #000;
      shape-rendering: crispEdges;
    }

    .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() {
      //helper fun

      class GenerateChartParams {
        constructor(height, width, targetData, 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()+100} ${$(window).height()-30}`)
             .classed("svg-content-responsive", true)
          this.maxYValue = d3.max(targetData, (d) => Math.abs(d.axisY));
          this.minYValue = d3.min(targetData, (d) => Math.abs(d.axisY));
          const mergedReferenceData = referenceData.map(({axisX, axisY}) => {
            return {axisX, axisY}
          })
          const mergedTargetedData = targetData.map(({axisX, axisY}) => {
            return {axisX, axisY}
          })

          this.charts2 = mergedReferenceData.concat(mergedTargetedData)
          this.charts2 = this.charts2.sort(function(a, b) { return a - b; });

          this.targetBinWidth = targetData[1]?.axisX - targetData[0]?.axisX
          this.referenceBinWidth = referenceData[1]?.axisX - referenceData[0]?.axisX
          this.maxTargetXValue = d3.max(targetData, (d) => d.axisX);

          this.maxReferenceXValue = d3.max(referenceData, (d) => d.axisX);

          this.xScale = d3
              .scaleLinear()
         .domain([d3.min(this.charts2, function(d) { return parseFloat(d.axisX); }),(this.maxTargetXValue+this.targetBinWidth >= this.maxReferenceXValue+this.referenceBinWidth) ? this.maxTargetXValue+this.targetBinWidth:this.maxReferenceXValue+this.referenceBinWidth])
         .range([0, this.CHART_WIDTH ]);

         this.svgEl.append("g")
             .attr("transform", "translate("+ this.MARGIN.LEFT +"," + this.SVG_HEIGHT + ")")
             .call(d3.axisBottom(this.xScale));
          this.yScale = d3.scaleLinear()
              .range([this.CHART_HEIGHT , 0])
              .domain([d3.min(this.charts2, function(d) { return parseFloat(d.axisY); }), d3.max(this.charts2, function(d) { return parseFloat(d.axisY); })*1.2])
              .nice();
        }
      }

      function chartData(column) {
        const data = [];
        if (column?.histogram) {
          for (let i = 0; i<column.histogram.bins.length; i++) {
            data.push({
              axisY: column.histogram.counts[i] || 0,
              axisX: column.histogram.bins[i] || 0,
            });
          }
        } 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 verticalLine(column) {
        const line_data = [];
        if (column?.vertical_line) {
            line_data.push({
              axisX: column?.vertical_line || 0,
            });

        }

        return line_data
      }

      function generateDoubleHistogramChart(targetData, referenceData) {
        let histogramData = [],
            overlappedHistogramData = [];
        let yFormat;

        histogramData = chartData(targetData)
        overlappedHistogramData = chartData(referenceData)
        lineData = verticalLine(targetData)

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

        const rectColors = ["#44C0E7", "#F5843C"]
        const yAxis = d3.axisLeft(yScale).ticks(SVG_HEIGHT / 40);

        svgEl.append("g")
          .attr("transform", `translate(${MARGIN.LEFT}, ${MARGIN.BOTTOM})`)
          .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("line")
          .attr("transform", `translate(${MARGIN.LEFT}, ${MARGIN.BOTTOM})`)
          .data(lineData)
          .attr("x1", (d) => xScale(d.axisX))
          .attr("y1", MARGIN.TOP + MARGIN.TOP)
          .attr("x2", (d) => xScale(d.axisX))
          .attr("y2", CHART_HEIGHT + MARGIN.TOP)
          .style("stroke-width", 2)
          .style("stroke", "#44C0E7")
          .style("stroke-dasharray", (3,3))
          .style("fill", "none");

        svgEl.append("text")
          .attr("transform", "rotate(-90)")
          .data(lineData)
          .attr("y", (d) => xScale(d.axisX) + MARGIN.LEFT)
          .attr("x", 0 - (SVG_HEIGHT / 2))
          .attr("dy", "1em")
          .style("text-anchor", "middle")
          .text("single value")
          .style("font-size", "15")
          .style("opacity", "0.6")

        svgEl.append("text")
          .attr("transform",
                "translate(" + (CHART_WIDTH/2) + " ," +
                               (CHART_HEIGHT + MARGIN.TOP + 75) + ")")
          .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")

          const gChart = svgEl.append("g");
          gChart
          .attr("transform", "translate("+ MARGIN.LEFT +",0)")
          .selectAll(".bar")
          .data(histogramData)
          .enter()
          .append("rect")
          .style("stroke", "#021826")
          .classed("bar", true)
          .attr("width", function(d) { return xScale(histogramData[1]?.axisX)-xScale(histogramData[0]?.axisX); })
          .attr("height", (d) => CHART_HEIGHT - yScale(d.axisY))
          .attr("x", 1)
          .attr("transform", function(d) { return "translate(" + xScale(d.axisX) + "," + 0  +  ")"; })
          .attr("y", (d) => yScale(d.axisY) + MARGIN.TOP + MARGIN.BOTTOM)
          .attr("fill", rectColors[0])
          .style("opacity","0.6")

          const gChart1 = svgEl.append("g");
          gChart1
          .attr("transform", "translate("+ MARGIN.LEFT +",0)")
          .selectAll(".bar")
          .data(overlappedHistogramData)
          .enter()
          .append("rect")
          .style("stroke", "#021826")
          .classed("bar", true)
          .attr("width", function(d) { return xScale(overlappedHistogramData[1]?.axisX)-xScale(overlappedHistogramData[0]?.axisX); })
          .attr("height", (d) => CHART_HEIGHT - yScale(d.axisY))
          .attr("x", 1)
          .attr("transform", function(d) { return "translate(" + xScale(d.axisX) + "," + 0  +  ")"; })
          .attr("y", (d) => yScale(d.axisY) + MARGIN.TOP + MARGIN.BOTTOM)
          .attr("fill", rectColors[1])
          .style("opacity","0.6")

        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  generateDoubleHistogramChart (
              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>