Files
shap--shap/javascript/visualizers/AdditiveForceArrayVisualizer.jsx
2026-07-13 13:22:52 +08:00

951 lines
30 KiB
React

import React from "react";
import { select, pointer } from "d3-selection";
import { scaleLinear, scaleTime } from "d3-scale";
import { format } from "d3-format";
import { timeFormat, timeParse } from "d3-time-format";
import { axisBottom, axisLeft } from "d3-axis";
import { line } from "d3-shape";
import { hsl } from "d3-color";
import {
sortBy,
min,
max,
map,
each,
sum,
filter,
debounce,
keys,
range,
rangeRight,
cloneDeep,
findKey
} from "lodash";
import colors from "./color-set";
class AdditiveForceArrayVisualizer extends React.Component {
constructor() {
super();
window.lastAdditiveForceArrayVisualizer = this;
this.topOffset = 28;
this.leftOffset = 80;
this.height = 350;
this.effectFormat = format(".2");
this.redraw = debounce(() => this.draw(), 200);
}
componentDidMount() {
// create our permanent elements
this.mainGroup = this.svg.append("g");
this.onTopGroup = this.svg.append("g");
this.xaxisElement = this.onTopGroup
.append("g")
.attr("transform", "translate(0,35)")
.attr("class", "force-bar-array-xaxis");
this.yaxisElement = this.onTopGroup
.append("g")
.attr("transform", "translate(0,35)")
.attr("class", "force-bar-array-yaxis");
this.hoverGroup1 = this.svg.append("g");
this.hoverGroup2 = this.svg.append("g");
this.baseValueTitle = this.svg.append("text");
this.hoverLine = this.svg.append("line");
this.hoverxOutline = this.svg
.append("text")
.attr("text-anchor", "middle")
.attr("font-weight", "bold")
.attr("fill", "#fff")
.attr("stroke", "#fff")
.attr("stroke-width", "6")
.attr("font-size", "12px");
this.hoverx = this.svg
.append("text")
.attr("text-anchor", "middle")
.attr("font-weight", "bold")
.attr("fill", "#000")
.attr("font-size", "12px");
this.hoverxTitle = this.svg
.append("text")
.attr("text-anchor", "middle")
.attr("opacity", 0.6)
.attr("font-size", "12px");
this.hoveryOutline = this.svg
.append("text")
.attr("text-anchor", "end")
.attr("font-weight", "bold")
.attr("fill", "#fff")
.attr("stroke", "#fff")
.attr("stroke-width", "6")
.attr("font-size", "12px");
this.hovery = this.svg
.append("text")
.attr("text-anchor", "end")
.attr("font-weight", "bold")
.attr("fill", "#000")
.attr("font-size", "12px");
this.xlabel = this.wrapper.select(".additive-force-array-xlabel");
this.ylabel = this.wrapper.select(".additive-force-array-ylabel");
// Create our colors and color gradients
//Verify custom color map
let plot_colors=undefined;
if (typeof this.props.plot_cmap === "string")
{
if (!(this.props.plot_cmap in colors.colors))
{
console.log("Invalid color map name, reverting to default.");
plot_colors=colors.colors.RdBu;
}
else
{
plot_colors = colors.colors[this.props.plot_cmap]
}
}
else if (Array.isArray(this.props.plot_cmap)){
plot_colors = this.props.plot_cmap
}
this.colors = plot_colors.map(x => hsl(x));
this.brighterColors = [1.45, 1.6].map((v, i) => this.colors[i].brighter(v));
// create our axes
let defaultFormat = format(",.4");
if ((this.props.ordering_keys != null) && (this.props.ordering_keys_time_format != null)) {
this.parseTime = timeParse(this.props.ordering_keys_time_format);
this.formatTime = timeFormat(this.props.ordering_keys_time_format);
function condFormat(x) {
if (typeof(x) == "object") {
return this.formatTime(x);
} else {
return defaultFormat(x);
};
}
this.xtickFormat = condFormat;
} else {
this.parseTime = null;
this.formatTime = null;
this.xtickFormat = defaultFormat;
}
this.xscale = scaleLinear();
this.xaxis = axisBottom()
.scale(this.xscale)
.tickSizeInner(4)
.tickSizeOuter(0)
.tickFormat(d => this.xtickFormat(d))
.tickPadding(-18);
this.ytickFormat = defaultFormat;
this.yscale = scaleLinear();
this.yaxis = axisLeft()
.scale(this.yscale)
.tickSizeInner(4)
.tickSizeOuter(0)
.tickFormat(d => this.ytickFormat(this.invLinkFunction(d)))
.tickPadding(2);
this.xlabel.node().onchange = () => this.internalDraw();
this.ylabel.node().onchange = () => this.internalDraw();
this.svg.on("mousemove", event => this.mouseMoved(event));
this.svg.on("click", () => alert("This original index of the sample you clicked is " + this.nearestExpIndex));
this.svg.on("mouseout", x => this.mouseOut(x));
// draw and then listen for resize events
//this.draw();
window.addEventListener("resize", this.redraw);
window.setTimeout(this.redraw, 50); // re-draw after interface has updated
}
componentDidUpdate() {
this.draw();
}
mouseOut() {
this.hoverLine.attr("display", "none");
this.hoverx.attr("display", "none");
this.hoverxOutline.attr("display", "none");
this.hoverxTitle.attr("display", "none");
this.hovery.attr("display", "none");
this.hoveryOutline.attr("display", "none");
this.hoverGroup1.attr("display", "none");
this.hoverGroup2.attr("display", "none");
}
mouseMoved(event) {
let i, nearestExp;
this.hoverLine.attr("display", "");
this.hoverx.attr("display", "");
this.hoverxOutline.attr("display", "");
this.hoverxTitle.attr("display", "");
this.hovery.attr("display", "");
this.hoveryOutline.attr("display", "");
this.hoverGroup1.attr("display", "");
this.hoverGroup2.attr("display", "");
let x = pointer(event, this.svg.node())[0];
if (this.props.explanations) {
// Find the nearest explanation to the cursor position
for (i = 0; i < this.currExplanations.length; ++i) {
if (
!nearestExp ||
Math.abs(nearestExp.xmapScaled - x) >
Math.abs(this.currExplanations[i].xmapScaled - x)
) {
nearestExp = this.currExplanations[i];
}
}
this.nearestExpIndex = nearestExp.origInd;
this.hoverLine
.attr("x1", nearestExp.xmapScaled)
.attr("x2", nearestExp.xmapScaled)
.attr("y1", 0 + this.topOffset)
.attr("y2", this.height);
this.hoverx
.attr("x", nearestExp.xmapScaled)
.attr("y", this.topOffset - 5)
.text(this.xtickFormat(nearestExp.xmap));
this.hoverxOutline
.attr("x", nearestExp.xmapScaled)
.attr("y", this.topOffset - 5)
.text(this.xtickFormat(nearestExp.xmap));
this.hoverxTitle
.attr("x", nearestExp.xmapScaled)
.attr("y", this.topOffset - 18)
.text(
nearestExp.count > 1 ? nearestExp.count + " averaged samples" : ""
);
this.hovery
.attr("x", this.leftOffset - 6)
.attr("y", nearestExp.joinPointy)
.text(this.ytickFormat(this.invLinkFunction(nearestExp.joinPoint)));
this.hoveryOutline
.attr("x", this.leftOffset - 6)
.attr("y", nearestExp.joinPointy)
.text(this.ytickFormat(this.invLinkFunction(nearestExp.joinPoint)));
let posFeatures = [];
let lastPos, pos;
for (let j = this.currPosOrderedFeatures.length - 1; j >= 0; --j) {
let i = this.currPosOrderedFeatures[j];
let d = nearestExp.features[i];
pos = 5 + (d.posyTop + d.posyBottom) / 2;
if (
(!lastPos || pos - lastPos >= 15) &&
d.posyTop - d.posyBottom >= 6
) {
posFeatures.push(d);
lastPos = pos;
}
}
let negFeatures = [];
lastPos = undefined;
for (let i of this.currNegOrderedFeatures) {
let d = nearestExp.features[i];
pos = 5 + (d.negyTop + d.negyBottom) / 2;
if (
(!lastPos || lastPos - pos >= 15) &&
d.negyTop - d.negyBottom >= 6
) {
negFeatures.push(d);
lastPos = pos;
}
}
let labelFunc = d => {
let valString = "";
if (d.value !== null && d.value !== undefined) {
valString =
" = " + (isNaN(d.value) ? d.value : this.ytickFormat(d.value));
}
if (nearestExp.count > 1) {
return "mean(" + this.props.featureNames[d.ind] + ")" + valString;
} else {
return this.props.featureNames[d.ind] + valString;
}
};
let featureHoverLabels1 = this.hoverGroup1
.selectAll(".pos-values")
.data(posFeatures);
featureHoverLabels1
.enter()
.append("text")
.attr("class", "pos-values")
.merge(featureHoverLabels1)
.attr("x", nearestExp.xmapScaled + 5)
.attr("y", d => 4 + (d.posyTop + d.posyBottom) / 2)
.attr("text-anchor", "start")
.attr("font-size", 12)
.attr("stroke", "#fff")
.attr("fill", "#fff")
.attr("stroke-width", "4")
.attr("stroke-linejoin", "round")
.attr("opacity", 1)
.text(labelFunc);
featureHoverLabels1.exit().remove();
let featureHoverLabels2 = this.hoverGroup2
.selectAll(".pos-values")
.data(posFeatures);
featureHoverLabels2
.enter()
.append("text")
.attr("class", "pos-values")
.merge(featureHoverLabels2)
.attr("x", nearestExp.xmapScaled + 5)
.attr("y", d => 4 + (d.posyTop + d.posyBottom) / 2)
.attr("text-anchor", "start")
.attr("font-size", 12)
.attr("fill", this.colors[0])
.text(labelFunc);
featureHoverLabels2.exit().remove();
let featureHoverNegLabels1 = this.hoverGroup1
.selectAll(".neg-values")
.data(negFeatures);
featureHoverNegLabels1
.enter()
.append("text")
.attr("class", "neg-values")
.merge(featureHoverNegLabels1)
.attr("x", nearestExp.xmapScaled + 5)
.attr("y", d => 4 + (d.negyTop + d.negyBottom) / 2)
.attr("text-anchor", "start")
.attr("font-size", 12)
.attr("stroke", "#fff")
.attr("fill", "#fff")
.attr("stroke-width", "4")
.attr("stroke-linejoin", "round")
.attr("opacity", 1)
.text(labelFunc);
featureHoverNegLabels1.exit().remove();
let featureHoverNegLabels2 = this.hoverGroup2
.selectAll(".neg-values")
.data(negFeatures);
featureHoverNegLabels2
.enter()
.append("text")
.attr("class", "neg-values")
.merge(featureHoverNegLabels2)
.attr("x", nearestExp.xmapScaled + 5)
.attr("y", d => 4 + (d.negyTop + d.negyBottom) / 2)
.attr("text-anchor", "start")
.attr("font-size", 12)
.attr("fill", this.colors[1])
.text(labelFunc);
featureHoverNegLabels2.exit().remove();
}
}
draw() {
if (!this.props.explanations || this.props.explanations.length === 0)
return;
// record the order in which the explanations were given
each(this.props.explanations, (x, i) => (x.origInd = i));
// Find what features are actually used
let posDefinedFeatures = {};
let negDefinedFeatures = {};
let definedFeaturesValues = {};
for (let e of this.props.explanations) {
for (let k in e.features) {
if (posDefinedFeatures[k] === undefined) {
posDefinedFeatures[k] = 0;
negDefinedFeatures[k] = 0;
definedFeaturesValues[k] = 0;
}
if (e.features[k].effect > 0) {
posDefinedFeatures[k] += e.features[k].effect;
} else {
negDefinedFeatures[k] -= e.features[k].effect;
}
if (e.features[k].value !== null && e.features[k].value !== undefined) {
definedFeaturesValues[k] += 1;
}
}
}
this.usedFeatures = sortBy(
keys(posDefinedFeatures),
i => -(posDefinedFeatures[i] + negDefinedFeatures[i])
);
console.log("found ", this.usedFeatures.length, " used features");
this.posOrderedFeatures = sortBy(
this.usedFeatures,
i => posDefinedFeatures[i]
);
this.negOrderedFeatures = sortBy(
this.usedFeatures,
i => -negDefinedFeatures[i]
);
this.singleValueFeatures = filter(
this.usedFeatures,
i => definedFeaturesValues[i] > 0
);
let options = [
"sample order by similarity",
"sample order by output value",
"original sample ordering"
].concat(this.singleValueFeatures.map(i => this.props.featureNames[i]));
if (this.props.ordering_keys != null) {
options.unshift("sample order by key");
}
let xLabelOptions = this.xlabel.selectAll("option").data(options);
xLabelOptions
.enter()
.append("option")
.merge(xLabelOptions)
.attr("value", d => d)
.text(d => d);
xLabelOptions.exit().remove();
let n = this.props.outNames[0]
? this.props.outNames[0]
: "model output value";
options = map(this.usedFeatures, i => [
this.props.featureNames[i],
this.props.featureNames[i] + " effects"
]);
options.unshift(["model output value", n]);
let yLabelOptions = this.ylabel.selectAll("option").data(options);
yLabelOptions
.enter()
.append("option")
.merge(yLabelOptions)
.attr("value", d => d[0])
.text(d => d[1]);
yLabelOptions.exit().remove();
this.ylabel
.style(
"top",
(this.height - 10 - this.topOffset) / 2 + this.topOffset + "px"
)
.style("left", 10 - this.ylabel.node().offsetWidth / 2 + "px");
this.internalDraw();
}
internalDraw() {
// we fill in any implicit feature values and assume they have a zero effect and value
for (let e of this.props.explanations) {
for (let i of this.usedFeatures) {
if (!e.features.hasOwnProperty(i)) {
e.features[i] = { effect: 0, value: 0 };
}
e.features[i].ind = i;
}
}
let explanations;
let xsort = this.xlabel.node().value;
// Set scaleTime if time ticks provided for original ordering
let isTimeScale = ((xsort === "sample order by key") &&
(this.props.ordering_keys_time_format != null));
if (isTimeScale) {
this.xscale = scaleTime();
} else {
this.xscale = scaleLinear();
}
this.xaxis.scale(this.xscale);
if (xsort === "sample order by similarity") {
explanations = sortBy(this.props.explanations, x => x.simIndex);
each(explanations, (e, i) => (e.xmap = i));
} else if (xsort === "sample order by output value") {
explanations = sortBy(this.props.explanations, x => -x.outValue);
each(explanations, (e, i) => (e.xmap = i));
} else if (xsort === "original sample ordering") {
explanations = sortBy(this.props.explanations, x => x.origInd);
each(explanations, (e, i) => (e.xmap = i));
} else if (xsort === "sample order by key") {
explanations = this.props.explanations;
if (isTimeScale) {
each(explanations, (e, i) => (e.xmap = this.parseTime(this.props.ordering_keys[i])));
} else {
each(explanations, (e, i) => (e.xmap = this.props.ordering_keys[i]));
}
explanations = sortBy(explanations, e => e.xmap);
} else {
let ind = findKey(this.props.featureNames, x => x === xsort);
each(this.props.explanations, (e, i) => (e.xmap = e.features[ind].value));
let explanations2 = sortBy(this.props.explanations, x => x.xmap);
let xvals = map(explanations2, x => x.xmap);
if (typeof xvals[0] == "string") {
alert("Ordering by category names is not yet supported.");
return;
}
let xmin = min(xvals);
let xmax = max(xvals);
let binSize = (xmax - xmin) / 100;
// Build explanations where effects are averaged when the x values are identical
explanations = [];
let laste, copye;
for (let i = 0; i < explanations2.length; ++i) {
let e = explanations2[i];
if (
(laste && (!copye && e.xmap - laste.xmap <= binSize)) ||
(copye && e.xmap - copye.xmap <= binSize)
) {
if (!copye) {
copye = cloneDeep(laste);
copye.count = 1;
}
for (let j of this.usedFeatures) {
copye.features[j].effect += e.features[j].effect;
copye.features[j].value += e.features[j].value;
}
copye.count += 1;
} else if (laste) {
if (copye) {
for (let j of this.usedFeatures) {
copye.features[j].effect /= copye.count;
copye.features[j].value /= copye.count;
}
explanations.push(copye);
copye = undefined;
} else {
explanations.push(laste);
}
}
laste = e;
}
if (laste.xmap - explanations[explanations.length - 1].xmap > binSize) {
explanations.push(laste);
}
}
// adjust for the correct y-value we are plotting
this.currUsedFeatures = this.usedFeatures;
this.currPosOrderedFeatures = this.posOrderedFeatures;
this.currNegOrderedFeatures = this.negOrderedFeatures;
//let filteredFeatureNames = this.props.featureNames;
let yvalue = this.ylabel.node().value;
if (yvalue !== "model output value") {
let olde = explanations;
explanations = cloneDeep(explanations); // TODO: add pointer from old explanations which is prop.explanations to new ones
let ind = findKey(this.props.featureNames, x => x === yvalue);
for (let i = 0; i < explanations.length; ++i) {
let v = explanations[i].features[ind];
explanations[i].features = {};
explanations[i].features[ind] = v;
olde[i].remapped_version = explanations[i];
}
//filteredFeatureNames = [this.props.featureNames[ind]];
this.currUsedFeatures = [ind];
this.currPosOrderedFeatures = [ind];
this.currNegOrderedFeatures = [ind];
}
this.currExplanations = explanations;
// determine the link function
if (this.props.link === "identity") {
// assume all links are the same
this.invLinkFunction = x => this.props.baseValue + x;
} else if (this.props.link === "logit") {
this.invLinkFunction = x =>
1 / (1 + Math.exp(-(this.props.baseValue + x))); // logistic is inverse of logit
} else {
console.log("ERROR: Unrecognized link function: ", this.props.link);
}
this.predValues = map(explanations, e =>
sum(map(e.features, x => x.effect))
);
let width = this.wrapper.node().offsetWidth;
if (width == 0) return setTimeout(() => this.draw(explanations), 500);
this.svg.style("height", this.height + "px");
this.svg.style("width", width + "px");
let xvals = map(explanations, x => x.xmap);
this.xscale
.domain([min(xvals), max(xvals)])
.range([this.leftOffset, width])
.clamp(true);
this.xaxisElement
.attr("transform", "translate(0," + this.topOffset + ")")
.call(this.xaxis);
for (let i = 0; i < this.currExplanations.length; ++i) {
this.currExplanations[i].xmapScaled = this.xscale(
this.currExplanations[i].xmap
);
}
let N = explanations.length;
let domainSize = 0;
for (let ind = 0; ind < N; ++ind) {
let data2 = explanations[ind].features;
//if (data2.length !== P) error("Explanations have differing numbers of features!");
let totalPosEffects =
sum(map(filter(data2, x => x.effect > 0), x => x.effect)) || 0;
let totalNegEffects =
sum(map(filter(data2, x => x.effect < 0), x => -x.effect)) || 0;
domainSize = Math.max(
domainSize,
Math.max(totalPosEffects, totalNegEffects) * 2.2
);
}
this.yscale
.domain([-domainSize / 2, domainSize / 2])
.range([this.height - 10, this.topOffset]);
this.yaxisElement
.attr("transform", "translate(" + this.leftOffset + ",0)")
.call(this.yaxis);
for (let ind = 0; ind < N; ++ind) {
let data2 = explanations[ind].features;
//console.log(length(data2))
let totalNegEffects =
sum(map(filter(data2, x => x.effect < 0), x => -x.effect)) || 0;
//let scaleOffset = height/2 - this.yscale(totalNegEffects);
// calculate the position of the join point between positive and negative effects
// and also the positions of each feature effect block
let pos = -totalNegEffects,
i;
for (i of this.currPosOrderedFeatures) {
data2[i].posyTop = this.yscale(pos);
if (data2[i].effect > 0) pos += data2[i].effect;
data2[i].posyBottom = this.yscale(pos);
data2[i].ind = i;
}
let joinPoint = pos;
for (i of this.currNegOrderedFeatures) {
data2[i].negyTop = this.yscale(pos);
if (data2[i].effect < 0) pos -= data2[i].effect;
data2[i].negyBottom = this.yscale(pos);
}
explanations[ind].joinPoint = joinPoint;
explanations[ind].joinPointy = this.yscale(joinPoint);
}
let lineFunction = line()
.x(d => d[0])
.y(d => d[1]);
let areasPos = this.mainGroup
.selectAll(".force-bar-array-area-pos")
.data(this.currUsedFeatures);
areasPos
.enter()
.append("path")
.attr("class", "force-bar-array-area-pos")
.merge(areasPos)
.attr("d", i => {
let topPoints = map(range(N), j => [
explanations[j].xmapScaled,
explanations[j].features[i].posyTop
]);
let bottomPoints = map(rangeRight(N), j => [
explanations[j].xmapScaled,
explanations[j].features[i].posyBottom
]);
return lineFunction(topPoints.concat(bottomPoints));
})
.attr("fill", this.colors[0]);
areasPos.exit().remove();
let areasNeg = this.mainGroup
.selectAll(".force-bar-array-area-neg")
.data(this.currUsedFeatures);
areasNeg
.enter()
.append("path")
.attr("class", "force-bar-array-area-neg")
.merge(areasNeg)
.attr("d", i => {
let topPoints = map(range(N), j => [
explanations[j].xmapScaled,
explanations[j].features[i].negyTop
]);
let bottomPoints = map(rangeRight(N), j => [
explanations[j].xmapScaled,
explanations[j].features[i].negyBottom
]);
return lineFunction(topPoints.concat(bottomPoints));
})
.attr("fill", this.colors[1]);
areasNeg.exit().remove();
let dividersPos = this.mainGroup
.selectAll(".force-bar-array-divider-pos")
.data(this.currUsedFeatures);
dividersPos
.enter()
.append("path")
.attr("class", "force-bar-array-divider-pos")
.merge(dividersPos)
.attr("d", i => {
let points = map(range(N), j => [
explanations[j].xmapScaled,
explanations[j].features[i].posyBottom
]);
return lineFunction(points);
})
.attr("fill", "none")
.attr("stroke-width", 1)
.attr("stroke", () => this.colors[0].brighter(1.2));
dividersPos.exit().remove();
let dividersNeg = this.mainGroup
.selectAll(".force-bar-array-divider-neg")
.data(this.currUsedFeatures);
dividersNeg
.enter()
.append("path")
.attr("class", "force-bar-array-divider-neg")
.merge(dividersNeg)
.attr("d", i => {
let points = map(range(N), j => [
explanations[j].xmapScaled,
explanations[j].features[i].negyTop
]);
return lineFunction(points);
})
.attr("fill", "none")
.attr("stroke-width", 1)
.attr("stroke", () => this.colors[1].brighter(1.5));
dividersNeg.exit().remove();
let boxBounds = function(es, ind, starti, endi, featType) {
let maxTop, minBottom;
if (featType === "pos") {
maxTop = es[starti].features[ind].posyBottom;
minBottom = es[starti].features[ind].posyTop;
} else {
maxTop = es[starti].features[ind].negyBottom;
minBottom = es[starti].features[ind].negyTop;
}
let t, b;
for (let i = starti + 1; i <= endi; ++i) {
if (featType === "pos") {
t = es[i].features[ind].posyBottom;
b = es[i].features[ind].posyTop;
} else {
t = es[i].features[ind].negyBottom;
b = es[i].features[ind].negyTop;
}
if (t > maxTop) maxTop = t;
if (b < minBottom) minBottom = b;
}
return { top: maxTop, bottom: minBottom };
};
let neededWidth = 100;
let neededHeight = 20;
let neededBuffer = 100;
// find areas on the plot big enough for feature labels
let featureLabels = [];
for (let featType of ["pos", "neg"]) {
for (let ind of this.currUsedFeatures) {
let starti = 0,
endi = 0,
boxWidth = 0,
hbounds = { top: 0, bottom: 0 };
let newHbounds;
while (endi < N - 1) {
// make sure our box is long enough
while (boxWidth < neededWidth && endi < N - 1) {
++endi;
boxWidth =
explanations[endi].xmapScaled - explanations[starti].xmapScaled;
}
// and high enough
hbounds = boxBounds(explanations, ind, starti, endi, featType);
while (hbounds.bottom - hbounds.top < neededHeight && starti < endi) {
++starti;
hbounds = boxBounds(explanations, ind, starti, endi, featType);
}
boxWidth =
explanations[endi].xmapScaled - explanations[starti].xmapScaled;
// we found a spot!
if (
hbounds.bottom - hbounds.top >= neededHeight &&
boxWidth >= neededWidth
) {
//console.log(`found a spot! ind: ${ind}, starti: ${starti}, endi: ${endi}, hbounds:`, hbounds)
// make our box as long as possible
while (endi < N - 1) {
++endi;
newHbounds = boxBounds(explanations, ind, starti, endi, featType);
if (newHbounds.bottom - newHbounds.top > neededHeight) {
hbounds = newHbounds;
} else {
--endi;
break;
}
}
boxWidth =
explanations[endi].xmapScaled - explanations[starti].xmapScaled;
//console.log("found ",boxWidth,hbounds)
featureLabels.push([
(explanations[endi].xmapScaled +
explanations[starti].xmapScaled) /
2,
(hbounds.top + hbounds.bottom) / 2,
this.props.featureNames[ind]
]);
let lastEnd = explanations[endi].xmapScaled;
starti = endi;
while (
lastEnd + neededBuffer > explanations[starti].xmapScaled &&
starti < N - 1
) {
++starti;
}
endi = starti;
}
}
}
}
let featureLabelText = this.onTopGroup
.selectAll(".force-bar-array-flabels")
.data(featureLabels);
featureLabelText
.enter()
.append("text")
.attr("class", "force-bar-array-flabels")
.merge(featureLabelText)
.attr("x", d => d[0])
.attr("y", d => d[1] + 4)
.text(d => d[2]);
featureLabelText.exit().remove();
}
componentWillUnmount() {
window.removeEventListener("resize", this.redraw);
}
render() {
return (
<div
ref={x => (this.wrapper = select(x))}
style={{ textAlign: "center" }}
>
<style
dangerouslySetInnerHTML={{
__html: `
.force-bar-array-wrapper {
text-align: center;
}
.force-bar-array-xaxis path {
fill: none;
opacity: 0.4;
}
.force-bar-array-xaxis .domain {
opacity: 0;
}
.force-bar-array-xaxis paths {
display: none;
}
.force-bar-array-yaxis path {
fill: none;
opacity: 0.4;
}
.force-bar-array-yaxis paths {
display: none;
}
.tick line {
stroke: #000;
stroke-width: 1px;
opacity: 0.4;
}
.tick text {
fill: #000;
opacity: 0.5;
font-size: 12px;
padding: 0px;
}
.force-bar-array-flabels {
font-size: 12px;
fill: #fff;
text-anchor: middle;
}
.additive-force-array-xlabel {
background: none;
border: 1px solid #ccc;
opacity: 0.5;
margin-bottom: 0px;
font-size: 12px;
font-family: arial;
margin-left: 80px;
max-width: 300px;
}
.additive-force-array-xlabel:focus {
outline: none;
}
.additive-force-array-ylabel {
position: relative;
top: 0px;
left: 0px;
transform: rotate(-90deg);
background: none;
border: 1px solid #ccc;
opacity: 0.5;
margin-bottom: 0px;
font-size: 12px;
font-family: arial;
max-width: 150px;
}
.additive-force-array-ylabel:focus {
outline: none;
}
.additive-force-array-hoverLine {
stroke-width: 1px;
stroke: #fff;
opacity: 1;
}`
}}
/>
<select className="additive-force-array-xlabel" />
<div style={{ height: "0px", textAlign: "left" }}>
<select className="additive-force-array-ylabel" />
</div>
<svg
ref={x => (this.svg = select(x))}
style={{
userSelect: "none",
display: "block",
fontFamily: "arial",
sansSerif: true
}}
/>
</div>
);
}
}
AdditiveForceArrayVisualizer.defaultProps = {
plot_cmap: "RdBu",
ordering_keys: null,
ordering_keys_time_format: null
};
export default AdditiveForceArrayVisualizer;