whylabs/whylogs-python

View on GitHub
python/whylogs/viz/html/js/handlebars.js

Summary

Maintainability
A
0 mins
Test Coverage
function registerHandlebarHelperFunctions() {
  //helper fun
  function fixNumberTo(number, decimals = 3) {
    return parseFloat(number).toFixed(decimals);
  }

  Handlebars.registerHelper("json", function (context) {
    return JSON.stringify(context);
  });

  Handlebars.registerHelper("inferredType", function (column) {
    let infferedType = "";

    if (column.numberSummary) {
      if (column.numberSummary.isDiscrete) {
        infferedType = "Discrete";
      } else {
        infferedType = "Non-discrete";
      }
    } else {
      infferedType = "Unknown";
    }
    return infferedType;
  });

  Handlebars.registerHelper("frequentItems", function (column) {
    frequentItemsElemString = "";
    if (column.numberSummary && column.numberSummary.isDiscrete) {
      const slicedFrequentItems = column.frequentItems.items.slice(0, 5);
      for (let fi = 0; fi < slicedFrequentItems.length; fi++) {
        frequentItemsElemString +=
          '<span class="wl-table-cell__bedge">' + slicedFrequentItems[fi].jsonValue + "</span>";
      }
    } else {
      frequentItemsElemString += "No data to show";
    }
    return frequentItemsElemString;
  });

  Handlebars.registerHelper("totalCount", function (column) {
    if (column.numberSummary) {
      return column.numberSummary.count.toString();
    }

    return "-";
  });

  Handlebars.registerHelper("nullFraction", function (column) {
    nullRatio = column.schema.typeCounts.NULL ? column.schema.typeCounts.NULL : "0";

    return nullRatio;
  });

  Handlebars.registerHelper("estUniqValue", function (column) {
    estUniqueVal = column.uniqueCount ? fixNumberTo(column.uniqueCount.estimate) : "-";
    return estUniqueVal;
  });

  Handlebars.registerHelper("dataType", function (column) {
    return column.schema.inferredType.type;
  });

  Handlebars.registerHelper("dataTypeCount", function (column) {
    dataType = column.schema.inferredType.type;
    return column.schema.typeCounts[dataType];
  });

  Handlebars.registerHelper("mean", function (column) {
    if (column.numberSummary) {
      return fixNumberTo(column.numberSummary.mean);
    }
    return "-";
  });

  Handlebars.registerHelper("stddev", function (column) {
    if (column.numberSummary) {
      return fixNumberTo(column.numberSummary.stddev);
    }
    return "-";
  });

  Handlebars.registerHelper("min", function (column) {
    if (column.numberSummary && column.numberSummary.quantiles.quantileValues) {
      return fixNumberTo(column.numberSummary.quantiles.quantileValues[0]);
    }
    return "-";
  });

  Handlebars.registerHelper("firstQuantile", function (column) {
    if (column.numberSummary && column.numberSummary.quantiles.quantileValues) {
      return fixNumberTo(column.numberSummary.quantiles.quantileValues[3]);
    }
    return "-";
  });

  Handlebars.registerHelper("median", function (column) {
    if (column.numberSummary && column.numberSummary.quantiles.quantileValues) {
      return fixNumberTo(column.numberSummary.quantiles.quantileValues[4]);
    }
    return "-";
  });

  Handlebars.registerHelper("thirdQuantile", function (column) {
    if (column.numberSummary && column.numberSummary.quantiles.quantileValues) {
      return fixNumberTo(column.numberSummary.quantiles.quantileValues[5]);
    }
    return "-";
  });

  Handlebars.registerHelper("max", function (column) {
    if (column.numberSummary && column.numberSummary.quantiles.quantileValues) {
      return fixNumberTo(column.numberSummary.quantiles.quantileValues[8]);
    }
    return "-";
  });

  Handlebars.registerHelper("getTotalFeatureCount", function () {
    return Object.values(this.columns).length.toString() || "0";
  });

  Handlebars.registerHelper("getFeatureList", function () {
    let featureListHTML = "";

    Object.entries(this.columns).forEach((feature) => {
      let inferredType = "Unknown";
      if (feature[1].numberSummary) {
        if (feature[1].numberSummary.isDiscrete) {
          inferredType = "Discrete";
        } else {
          inferredType = "Non-discrete";
        }
      }

      featureListHTML += `<li class="list-group-item js-list-group-item" data-feature-name="${feature[0]}" data-inferred-type="${inferredType}"><span data-feature-name-id="${feature[0]}" >${feature[0]}</span></li>`;
    });

    return featureListHTML;
  });

  Handlebars.registerHelper("getDiscreteTypeCount", function () {
    let count = 0;

    Object.entries(this.columns).forEach((feature) => {
      if (feature[1].numberSummary && feature[1].numberSummary.isDiscrete === true) {
        count++;
      }
    });
    return count.toString();
  });

  Handlebars.registerHelper("getNonDiscreteTypeCount", function () {
    let count = 0;

    Object.entries(this.columns).forEach((feature) => {
      if (feature[1].numberSummary && feature[1].numberSummary.isDiscrete === false) {
        count++;
      }
    });
    return count;
  });

  Handlebars.registerHelper("getUnknownTypeCount", function () {
    let count = 0;

    Object.entries(this.columns).forEach((feature) => {
      if (!feature[1].numberSummary) {
        count++;
      }
    });
    return count;
  });

  Handlebars.registerHelper("getGraphHtml", function (column) {
    let data = [];
    if (column.numberSummary) {
      if (column.numberSummary.isDiscrete) {
        column.frequentItems.items.forEach((item, index) => {
          data.push({
            axisY: item.estimate,
            axisX: index,
          });
        });
      } else {
        column.numberSummary.histogram.counts.slice(0, 30).forEach((count, index) => {
          data.push({
            axisY: count,
            axisX: index,
          });
        });
      }
    } else {
      return '<span class="wl-table-cell__bedge-wrap">No data to show the chart</span>';
    }
    // Map data to use in chart

    const MARGIN = {
      TOP: 5,
      RIGHT: 5,
      BOTTOM: 5,
      LEFT: 55,
    };
    const SVG_WIDTH = 350;
    const SVG_HEIGHT = 75;
    const CHART_WIDTH = SVG_WIDTH - MARGIN.LEFT - MARGIN.RIGHT;
    const CHART_HEIGHT = SVG_HEIGHT - MARGIN.TOP - MARGIN.BOTTOM;
    const PRIMARY_COLOR_HEX = "#0e7384";

    const svgEl = d3.create("svg").attr("width", SVG_WIDTH).attr("height", SVG_HEIGHT);

    const maxYValue = d3.max(data, (d) => Math.abs(d.axisY));

    const xScale = d3
      .scaleBand()
      .domain(data.map((d) => d.axisX))
      .range([MARGIN.LEFT, MARGIN.LEFT + CHART_WIDTH]);
    const yScale = d3
      .scaleLinear()
      .domain([0, maxYValue * 1.02]) // so that chart's height has 10ยง2% height of the maximum value
      .range([CHART_HEIGHT, 0]);

    // Add the y Axis
    svgEl
      .append("g")
      .attr("transform", "translate(" + MARGIN.LEFT + ", " + MARGIN.TOP + ")")
      .call(d3.axisLeft(yScale).tickValues([0, maxYValue]));

    const gChart = svgEl.append("g");
    gChart
      .selectAll(".bar")
      .data(data)
      .enter()
      .append("rect")
      .classed("bar", true)
      .attr("width", xScale.bandwidth() - 1)
      .attr("height", (d) => CHART_HEIGHT - yScale(d.axisY))
      .attr("x", (d) => xScale(d.axisX))
      .attr("y", (d) => yScale(d.axisY) + MARGIN.TOP)
      .attr("fill", PRIMARY_COLOR_HEX);

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

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;
}

function initWebsiteScripts() {
  // Target HTML elements
  const $discrete = document.getElementById("inferredDiscrete");
  const $nonDiscrete = document.getElementById("inferredNonDiscrete");
  const $unknown = document.getElementById("inferredUnknown");
  const $sidebarFeatureNameList = document.getElementById("feature-list");
  const $featureSearch = document.getElementById("wl__feature-search");
  const $tableBody = document.getElementById("table-body");

  // Global variables
  const activeTypes = {
    discrete: true,
    "non-discrete": true,
    unknown: true,
  };
  let searchString = "";

  function handleSearch() {
    const featureListChildren = $sidebarFeatureNameList.children;
    const tableBodyChildren = $tableBody.children;

    for (let i = 0; i < tableBodyChildren.length; i++) {
      const type = tableBodyChildren[i].dataset.inferredType.toLowerCase();
      const name = tableBodyChildren[i].dataset.featureName.toLowerCase();

      if (activeTypes[type] && name.startsWith(searchString)) {
        tableBodyChildren[i].style.display = "";
      } else {
        tableBodyChildren[i].style.display = "none";
      }
    }

    for (let i = 0; i < featureListChildren.length; i++) {
      const name = featureListChildren[i].dataset.featureName.toLowerCase();
      const type = featureListChildren[i].dataset.inferredType.toLowerCase();

      if (activeTypes[type] && name.startsWith(searchString)) {
        featureListChildren[i].style.display = "";
      } else {
        featureListChildren[i].style.display = "none";
      }
    }
  }

  function debounce(func, wait, immediate) {
    let timeout;

    return function () {
      const context = this;
      const args = arguments;
      const later = function () {
        timeout = null;
        if (!immediate) func.apply(context, args);
      };

      const callNow = immediate && !timeout;
      clearTimeout(timeout);
      timeout = setTimeout(later, wait);
      if (callNow) func.apply(context, args);
    };
  }

  $featureSearch.addEventListener(
    "keyup",
    debounce((event) => {
      searchString = event.target.value.toLowerCase();
      handleSearch();
    }, 100),
  );

  $discrete.addEventListener("change", (event) => {
    if (event.currentTarget.checked) {
      activeTypes["discrete"] = true;
    } else {
      activeTypes["discrete"] = false;
    }
    handleSearch();
  });

  $nonDiscrete.addEventListener("change", (event) => {
    if (event.currentTarget.checked) {
      activeTypes["non-discrete"] = true;
    } else {
      activeTypes["non-discrete"] = false;
    }
    handleSearch();
  });

  $unknown.addEventListener("change", (event) => {
    if (event.currentTarget.checked) {
      activeTypes["unknown"] = true;
    } else {
      activeTypes["unknown"] = false;
    }
    handleSearch();
  });
}

function openPropertyPanel(column, infType) {
  const feature = JSON.parse(column);
  let propertyPanelData = [];

  if (feature.numberSummary) {
    if (feature.numberSummary.isDiscrete) {
      propertyPanelData = feature.frequentItems.items.reduce((acc, item) => {
        acc.push({
          value: item.jsonValue,
          count: item.estimate,
        });
        return acc;
      }, []);
    } else {
      propertyPanelData = feature.numberSummary.histogram.counts.reduce((acc, value, index) => {
        acc.push({
          value: value,
          count: feature.numberSummary.histogram.bins[index],
        });
        return acc;
      }, []);
    }
  }
  let chipString = "";
  const chipElement = (chip) => `<span class="wl-table-cell__bedge">${chip}</span>`;
  const chipElementTableData = (value) => `<td class="wl-property-panel__table-td" >${chipElement(value)}</td>`;
  const chipElementEstimation = (count) =>
    `<td class="wl-property-panel__table-td wl-property-panel__table-td-profile" >${count}</td>`;

  propertyPanelData.forEach((item) => {
    chipString += `
      <tr class="wl-property-panel__table-tr">
        ${chipElementTableData(item.value)}
        ${chipElementEstimation(item.count)}
      </tr>
      `;
  });
  $(".wl-property-panel__frequent-items").html(chipString);
  if (infType === "non-discrete") {
    $propertyPanelTitle.html("Histogram data:");
    $propertyPanelProfileName.html("Bin values");
  } else if (infType === "discrete") {
    $propertyPanelTitle.html("Frequent items:");
    $propertyPanelProfileName.html("Counts");
  }

  $(".wl-property-panel").addClass("wl-property-panel--open");
  $(".wl-table-wrap").addClass("wl-table-wrap--narrow");
}

function handleClosePropertyPanel() {
  $(".wl-property-panel").removeClass("wl-property-panel--open");
  $(".wl-table-wrap").removeClass("wl-table-wrap--narrow");
  $(".wl-property-panel__frequent-items").html("");
}

// Invoke functions -- keep in mind invokation order
registerHandlebarHelperFunctions();
initHandlebarsTemplate();
initWebsiteScripts();