initParallelPlot()

in src/parallel/parallel.tsx [296:643]


  initParallelPlot(this: ParallelPlot) {
    const me = this;

    var div = this.div = d3.select(me.root_ref.current);
    var svg = d3.select(me.svg_ref.current);

    // Foreground canvas for primary view
    me.foreground = this.foreground_ref.current.getContext('2d');
    me.foreground.globalCompositeOperation = "destination-over";

    // Highlight canvas for temporary interactions
    me.highlighted = this.highlighted_ref.current.getContext('2d');

    // SVG for ticks, labels, and interactions

    // Load the data and visualization
    function redraw_axis() {
      // Extract the list of numerical dimensions and create a scale for each.
      me.xscale.domain(me.state.dimensions);

      // Add a group element for each dimension.
      function create_drag_beh() {
        return d3.drag().on("start", function(d: string) {
          me.setState({
            dragging: {
              col: d,
              pos: me.xscale(d),
              origin: me.xscale(d),
              dragging: false
            }
          });
          d3.select(me.foreground_ref.current).style("opacity", "0.35");
        })
        .on("drag", function(d: string) {
          const eventdx = d3.event.dx;
          const brushEl = d3.select(this).select("." + style.brush);
          me.setState(function(prevState, _) { return {
            dragging: {
              col: d,
              pos: Math.min(me.w, Math.max(0, prevState.dragging.pos + eventdx)),
              origin: prevState.dragging.origin,
              dragging: true
            }
          };}, function() {
            // Feedback for axis deletion if dropped
            if (me.state.dragging.pos < 12 || me.state.dragging.pos > me.w-12) {
              brushEl.style('fill', 'red');
            } else {
              brushEl.style('fill', null);
            }
          });

          var new_dimensions = Array.from(me.state.dimensions);
          new_dimensions.sort(function(a, b) { return me.position(a) - me.position(b); });
          if (!new_dimensions.every(function(val, idx) { return val == me.state.dimensions[idx]; })) {
            me.setState({dimensions: new_dimensions});
          }
          me.dimensions_dom.attr("transform", function(d) { return "translate(" + me.position(d) + ")"; });
          redrawAllForeignObjectsIfSafari();
        })
        .on("end", function(d: string) {
          if (!me.state.dragging.dragging) {
            // no movement, invert axis
            var extent = invert_axis(d);
          } else {
            // reorder axes
            var drag: any = d3.select(this);
            if (!IS_SAFARI) {
              drag = drag.transition();
            }
            drag.attr("transform", "translate(" + me.xscale(d) + ")");
            var extents = brush_extends();
            extent = extents[d];
          }

          // remove axis if dragged all the way left
          if (me.state.dragging.pos < 12 || me.state.dragging.pos > me.w-12) {
            me.remove_axis(d);
          } else {
            me.setState({order: Array.from(me.state.dimensions)});
          }

          me.update_ticks(d, extent);

          // rerender
          d3.select(me.foreground_ref.current).style("opacity", null);
          me.setState({dragging: null});
        });
      }
      if (me.dimensions_dom) {
        me.dimensions_dom.remove();
      }
      me.dimensions_dom = d3.select(me.svgg_ref.current).selectAll<SVGGElement, string>(".dimension")
          // reverse the order so that the tooltips appear on top of the axis ticks
          .data(me.state.dimensions.reverse())
        .enter().append<SVGGElement>("svg:g")
          .attr("class", "dimension")
          .attr("transform", function(d) { return "translate(" + me.xscale(d) + ")"; })
          //@ts-ignore
          .call(create_drag_beh());

      // Add an axis and title.
      me.dimensions_dom.append("svg:g")
          .attr("class", style.axis)
          .attr("transform", "translate(0,0)")
          .each(function(d) { console.assert(me.yscale[d], d, me.yscale, this); d3.select(this).call(me.axis.scale(me.yscale[d])); })
        .append(function(dim) { return foCreateAxisLabel(me.props.params_def[dim], me.props.context_menu_ref, "Drag to move, right click for options"); })
          .attr("y", -20)
          .attr("text-anchor", "left")
          .classed("pplot-label", true)
          .classed(style.pplotLabel, true);
      me.dimensions_dom.selectAll(".label-name").style("font-size", "20px");
      me.dimensions_dom.selectAll(".pplot-label").each(function(this: SVGForeignObjectElement, d: string) {
        foDynamicSizeFitContent(this, [-me.xscale(d) + 5, -me.xscale(d) + me.state.width - 5]);
      }).attr("x", 0).style("width", "1px");
      me.updateAxisTitlesAnglesAndFontSize();
      // Add and store a brush for each axis.
      me.dimensions_dom.append("svg:g")
          .classed(style.brush, true)
          .classed("pplot-brush", true)
          .each(function(d) { d3.select(this).call(me.d3brush); })
        .selectAll("rect")
          .style("visibility", null)
          .append("title")
            .text("Drag up or down to brush along this axis");

      me.dimensions_dom.selectAll(".extent")
          .append("title")
            .text("Drag or resize this filter");
    };

    function invert_axis(d: string) {
      // save extent before inverting
      var extents = brush_extends();
      var extent = extents[d] !== null ? [me.h - extents[d][1], me.h - extents[d][0]] : null;

      if (me.state.invert.has(d)) {
        me.setState(function(prevState, props) {
          var newInvert = new Set(prevState.invert);
          newInvert.delete(d);
          return {
            invert: newInvert
          };
        });
        me.setScaleRange(d);
        div.selectAll("." + style.label)
          .filter(function(p) { return p == d; })
          .style("text-decoration", null);
      } else {
        me.setState(function(prevState, props) {
          var newInvert = new Set(prevState.invert);
          newInvert.add(d);
          return {
            invert: newInvert
          };
        });
        me.setScaleRange(d);
        div.selectAll("." + style.label)
          .filter(function(p) { return p == d; })
          .style("text-decoration", "underline");
      }
      return extent;
    }

    function brush_extends() {
      var extents = {};
      me.dimensions_dom.selectAll("." + style.brush).each(function(this: SVGGElement, dim: string) {
        extents[dim] = d3.brushSelection(this);
      });
      return extents;
    }

    function brush() {
      /**
       * Called whenever a brush happens. Recomputes which points are selected.
       */
      if (me.props.context_menu_ref !== undefined) {
        me.props.context_menu_ref.current.hide();
      }
      var extents = brush_extends();
      var actives = me.state.dimensions.filter(function(p) { return extents[p] !== null && extents[p] !== undefined; });

      // hack to hide ticks beyond extent
      me.dimensions_dom
        .each(function(dimension) {
          if (_.include(actives, dimension)) {
            var scale = me.yscale[dimension];
            var extent = extents[dimension];
            d3.select(this)
              .selectAll('text')
              .classed(style.tickSelected, true)
              .style('display', function() {
                if (d3.select(this).classed(style.label)) {
                  return null;
                }
                var value = d3.select(this).data();
                return extent[0] <= scale(value) && scale(value) <= extent[1] ? null : "none";
              });
          } else {
            d3.select(this)
              .selectAll('text')
              .classed(style.tickSelected, false)
              .style('display', null);
          }
          d3.select(this)
            .selectAll("." + style.label)
            .style('display', null);
        });
        ;

      // bold dimensions with label
      div.selectAll("." + style.label)
        .style("font-weight", function(dimension) {
          if (_.include(actives, dimension)) return "bold";
          return null;
        });

      // Get lines within extents
      var filters: Array<Filter> = actives.map(function(dimension) {
        const scale = me.yscale[dimension];
        var extent = extents[dimension];
        const range = scale_pixels_range(scale, extent);
        if (range.type == ParamType.CATEGORICAL && !range.values) {
          // Select nothing
          return {
            type: FilterType.Not,
            data: {
              type: FilterType.All,
              data: [],
            }
          };
        }
        var min, max;
        if (range.type == ParamType.CATEGORICAL) {
          if (range.values.length == 0) {
            return {
              type: FilterType.None,
              data: {}
            };
          }
          min = range.values[0];
          max = range.values[range.values.length - 1];
          console.assert(typeof min == typeof max, min, max);
        }
        else {
          min = Math.min(...range.range);
          max = Math.max(...range.range);
        }
        return {
          type: FilterType.Range,
          data: {
            col: dimension,
            type: range.type,
            min: min,
            max: max,
            include_infnans: range.include_infnans,
          }
        };
      });
      const selected = apply_filters(me.props.rows_filtered, filters);

      if (me.props.asserts) {
          // Check that pixel-based selected rows
          // match filters-based selected rows
          // But relax the verification a bit - math errors can happen
          // and we only require a 1 pixel precision
          var selected_pixels_minset = [];
          var selected_pixels_maxset = [];
          me.props.rows_filtered
            .forEach(function(d) {
              if (actives.every(function(dimension) {
                var scale = me.yscale[dimension];
                var extent = extents[dimension];
                var value = d[dimension];
                return extent[0] + 1 <= scale(value) && scale(value) <= extent[1] - 1;
              })) {
                selected_pixels_minset.push(d);
              }
              if (actives.every(function(dimension) {
                var scale = me.yscale[dimension];
                var extent = extents[dimension];
                var value = d[dimension];
                return extent[0] - 1 <= scale(value) && scale(value) <= extent[1] + 1;
              })) {
                selected_pixels_maxset.push(d);
              }
            });
          const missed = _.difference(selected_pixels_minset, selected);
          const overselected = _.difference(selected, selected_pixels_maxset);
          if (overselected.length || missed.length) {
              console.error(`Warning! Filter on ${actives.join(" ")} (`, filters, ") does not match actually selected rows",
                " Computed rows with filter:", selected,
                " Missed:", missed, " Falsely selected:", overselected);
              console.error("filters", filters, JSON.stringify(filters));
              if (missed.length) {
                console.error("first missed", JSON.stringify(missed[0]));
              }
              if (overselected.length) {
                console.error("first falsely selected", JSON.stringify(overselected[0]));
              }
          }
      }
      me.props.setSelected(selected, {
        type: FilterType.All,
        data: filters,
      });
    }

    // scale to window size
    this.on_resize = _.debounce(function() {
      me.compute_dimensions();

      div.selectAll(".dimension")
        .attr("transform", function(d: string) {
          return "translate(" + me.xscale(d) + ")";
      })
      // update brush placement
      svg.selectAll("." + style.brush)
        .each(function(d) { d3.select(this).call(me.d3brush); })

      // update axis placement
      me.axis = me.axis.ticks(1+me.state.height/50);
      div.selectAll("." + style.axis)
        .each(function(d: string) {
          d3.select(this).call(me.axis.scale(me.yscale[d]));
      });
      me.updateAxisTitlesAnglesAndFontSize();

      // render data
      this.setState(function(prevState) { return { brush_count: prevState.brush_count + 1}; });
      this.props.sendMessage("height_changed", () => null);
    }, 100);

    me.compute_dimensions();
    redraw_axis();

    // Render full foreground
    brush();
    me.sendBrushExtents();

    // Trigger initial brush
    me.setState(function(prevState) { return { brush_count: prevState.brush_count + 1}; });

    me.pplot = {
      'redraw_axis': redraw_axis,
      'brush': brush,
    };
  }