Skip to main content
cancel
Showing results for 
Search instead for 
Did you mean: 

Register now to learn Fabric in free live sessions led by the best Microsoft experts. From Apr 16 to May 9, in English and Spanish.

Reply
cogsie
Helper I
Helper I

Access Individuals Parts of a Merged SVG

I am creating a custom visualization based on a horizontal bar chart.  I would like to add labels to it, but was not able to figure out the label utility in chart utilities in Power BI, so I am creating them from scratch.

 

Overall it is working pretty well, but I am running into a small problem I can't seem to figure out.  If the bars are very short it is possible that the label will spill over the left side axis.  In this case I would like to hide the labels.  The problem is the only way I can figure out to do this is to either hide all of them or none of them.  I am not able to determine the position of an individual label and then only hide that label.

 

I'm not sure if it is relevant, but I create the labels using the d3.merge function.  When I try to use d3.select or d3.selectAll it only returns values for the first data point but none of the others.  I am very new to all of this, so I am not entirely sure how it all works together, but my best guess is that the data points driving the labels are stored as an array in the merged SVG element, but I can't figure out how to access individual data points/labels.  Any suggestions would be much appreciated.

 

Below are the parts of the code that seemed relevant to me.  If I am leaving out any important parts of the code, or any important details, please let me know.  Thanks in advance for the help.

 

From the constructor:

        /** Assign visual host */
        this.host = options.host;

        this.svg = d3.select(this.target)
            .append('div')
            .append('svg')
            .classed('Barchart',true);

        this.backBarContainer = this.svg
            .append('g')
            .classed('backBarContainer', true);

        this.backPlotBackground = this.backBarContainer
            .append('rect')
            .classed('backPlotBackground', true);
            
        this.xAxisContainer = this.svg
            .append('g')
            .classed('xAxis', true);

        this.yAxisContainer = this.svg
            .append('g')
            .classed('yAxis', true);

        this.frontBarContainer = this.svg
            .append('g')
            .classed('frontBarContainer', true);

        this.frontPlotBackground = this.frontBarContainer
            .append('rect')
            .classed('frontPlotBackground', true);

        this.backLabelContainer = this.svg
            .append('g')
            .classed('backLabelContainer', true);

        this.backTextContainer = this.svg
            .append('g')
            .classed('backTextContainer', true);

        this.settings = VisualSettings.getDefault() as VisualSettings;

 

From the update:

        this.backLabelContainer
            .attr("transform", "translate(" + plotArea.x + "," + plotArea.y + ")")
            .attr("width", options.viewport.width)
            .attr("height", options.viewport.height);

        this.backTextContainer
            .attr("transform", "translate(" + plotArea.x + "," + plotArea.y + ")")
            .attr("width", options.viewport.width)
            .attr("height", options.viewport.height);

...

        this.backLabelSelection = this.backLabelContainer
            .selectAll('.bar')
            .data(viewModel.DataPoints);

        this.backTextSelection = this.backTextContainer
            .selectAll('.bar')
            .data(viewModel.DataPoints);

...
        const backLabelMerged = this.backLabelSelection
            .enter()
            .append('rect')
            .merge(<any>this.backLabelSelection)
            .classed('bar', true);

        const backLabelTextMerged = this.backTextSelection
            .enter()
            .append('text')
            .merge(<any>this.backTextSelection)
            .classed('bar', true);

