in src/barChart.ts [499:969]
public update(options: VisualUpdateOptions) {
// bar chart diagram
// ________________________________ _
// | | |
// | | | top and bottom padding (calcOuterPadding, outerPadding)
// |_______________________ | _ _
// | | | | |
// | | | | |
// |_______________________| | _ | x (calcX, xScaledMin, xScaledMax)
// | | | | It is sum of the bar height
// | | | | and the space between two bars
// | | | |
// |_____________________ | _ _
// | | | |
// | | | | h
// |_____________________| | _
// | | | padding between bars (barPadding).
// | | | This is percent of x that will be
// | | | used for padding and th rest will be bar height.
// |_________________ | _
// | | | |
// | | | | h
// |_________________| | _
// | | |
// | | | top and bottom padding are equal
// |_______________________________| _
this.events.renderingStarted(options);
let viewModel: IBarChartViewModel = visualTransform(options, this.host);
let settings = this.IBarChartSettings = viewModel.settings;
let width = options.viewport.width;
let height = options.viewport.height;
// Calculate max height of each bar based on the total height of the visual
let xScaledMax = height / BarChart.Config.maxHeightScale;
// Min height is independent of height the bar should be visible
// irrespective of the number of bars or the height of the visual
let xScaledMin = BarChart.Config.xScaledMin;
if (settings.barHeight && settings.barHeight.show) {
xScaledMin = settings.barHeight.height;
} else {
xScaledMin = BarChart.Config.xScaledMin;
}
let outerPadding = -0.1;
// calcX is the calculated height of the bar+inner padding that will be required if we simply
// distribute the height with the bar count (no scrolling)
let calcX = height /
(2 * BarChart.Config.outerPaddingScale - BarChart.Config.xScalePadding + viewModel.dataPoints.length);
// calcHeight is the height required for the entire bar chart
// if min allowed bar height is used. (This is needed for setting the scroll height)
let calcHeight = (-2 * outerPadding - BarChart.Config.xScalePadding + viewModel.dataPoints.length)
* xScaledMin;
// The parent element is not directly available to us since we are in a sandbox
if (calcX > xScaledMax) {
if (xScaledMax >= xScaledMin) {
let tempouterPadding = (height - (-BarChart.Config.xScalePadding + viewModel.dataPoints.length)
* xScaledMax) /
(2 * xScaledMax);
if (tempouterPadding > 0) {
outerPadding = tempouterPadding;
}
} else {
let tempOuterPadding = (height - (-BarChart.Config.xScalePadding + viewModel.dataPoints.length)
* xScaledMin) /
(2 * xScaledMin);
if (tempOuterPadding > 0) {
outerPadding = tempOuterPadding;
}
}
} else {
if (calcX < xScaledMin && calcHeight > height) {
height = calcHeight;
}
}
let h = options.viewport.height + 5;
let w = options.viewport.width;
this.divContainer.attr("style", "width:" + w + "px;height:" + h + "px;overflow-y:auto;overflow-x:hidden;");
this.svg.attr("width", width);
this.svg.attr("height", height);
this.xAxis.style("font-size", parseInt(min(<any>[height, width]), 10) * BarChart.Config.xAxisFontMultiplier);
// this.xAxis.attr("font-size",parseInt(min([height, width])) * BarChart.Config.xAxisFontMultiplier)
let yScale = scaleBand()
.domain(viewModel.dataPoints.map((d) => d.category))
.rangeRound([5, height])
.padding(BarChart.Config.barPadding)
.paddingOuter(outerPadding);
// .rangeBands([5, height], BarChart.Config.barPadding, outerPadding);
// cap the fontsize between 8.5 and 40 for aesthetics (only when autoscaling font)
let fontSizeToUse = this.IBarChartSettings.fontParams && this.IBarChartSettings.fontParams.show
? this.IBarChartSettings.fontParams.fontSize
: yScale.bandwidth() / BarChart.Config.fontScaleFactor;
if (fontSizeToUse < 8.5 && !this.IBarChartSettings.fontParams.show) {
fontSizeToUse = 8.5;
}
if (fontSizeToUse > 40 && !this.IBarChartSettings.fontParams.show) {
fontSizeToUse = 40;
}
// Calculate label size to compute max bar size to use
// to leave room for label to be displayed inside the draw area for the .
// Use the formatted value for the longest bar
let indexForDataMax = getIndexForDataMax(viewModel.dataPoints);
let formattedValue = viewModel.dataPoints.length > 0
? viewModel.dataPoints[indexForDataMax].formattedValue
: "";
let textProperties: ITextProperties = {
fontFamily: "sans-serif",
fontSize: fontSizeToUse + "px",
text: formattedValue,
};
let offset = textMeasurementService.textMeasurementService.measureSvgTextWidth(textProperties);
let xScale = scaleLinear()
.domain([0, viewModel.dataMax])
.range([0, width - offset - 40]); // subtracting 40 for padding between the bar and the label
// empty rect to take full width for clickable area for clearing selection
let rectContainer = this.barContainer.selectAll("rect.rect-container").data([0]);
rectContainer
.enter()
.append<SVGElement> ("rect")
.classed("rect-container", true);
rectContainer.attr("width", width);
rectContainer.attr("height", height);
rectContainer.attr("fill", "transparent");
let bars = this.barContainer
.selectAll("g.bar")
.data(viewModel.dataPoints);
if (viewModel.dataPoints.length === 0) {
let removeBars = this.barContainer.selectAll("g.bar");
removeBars.selectAll("rect.bar").remove();
removeBars.selectAll("rect.overlapBar").remove();
removeBars.selectAll("circle").remove();
removeBars.selectAll("line").remove();
removeBars.selectAll("text.bar-value").remove();
removeBars.selectAll("text.bar-text").remove();
removeBars.selectAll("rect.valuesRect").remove();
removeBars.remove();
}
bars
.enter()
.append<SVGElement> ("g")
.classed("bar", true)
.attr("x", BarChart.Config.xScalePadding)// .merge(bars)
.attr("y", (d) => yScale(d.category))
.attr("height", yScale.bandwidth())
.attr("width", (d) => xScale(<number> d.value))
.attr("selected", (d) => d.selected);
bars = this.barContainer
.selectAll("g.bar")
.data(viewModel.dataPoints);
let rects = bars
.selectAll("rect.bar").data((d) => [d]);
let mergeElement = rects
.enter()
.append<SVGElement> ("rect")
.classed("bar", true);
rects
.merge(mergeElement)
.attr("x", BarChart.Config.xScalePadding)
.attr("y", (d) => yScale(d.category))
.attr("height", yScale.bandwidth() /
(
(settings.barShape.shape === "Line" ||
settings.barShape.shape === "Lollipop" ||
settings.barShape.shape === "Hammer Head") ? 8 : 1
))
.attr("width", (d) => xScale(<number> d.value))
.attr("fill", viewModel.settings.generalView.barsColor.solid.color)
.attr("fill-opacity", viewModel.settings.generalView.opacity / 100)
.attr("selected", (d) => d.selected);
rects.exit().remove();
let overlapRects = bars.selectAll("rect.overlapBar").data((d) => [d]);
mergeElement = overlapRects
.enter()
.append<SVGElement> ("rect")
.classed("overlapBar", true);
overlapRects
.merge(mergeElement)
// overlapRects
.attr("x", BarChart.Config.xScalePadding)
.attr("y", (d) => yScale(d.category))
.attr("height", yScale.bandwidth() /
(
(
settings.barShape.shape === "Line" ||
settings.barShape.shape === "Lollipop" ||
settings.barShape.shape === "Hammer Head"
) ? 8 : 1))
.attr("width", (d) => xScale(<number> d.overlapValue)).merge(mergeElement)
.attr("fill", viewModel.settings.generalView.overlapColor.solid.color)
.attr("fill-opacity", viewModel.settings.generalView.opacity / 100)
.attr("selected", (d) => d.selected);
overlapRects.exit().remove();
if (settings.barShape.shape === "Lollipop") {
let circle = bars.selectAll("circle").data((d) => [d]);
mergeElement = circle.enter()
.append<SVGElement> ("circle")
.classed("head", true);
circle
.merge(mergeElement)
.attr("cx", (d) => getHeadPositionX(d.value, d.width) - 2 - yScale.bandwidth() / 8)
.attr("cy", (d) => yScale(d.category) + yScale.bandwidth() / 16)
// - textMeasurementService.textMeasurementService.measureSvgTextHeight(textProperties) / 4,
.attr("r", yScale.bandwidth() / 8)
.attr("fill", viewModel.settings.barShape.headColor.solid.color)
.attr("fill-opacity", viewModel.settings.generalView.opacity / 100);
circle.exit().remove();
} else {
bars.selectAll("circle").remove();
}
if (settings.barShape.shape === "Hammer Head") {
let line = bars.selectAll("line").data((d) => [d]);
mergeElement = line.enter()
.append<SVGElement> ("line")
.classed("head", true);
line.merge(mergeElement)
.attr("x1", (d) => getHeadPositionX(d.value, d.width) - 7 - yScale.bandwidth() / 32)
.attr("x2", (d) => getHeadPositionX(d.value, d.width) - 7 - yScale.bandwidth() / 32)
.attr("y1", (d) => yScale(d.category) - yScale.bandwidth() / 16)
// - textMeasurementService.textMeasurementService.measureSvgTextHeight(textProperties) / 4,
.attr("y2", (d) => yScale(d.category) + yScale.bandwidth() / 16 + yScale.bandwidth() / 8)
// - textMeasurementService.textMeasurementService.measureSvgTextHeight(textProperties) / 4,
.attr("stroke-width", yScale.bandwidth() / 16)
.attr("stroke", viewModel.settings.barShape.headColor.solid.color)
.attr("stroke-opacity", viewModel.settings.generalView.opacity / 100);
line.exit().remove();
} else {
bars.selectAll("line").remove();
}
textProperties = {
fontFamily: "Segoe UI",
fontSize: fontSizeToUse + "px",
text: "TEXT for calculating height",
};
let texts = bars
.selectAll("text.bar-text").data((d) => [d]);
mergeElement = texts
.enter()
.append<SVGElement> ("text")
.classed("bar-text", true);
texts.merge(mergeElement)
.attr("height", yScale.bandwidth())
.attr("y", (d) => yScale(d.category) + yScale.bandwidth() /
2 + textMeasurementService.textMeasurementService.measureSvgTextHeight(textProperties) / 4)
.attr("x", 5)
.attr("font-size", fontSizeToUse)
.attr("fill", viewModel.settings.generalView.textColor.solid.color)
.text((d) => d.category )
.each((d) => d.width = xScale(<number> d.value));
if (this.IBarChartSettings.experimental.show) {
texts.attr("style", "mix-blend-mode: " + this.IBarChartSettings.experimental.blendMode);
} else {
texts.attr("style", "mix-blend-mode: initial");
}
texts.exit().remove();
if (viewModel.settings.showBarLabels.show) {
let valuesRect = bars.selectAll("rect.valuesRect").data((d) => [d]);
mergeElement = valuesRect
.enter()
.append<SVGElement> ("rect")
.classed("valuesRect", true);
valuesRect
.merge(mergeElement)
.attr("x", (d) => viewModel.settings.alignBarLabels.show
? getTextPositionX(viewModel.dataMax, d.currTextWidth) - 2
: getTextPositionX(d.value, d.currTextWidth) - 2)
.attr("y", (d) => getTextPositionY(d.category, textProperties) - 3
/ 4 * textMeasurementService.textMeasurementService.measureSvgTextHeight(textProperties))
.attr("height", textMeasurementService.textMeasurementService.measureSvgTextHeight(textProperties))
// width is adding 5 for padding around text
.attr("width", (d) => 5 + textMeasurementService.textMeasurementService.measureSvgTextWidth(
{
fontFamily: "Segoe UI",
fontSize: fontSizeToUse + "px",
text: <string> d.formattedValue,
}))
.attr("fill", viewModel.settings.showBarLabels.highlightColor.solid.color)
.attr("fill-opacity", viewModel.settings.generalView.opacity / 100)
.attr("rx", 2)
.attr("ry", 2);
valuesRect.exit().remove();
let textValues = bars
.selectAll("text.bar-value").data((d) => [d]);
mergeElement = textValues
.enter()
.append<SVGElement> ("text")
.classed("bar-value", true);
textValues.merge(mergeElement).attr("height", yScale.bandwidth())
.attr("y", (d) => getTextPositionY(d.category, textProperties))
.attr("x", (d) => {
return viewModel.settings.alignBarLabels.show
? getTextPositionX(viewModel.dataMax, d.currTextWidth)
: getTextPositionX(d.value, d.currTextWidth);
})
.attr("font-size", fontSizeToUse)
.attr("fill", viewModel.settings.showBarLabels.textColor.solid.color)
.text((d) => { return <string> d.formattedValue; });
textValues.exit().remove();
} else {
let valuesRect = bars.selectAll("rect.valuesRect")
let textValues = bars.selectAll("text.bar-value")
valuesRect.remove()
textValues.remove()
}
else {
let valuesRect = bars.selectAll("rect.valuesRect")
let textValues = bars.selectAll("text.bar-value")
valuesRect.remove()
textValues.remove()
}
this.tooltipServiceWrapper.addTooltip(this.barContainer.selectAll(".bar"),
(tooltipEvent: ITooltipEventArgs<IBarChartDataPoint>) => this.getTooltipData(tooltipEvent.data),
(tooltipEvent: ITooltipEventArgs<IBarChartDataPoint>) => tooltipEvent.data.selectionId,
);
this.syncSelectionState(
bars,
this.selectionManager.getSelectionIds() as ISelectionId[],
);
this.syncSelectionState(
rects,
this.selectionManager.getSelectionIds() as ISelectionId[],
);
this.syncSelectionState(
overlapRects,
this.selectionManager.getSelectionIds() as ISelectionId[],
);
let selectionManager = this.selectionManager;
// this.svg.on("contextmenu", () => {
// const mouseEvent: MouseEvent = event as MouseEvent;
// const eventTarget: EventTarget = mouseEvent.target;
// let dataPoint = select(eventTarget).datum();
// selectionManager.showContextMenu(dataPoint ? dataPoint.selectionId : {}, {
// x: mouseEvent.clientX,
// y: mouseEvent.clientY
// });
// mouseEvent.preventDefault();
// });
// This must be an anonymous function instead of a lambda because
// d3 uses "this" as the reference to the element that was clicked.
let area = select("rect.rect-container");
area.on("click", () => {
if (self.IBarChartSettings.clearFilters.show && selectionManager.hasSelection()) {
selectionManager.clear().then(() => {
self.syncSelectionState(bars, []);
self.syncSelectionState(rects, []);
self.syncSelectionState(overlapRects, []);
});
}
bars.attr("fill-opacity", BarChart.Config.solidOpacity);
rects.attr("fill-opacity", BarChart.Config.solidOpacity);
overlapRects.attr("fill-opacity", BarChart.Config.solidOpacity);
});
let self: this = this;
bars.on("click", (d) => {
// set selected property of the attached data to false for all (later mark the one clicked as selected)
selectionManager.select(d.selectionId).then((ids: ISelectionId[]) => {
self.syncSelectionState(bars, ids);
self.syncSelectionState(rects, ids);
self.syncSelectionState(overlapRects, ids);
});
});
bars.exit()
.remove();
function getTextPositionX(value: PrimitiveValue, wid: number) {
if (settings.barShape.shape === "Bar") {
return xScale(<number> value) > wid ? xScale(<number> value) + 8 : wid + 12;
} else if (
settings.barShape.shape === "Line" ||
settings.barShape.shape === "Lollipop" ||
settings.barShape.shape === "Hammer Head") {
if (viewModel.settings.alignBarLabels.show) {
return 1.01 * (xScale(<number> value) + 8);
}
if (settings.barShape.labelPosition === "Top") {
return 1.01 * (xScale(<number> value) + 8);
} else {
return 1.01 * (wid + 8);
}
}
}
function getTextPositionY(category: string, textProps: TextProperties) {
if (settings.barShape.shape === "Bar") {
return yScale(category) + yScale.bandwidth() / 2 +
textMeasurementService.textMeasurementService.measureSvgTextHeight(textProps) / 4;
} else if (settings.barShape.shape === "Line" ||
settings.barShape.shape === "Lollipop" ||
settings.barShape.shape === "Hammer Head") {
if (settings.barShape.labelPosition === "Top") {
return yScale(category) +
yScale.bandwidth() / 16 +
textMeasurementService.textMeasurementService.measureSvgTextHeight(textProps) / 4;
} else {
return yScale(category) +
yScale.bandwidth() / 2 +
textMeasurementService.textMeasurementService.measureSvgTextHeight(textProps) / 4;
}
}
}
function getHeadPositionX(value: PrimitiveValue, wid: number) {
if (settings.barShape.shape === "Bar") {
return xScale(<number> value) > wid ? xScale(<number> value) + 8 : wid + 8;
} else if (settings.barShape.shape === "Line" ||
settings.barShape.shape === "Lollipop" ||
settings.barShape.shape === "Hammer Head") {
return xScale(<number> value) + 8;
}
}
this.events.renderingFinished(options);
}