【问题标题】:Create Grouped Stacked Bar Chart创建分组堆积条形图
【发布时间】:2021-07-23 21:08:10
【问题描述】:

大家好。再次在这里询问更多关于D3的问题。

我已经成功创建了这个堆积条形图,它按年份显示每个堆积条形图。现在,我想按组(CA、TX 和 HI)将这些堆叠的条形图组合在一起。

最后,我希望它看起来像这样。

我已经把头撞在墙上好几个小时了,试图调整 X 轴以适当地分组这些。谁能帮帮我?

这是/是我正在做的一些假设

  1. 我需要两个不同的 xScales(一个用于group,一个用于year
  2. 我不需要使用 d3.groupd3.nest(无论如何在最新版本的 d3 中不可用),因为我的数据已格式化为可以访问组。
  3. 我需要在组之间(如前所述 3 个)和年份之间的内部(例如,第一组有 3 个不同的年份)添加一些新的填充。

请帮帮我!任何事情都有帮助。

class D3GroupedStackedBarChart extends React.Component<Props, State> {
    state: State = {
        data: [
            {group: "CA", year: 1993, males: 100, females: 95, pets: 12},
            {group: "CA", year: 1994, males: 80, females: 88, pets: 8},
            {group: "CA", year: 1995, males: 70, females: 30, pets: 34},
            {group: "TX", year: 1996, males: 111, females: 122, pets: 32},
            {group: "TX", year: 1997, males: 25, females: 25, pets: 64},
            {group: "HI", year: 1998, males: 13, females: 45, pets: 72},
        ],
    };

    /*
    https://bl.ocks.org/SpaceActuary/6233700e7f443b719855a227f4749ee5
    */

    componentDidMount() {
        const {data} = this.state;
        const keys = ["males", "females", "pets"];
        const groups = ["CA", "TX", "HI"];
        const colors = {
            males: "blue",
            females: "pink",
            pets: "green",
        };

        // Width and height of our original box
        const width = 1000;
        const height = 1000;
        // Margin we want, and making the center SVG to hold our graph
        const margin = {top: 80, right: 180, bottom: 80, left: 180};
        const padding = 0.1;

        // Creating a function to create layers
        const stackGenerator = d3.stack().keys(keys); // now a function
        // Creating layers from our data and keys
        // keys = our layers
        const layers = stackGenerator(data); // now a function

        // Origin of an SVG is in the TOP LEFT corner
        const svg = d3
            .select("#test")
            .append("svg") // append an svg element to our div#test
            // Creating the actual height and width of our svg to hold report
            .attr("height", height - margin.top - margin.bottom)
            .attr("width", width - margin.left - margin.right)
            .attr("viewBox", [0, 0, width, height])
            .style("background-color", Color.white);

        // SCALE (group)
        const xScaleGroup = d3
            .scaleBand()
            .domain(data.map(d => d.group))
            .range([margin.left, width - margin.right]);

        // SCALE (year)
        const xScale = d3
            // Scaleband just means determined based off # of inputs
            // and not off a huge range (that's for the y-axis)
            .scaleBand()
            // Complete set of values, so YEAR
            .domain(data.map(d => d.year))
            // Range is the remaining width of our SVG we want report ing
            .range([margin.left, width - margin.right])
            .padding(padding);

        // looking at second value / y value
        const extent = [
            0.9 * d3.min(layers, layer => d3.min(layer, d => d[1])),
            1.1 * d3.max(layers, layer => d3.max(layer, d => d[1])),
        ];
        const [yMin, yMax] = extent;

        const yScale = d3
            .scaleLinear()
            .domain(extent)
            .range([height - margin.bottom, margin.top]); // range from bottom up

        // AXIS
        const xAxis = g => {
            // bottom align it
            g.attr("transform", `translate(0, ${height - margin.bottom})`)
                .attr("class", "x-axis")
                .call(d3.axisBottom(xScale))
                .call(g => {
                    g.selectAll(".tick>line").remove();
                })
                .call(g => g.select(".domain").attr("d", "M180,0H820"))
                .attr("font-size", "12px");
        };

        const yAxis = g => {
            g.attr("transform", `translate(${margin.left - 20}, 0)`)
                .attr("class", "y-axis")
                .call(d3.axisLeft(yScale))
                .call(g => g.selectAll(".domain").remove())
                .call(g => {
                    g.selectAll(".tick>line")
                        .attr("x2", -50)
                        .attr("x1", -34);
                })
                .attr("font-size", "12px");
        };

        const yAxisLabel = g => {
            g.append("text")
                .attr("text-anchor", "start")
                .attr("fill", "black")
                .attr("font-size", "12px")
                .attr("x", -40)
                .attr("y", height - 60)
                .text("UNITS");
        };

        // Create tooltip
        const Tooltip = d3
            .select("#test")
            .append("div")
            .style("opacity", 0)
            .attr("class", css(styles.tooltip));

        // Three function that change the tooltip when user hover / move / leave a cell
        const mouseover = function(event, data) {
            Tooltip.style("opacity", 1);
            d3.select(this)
                .style("stroke", "black")
                .style("opacity", 1);
        };

        const createTooltipHtml = (key, year, value) => {
            return ReactDOMServer.renderToStaticMarkup(
                <>
                    <HeadingXSmall style={styles.tooltipHeader}>
                        5th {key} / 211-217
                    </HeadingXSmall>
                    <Body style={styles.tooltipSubheader}>
                        Identify coordinates
                    </Body>
                    <Body style={styles.infoContainer}>
                        <div
                            className={css(styles.box)}
                            style={{background: colors[key]}}
                        ></div>
                        <Body style={styles.tooltipInfo}>
                            {year}: {value} things
                        </Body>
                    </Body>
                    <hr style={{margin: "24px 0"}}></hr>
                    <img
                        src={staticUrl("/images/districts/graph.png")}
                        alt={i18n._("Sample image for tooltip")}
                        style={styles.tooltipImage}
                    />
                </>,
            );
        };

        const mousemove = function(event, data) {
            const {0: start, 1: end, data: d} = data;
            const {target, layerX: x, layerY: y} = event;

            const layerKey = d3.select(target.parentNode).datum().key;

            const tooltipHtml = createTooltipHtml(
                layerKey,
                d.year,
                end - start,
            );

            Tooltip.html(tooltipHtml)
                .style("left", x + 10 + "px")
                .style("top", y - 10 + "px");
        };

        const mouseleave = function(event, data) {
            Tooltip.style("opacity", 0);
            d3.select(this)
                .style("stroke", "none")
                .style("opacity", 1);
        };

        // Creating Legend
        const legend = svg
            .append("g")
            .attr("class", "legend")
            .attr("transform", d => "translate(0, 0)")
            .attr("font-size", "12px")
            .attr("text-anchor", "start")
            .selectAll("g")
            .data(keys)
            .join("g") // Create 3 "g" elements that are initially empty
            .attr("transform", (d, i) => "translate(0," + i * 30 + ")");

        // Add square and their color
        legend
            .append("rect") // append a rect to each individual g
            .attr("fill", d => colors[d])
            .attr("x", width - margin.right)
            .attr("rx", 3)
            .attr("width", 19)
            .attr("height", 19);

        // Add text next to squares
        legend
            .append("text")
            .attr("x", width - margin.right + 40)
            .attr("y", 9.5)
            .attr("dy", "0.32em")
            .text(d => d);

        // Add header
        const legendHeader = d3
            .select(".legend")
            .append("g")
            .attr("transform", (d, i) => "translate(0, -20)")
            .lower()
            .append("text")
            .attr("x", width - margin.right)
            .attr("font-size", "12px")
            .text(() => {
                const text = "Master Levels";
                return text.toLocaleUpperCase();
            });

        // Get coordinates and height of legend to add border
        const {
            x: legendX,
            y: legendY,
            width: legendWidth,
            height: legendHeight,
        } = d3
            .select(".legend")
            .node()
            .getBBox();

        const borderPadding = 20;

        // Create border for legend
        // Adding a "border" manually
        const legendBox = svg
            .select(".legend")
            .append("rect")
            .lower()
            .attr("class", "legend-box")
            .attr("x", legendX - borderPadding)
            .attr("y", legendY - borderPadding)
            .attr("width", legendWidth + borderPadding * 2)
            .attr("height", legendHeight + borderPadding * 2)
            .attr("fill", "white")
            .attr("stroke", "black")
            .attr("opacity", 0.8);

        // Rendering
        // first, second, and third refer to `layers`
        // first --> layers
        // second --> edge1, edge2, and data
        svg.selectAll(".layer")
            .data(layers) // first
            .join("g") // create new element for each layer
            .attr("class", "layer")
            .attr("fill", layer => colors[layer.key])
            .selectAll("rect")
            .data(layer => layer) // second
            .join("rect")
            .attr("class", "series-rect")
            .attr("x", d => xScale(d.data.year))
            .attr("y", d => yScale(d[1]))
            .attr("width", xScale.bandwidth())
            .attr("height", (d, i, els) => {
                const [lower, upper] = d;
                const firstBarAdjustment = lower === 0 ? yMin : 0;

                return yScale(lower + firstBarAdjustment) - yScale(upper);
            })
            .on("mouseover", mouseover)
            .on("mousemove", mousemove)
            .on("mouseleave", mouseleave);

        svg.append("g").call(xAxis);

        svg.append("g")
            .call(yAxis)
            .call(yAxisLabel);

        svg.node();
    }

    render(): React.Node {
        return (
            <View>
                <LabelLarge>{i18n.doNotTranslate("D3.js")}</LabelLarge>
                <Strut size={Spacing.xLarge_32} />
                <div id="test" />
            </View>
        );
    }
}

【问题讨论】:

    标签: javascript d3.js


    【解决方案1】:

    这可能不是最干净的解决方案,但我之前已经这样做过并且它工作可靠。这是我刚刚拼凑的代码的超级粗略破解。我留下了几个内联 cmets。 (jsfiddle)

    const data = [{
        group: "CA",
        year: 1993,
        males: 100,
        females: 95,
        pets: 12
      },
      {
        group: "CA",
        year: 1994,
        males: 80,
        females: 88,
        pets: 8
      },
      {
        group: "CA",
        year: 1995,
        males: 70,
        females: 30,
        pets: 34
      },
      {
        group: "TX",
        year: 1996,
        males: 111,
        females: 122,
        pets: 32
      },
      {
        group: "TX",
        year: 1997,
        males: 25,
        females: 25,
        pets: 64
      },
      {
        group: "HI",
        year: 1998,
        males: 13,
        females: 45,
        pets: 72
      },
    ]
    
    const keys = ["males", "females", "pets"];
    const colors = {
      males: "blue",
      females: "pink",
      pets: "green",
      TX: "red",
      HI: "purple",
      CA: "yellow"
    };
    
    // Width and height of our original box
    const width = 1000;
    const height = 1000;
    // Margin we want, and making the center SVG to hold our graph
    const margin = {
      top: 80,
      right: 180,
      bottom: 80,
      left: 180
    };
    const padding = 0.1;
    
    const dataByState = d3.group(data, d => d.group)
    const dataByYear = d3.group(data, d => d.year)
    
    // Creating a function to create layers
    const stackGenerator = d3.stack().keys(keys); // now a function
    // Creating layers from our data and keys
    // keys = our layers
    const layers = stackGenerator(data); // now a function
    
    
    // Origin of an SVG is in the TOP LEFT corner
    const svg = d3
      .select("#test")
      .append("svg") // append an svg element to our div#test
      // Creating the actual height and width of our svg to hold report
      .attr("height", height - margin.top - margin.bottom)
      .attr("width", width - margin.left - margin.right)
      .attr("viewBox", [0, 0, width, height])
      .style("background-color", "white")
    
    
    // Create an outer axis that we will use to group initially
    const outerGroupXScale = d3.scaleBand()
        .domain(dataByState.keys())
        .range([margin.left, width - margin.right])
        .padding(0.05)
    
    const outerGroupXAxis = g => {
      // bottom align it
      g.attr("transform", `translate(0, ${height - margin.bottom/2})`)
        .attr("class", "x-axis")
        .call(d3.axisBottom(outerGroupXScale))
        .call(g => {
          g.selectAll(".tick>line").remove();
        })
        .call(g => g.select(".domain").attr("d", "M180,0H820"))
        .attr("font-size", "12px");
    };
    
    // Create an inner axis that we will use inside the outer group. Note that the width is the outer scale bandwidth
    // and this scale is not concerned with the entire graph width.
    const innerGroupXScale = d3.scaleBand()
        .domain(dataByYear.keys())
        .range([0, outerGroupXScale.bandwidth()])
        .padding(0.05)
    
    const innerGroupXAxis = g => {
      // bottom align it
      g.attr("transform", `translate(0, ${height - margin.bottom})`)
        .attr("class", "x-axis")
        .call(d3.axisBottom(innerGroupXScale))
        .attr("font-size", "12px");
    };
    
    // looking at second value / y value
    const extent = [
      0.9 * d3.min(layers, layer => d3.min(layer, d => d[1])),
      1.1 * d3.max(layers, layer => d3.max(layer, d => d[1])),
    ];
    const [yMin, yMax] = extent;
    const yScale = d3
      .scaleLinear()
      .domain(extent)
      .range([height - margin.bottom, margin.top]); // range from bottom up
    
    const yAxis = g => {
      g.attr("transform", `translate(${margin.left - 20}, 0)`)
        .attr("class", "y-axis")
        .call(d3.axisLeft(yScale))
        .call(g => g.selectAll(".domain").remove())
        .call(g => {
          g.selectAll(".tick>line")
            .attr("x2", -50)
            .attr("x1", -34);
        })
        .attr("font-size", "12px");
    };
    
    const yAxisLabel = g => {
      g.append("text")
        .attr("text-anchor", "start")
        .attr("fill", "black")
        .attr("font-size", "12px")
        .attr("x", -40)
        .attr("y", height - 60)
        .text("UNITS");
    };
    
    
    // create the initially grouping by binding to the data grouped by state
    var stateG = svg.selectAll(".state")
      .data(dataByState)
      .join("g")
      .attr("class", "state")
      .attr("fill", d => colors[d[0]])
      .attr("transform", d => `translate(${outerGroupXScale(d[0])}, 0)`)
    
    
    // draw the inner x axis on the state group because we will have one per state group
    stateG.append("g").attr("class", "stateAxis").call(innerGroupXAxis);
    
    
    // create the year groups inside the initial grouping of states and offset them
    // based on which state they belong to
    var yearG = stateG.selectAll(".yearG")
      .data(d => {
        const filteredByState = data.filter(i => i.group === d[0])
        const groupedByYear = d3.group(filteredByState, a => a.year)
        return groupedByYear
      })
      .join("g")
      .attr("class", "yearG")
      .attr("transform", d => {
        return `translate(${innerGroupXScale(d[0])}, 0)`
      })
    
    // for each year put down your layers
    yearG.selectAll(".layers")
      .data(d => {
        return stackGenerator(d[1])
      })
      .join("rect")
      .attr("class", "layers")
      .attr("y", d => yScale(d[0][1]))
      .attr("fill", d => colors[d.key])
      .attr("width", d => innerGroupXScale.bandwidth())
      .attr("height", d => {
        const lower = d[0][0]
        const upper = d[0][1];
        const firstBarAdjustment = lower === 0 ? yMin : 0;
        return yScale(lower + firstBarAdjustment) - yScale(upper);
      })
    
    svg.append("g").call(outerGroupXAxis);
    svg.append("g")
      .call(yAxis)
      .call(yAxisLabel);
    
    svg.node();
    
    

    主要思想是,您需要一个用于外部分组(在本例中为状态)的主要 x 比例,然后是在使用外部 x 比例的带宽的内部分组(在本例中为年份)上缩放的 x 比例作为它的范围。

    一旦你有了两个 x 刻度,剩下的就是你正常的 d3 数据绑定模式。所以在你的例子中,步骤是:

    1. 将按状态分组的数据绑定到 html 组标签,并通过外部 x 比例偏移 x 坐标
    2. 对于每个州组,过滤到正确的州,然后按年份分组。通过 html 组标签在此处创建一些子组,并根据内部 x 比例偏移每个子组的 x 坐标。
    3. 为每个状态组创建内部 x 轴
    4. 对于每一年组,调用您的分层函数并使用内部 x 刻度创建堆叠条形图。

    请参阅上面链接的 jsfiddle 以获取示例的工作版本。请注意,在您的图表中,它会跳过空列,这使得这对用户来说更加棘手且可读性较差,因为 x 轴将不一致。如果您真的想这样做,则必须通过循环为每组分组数据创建独立的 x 比例。

    【讨论】:

    • 非常感谢!你太棒了。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-01-02
    • 1970-01-01
    • 2012-11-09
    • 2020-11-10
    • 2019-08-27
    • 1970-01-01
    相关资源
    最近更新 更多