...
        var m = 0
        var labelfontSize = viewModel.LabelFontSize
        var textHeightNum = 0
        do{

            var textHeight = (dataPoint: BarchartDataPoint) => (textMeasurementService.measureSvgTextHeight(
                {
                    text: valueFormatter.format(dataPoint.backBar),
                    fontFamily: viewModel.FontFamily,
                    fontSize: labelfontSize  + "px"
                }
            ))

            backLabelTextMerged
                .attr("y", (dataPoint: BarchartDataPoint) => (
                    yScale(dataPoint.category)

                    + (yScale.bandwidth() / 2)

                    + (textMeasurementService.measureSvgTextHeight(
                        {
                            text: valueFormatter.format(dataPoint.backBar),
                            fontFamily: viewModel.FontFamily,
                            fontSize: labelfontSize + "px"
                        }
                    )
                    ) / 4
                ))
                .attr("x", (dataPoint: BarchartDataPoint) => (
                    xScale(Number(dataPoint.backBar)) 
                    - textMeasurementService.measureSvgTextWidth( {
                        text: valueFormatter.format(dataPoint.backBar),
                        fontFamily: viewModel.FontFamily,
                        fontSize: labelfontSize + "px"
                    })
                    -10

                    ))
                .style("fill", viewModel.BackBarColor)
                .style("font-family", viewModel.FontFamily)
                .style("font-size", labelfontSize + "px")
                .text((dataPoint: BarchartDataPoint) => (valueFormatter.format(dataPoint.backBar)))

            labelfontSize--
            m++
        } while (textHeight(viewModel.DataPoints[0]) > Number(backBarSelectionMerged.attr("height")) && m<100)

        backLabelMerged
            .attr("y", (dataPoint: BarchartDataPoint) => (
                yScale(dataPoint.category) 
                + (yScale.bandwidth() / 2)
                - (textMeasurementService.measureSvgTextHeight(
                    {
                    text: valueFormatter.format(dataPoint.backBar),
                        fontFamily: viewModel.FontFamily,
                        fontSize: labelfontSize + "px"
                    }
                )
                )/2
            ))

            .attr("x", (dataPoint: BarchartDataPoint) => (
                xScale(Number(dataPoint.backBar))
                - textMeasurementService.measureSvgTextWidth({
                    text: valueFormatter.format(dataPoint.backBar),
                    fontFamily: viewModel.FontFamily,
                    fontSize: labelfontSize + "px"
                })
                - 15
            ))
            
            .attr("height", (dataPoint: BarchartDataPoint) => {
                let textProperties: TextProperties = {
                    text: valueFormatter.format(dataPoint.backBar),
                    fontFamily: viewModel.FontFamily,
                    fontSize: labelfontSize + "px"
                }

                let textHeight = textMeasurementService.measureSvgTextHeight(textProperties) + "px"
                textHeightNum = textMeasurementService.measureSvgTextHeight(textProperties)
                return (textHeight)
            })

            .attr("rx",textHeightNum*.2 )

            .attr("width", (dataPoint: BarchartDataPoint) => {
                let textProperties: TextProperties = {
                    text: valueFormatter.format(dataPoint.backBar),
                    fontFamily: viewModel.FontFamily,
                    fontSize: labelfontSize + "px"
                }

                let textWidth = (textMeasurementService.measureSvgTextWidth(textProperties)+10) + "px"
                return(textWidth)
            })

            .style("fill", "#000000")
            .style("fill-opacity", "40%")

        var xPosition = [];
        for (let index = 0; index < viewModel.DataPoints.length; index++) {
            const element = (

                xScale(Number(viewModel.DataPoints[index].backBar))
                - textMeasurementService.measureSvgTextWidth({
                    text: valueFormatter.format(viewModel.DataPoints[index].backBar),
                    fontFamily: viewModel.FontFamily,
                    fontSize: labelfontSize + "px"
                })
                - 15
            )
            xPosition.push(element)
        }

        if (d3.min(xPosition) < 0) {
            d3.select(".backTextContainer").selectAll("*").remove()
            d3.select(".backLabelContainer").selectAll("*").remove()
        }
1 ACCEPTED SOLUTION
cogsie
Helper I
Helper I

In case anyone else has a similar question, I thought I would post how I resolved this issue.

 

I was not able to access the individual parts of the merged SVG element.  I am guessing there is a way to do it and I just can't figure out how.  But I found a work around.

 

I created a list for each property of the SVG element with each value in the list coming from the data points.  Then I evaluated each value in the list for whatever condition I did not want.  If the condition existed I zeroed out all the other properties related to that individual data point.

 

For example, I created a list of all the x positions for the rectangle I was using as a background for my data label.  Then I checked if any of the values were negative, which meant they were going to the left of the y-axis.  If any of the values were negative I zeroed out the x position, and the width of the rectangle so it would disappear.  I also zeroed out the x position of the the text for the label, and changed the text to being empty.  Below I am posting some snippets of the code, so you can get an idea of how this worked.

 

