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