First I created all the list variables with the initial values:

            /**BACK label BACKGROUND y position */
            var backLabelRectY = []
            for (let i = 0; i < dataPoints.length; i++) {
                backLabelRectY.push(
                    yScale(dataPoints[i].category)
                    + (yScale.bandwidth() / 2)
                    - (textMeasurementService.measureSvgTextHeight(
                        {
                            text: labelValueFormatter.format(dataPoints[i].backBar),
                            fontFamily: viewModel.FontFamily,
                            fontSize: backLabelfontSize + "px"
                        }
                    )
                    ) / 2
                )
            };

            /**BACK label BACKGROUND x position */
            var backLabelRectX = []
            for (let i = 0; i < dataPoints.length; i++) {
                backLabelRectX.push(
                    xScale(Number(dataPoints[i].backBar))
                    - textMeasurementService.measureSvgTextWidth({
                        text: labelValueFormatter.format(dataPoints[i].backBar),
                        fontFamily: viewModel.FontFamily,
                        fontSize: backLabelfontSize + "px"
                    })
                    - 15
                )

            };

            /**BACK label BACKGROUND height */
            var backLabelRectHeight = textMeasurementService.measureSvgTextHeight({
                text: labelValueFormatter.format(dataPoints[0].backBar),
                fontFamily: viewModel.FontFamily,
                fontSize: backLabelfontSize + "px"
                } )

            /**BACK label BACKGROUND width */
            var backLabelRectWidth = []
            for (let i = 0; i < dataPoints.length; i++) {
                backLabelRectWidth.push(
                    textMeasurementService.measureSvgTextWidth({
                        text: labelValueFormatter.format(dataPoints[i].backBar),
                        fontFamily: viewModel.FontFamily,
                        fontSize: backLabelfontSize + "px"
                    })
                    + 10
                )

            };

            /**BACK label BACKGROUND other variables*/
            var backLabelRectFill = viewModel.LabelBackgroundColor
            var backLabelRectOpacity = 1 - viewModel.LabelTransparency

 

Then I checked if any of the x position values were negative, indicating they were to the left of the left axis. If they are negative I emptied the text string, changed the x position of the text element and rectangle element to 0, and changed the rectangle width to 0.  This makes the label disappear (I'm not sure if there is a more effective way of making it disappear, but this is what I came up with).

 

            /**checking if back labels are to the left of the y-axis and removing them if they are*/
            for(let i=0; i < backLabelRectX.length; i++){
                if(backLabelRectX[i]<=0){
                    backLabelTextText[i]=""
                    backLabelTextX[i] =0
                    backLabelRectX[i]=0
                    backLabelRectWidth[i]=0
                }
            }

   

Then I created the label background based on the lists:

            /**create the BACK label background */
            backLabelMerged
                .attr("y", (_: BarchartDataPoint, i) => (backLabelRectY[i]))
                .attr("x", (_: BarchartDataPoint, i) => (backLabelRectX[i]))
                .attr("height", backLabelRectHeight)
                .attr("rx", backLabelRectHeight * .2)
                .attr("width", (_: BarchartDataPoint, i) => (backLabelRectWidth[i]))
                .style("fill", backLabelRectFill)
                .style("fill-opacity", backLabelRectOpacity)

 

And merged it into the rest of the bar element:

        const backLabelMerged = this.backLabelSelection
            .enter()
            .append('rect')
            .merge(<any>this.backLabelSelection)
            .classed('bar', true);

 

Hopefully this is helpful.

View solution in original post

1 REPLY 1
cogsie
Helper I
Helper I

In case anyone else has a similar question, I thought I would post how I resolved this issue.

 

I was not able to access the individual parts of the merged SVG element.  I am guessing there is a way to do it and I just can't figure out how.  But I found a work around.

 

I created a list for each property of the SVG element with each value in the list coming from the data points.  Then I evaluated each value in the list for whatever condition I did not want.  If the condition existed I zeroed out all the other properties related to that individual data point.

 

For example, I created a list of all the x positions for the rectangle I was using as a background for my data label.  Then I checked if any of the values were negative, which meant they were going to the left of the y-axis.  If any of the values were negative I zeroed out the x position, and the width of the rectangle so it would disappear.  I also zeroed out the x position of the the text for the label, and changed the text to being empty.  Below I am posting some snippets of the code, so you can get an idea of how this worked.

 

First I created all the list variables with the initial values:

            /**BACK label BACKGROUND y position */
            var backLabelRectY = []
            for (let i = 0; i < dataPoints.length; i++) {
                backLabelRectY.push(
                    yScale(dataPoints[i].category)
                    + (yScale.bandwidth() / 2)
                    - (textMeasurementService.measureSvgTextHeight(
                        {
                            text: labelValueFormatter.format(dataPoints[i].backBar),
                            fontFamily: viewModel.FontFamily,
                            fontSize: backLabelfontSize + "px"
                        }
                    )
                    ) / 2
                )
            };

            /**BACK label BACKGROUND x position */
            var backLabelRectX = []
            for (let i = 0; i < dataPoints.length; i++) {
                backLabelRectX.push(
                    xScale(Number(dataPoints[i].backBar))
                    - textMeasurementService.measureSvgTextWidth({
                        text: labelValueFormatter.format(dataPoints[i].backBar),
                        fontFamily: viewModel.FontFamily,
                        fontSize: backLabelfontSize + "px"
                    })
                    - 15
                )

            };

            /**BACK label BACKGROUND height */
            var backLabelRectHeight = textMeasurementService.measureSvgTextHeight({
                text: labelValueFormatter.format(dataPoints[0].backBar),
                fontFamily: viewModel.FontFamily,
                fontSize: backLabelfontSize + "px"
                } )

            /**BACK label BACKGROUND width */
            var backLabelRectWidth = []
            for (let i = 0; i < dataPoints.length; i++) {
                backLabelRectWidth.push(
                    textMeasurementService.measureSvgTextWidth({
                        text: labelValueFormatter.format(dataPoints[i].backBar),
                        fontFamily: viewModel.FontFamily,
                        fontSize: backLabelfontSize + "px"
                    })
                    + 10
                )

            };

            /**BACK label BACKGROUND other variables*/
            var backLabelRectFill = viewModel.LabelBackgroundColor
            var backLabelRectOpacity = 1 - viewModel.LabelTransparency

 

Then I checked if any of the x position values were negative, indicating they were to the left of the left axis. If they are negative I emptied the text string, changed the x position of the text element and rectangle element to 0, and changed the rectangle width to 0.  This makes the label disappear (I'm not sure if there is a more effective way of making it disappear, but this is what I came up with).

 

            /**checking if back labels are to the left of the y-axis and removing them if they are*/
            for(let i=0; i < backLabelRectX.length; i++){
                if(backLabelRectX[i]<=0){
                    backLabelTextText[i]=""
                    backLabelTextX[i] =0
                    backLabelRectX[i]=0
                    backLabelRectWidth[i]=0
                }
            }

   

Then I created the label background based on the lists:

            /**create the BACK label background */
            backLabelMerged
                .attr("y", (_: BarchartDataPoint, i) => (backLabelRectY[i]))
                .attr("x", (_: BarchartDataPoint, i) => (backLabelRectX[i]))
                .attr("height", backLabelRectHeight)
                .attr("rx", backLabelRectHeight * .2)
                .attr("width", (_: BarchartDataPoint, i) => (backLabelRectWidth[i]))
                .style("fill", backLabelRectFill)
                .style("fill-opacity", backLabelRectOpacity)

 

And merged it into the rest of the bar element:

        const backLabelMerged = this.backLabelSelection
            .enter()
            .append('rect')
            .merge(<any>this.backLabelSelection)
            .classed('bar', true);

 

Hopefully this is helpful.

Helpful resources

Announcements
Microsoft Fabric Learn Together

Microsoft Fabric Learn Together

Covering the world! 9:00-10:30 AM Sydney, 4:00-5:30 PM CET (Paris/Berlin), 7:00-8:30 PM Mexico City

PBI_APRIL_CAROUSEL1

Power BI Monthly Update - April 2024

Check out the April 2024 Power BI update to learn about new features.

April Fabric Community Update

Fabric Community Update - April 2024

Find out what's new and trending in the Fabric Community.