简体   繁体   English

创建分组堆积条形图

[英]Create Grouped Stacked Bar Chart

堆积条形图

Hey y'all.大家好。 Here again to ask more questions about D3.这里再次请教更多关于D3的问题。

I have successfully created this stacked bar chart that displays each stacked bar chart by year.我已经成功创建了这个堆积条形图,它按年份显示每个堆积条形图。 Now, I want to group these stacked bar charts together by group (CA, TX, and HI).现在,我想按组(CA、TX 和 HI)将这些堆积条形图组合在一起。

In the end, I want it to look kind of like this.最后,我希望它看起来像这样。

结果我要找

I have been banging my head against the wall for hours trying to adjust the X-Axis to group these appropriately.几个小时以来,我一直将头撞在墙上,试图调整 X 轴以适当地对这些进行分组。 Can anyone help me?谁能帮我?

Here were/are some assumptions I'm making这里有/是我正在做的一些假设

  1. I need two different xScales (one for group and then one for year )我需要两种不同的 xScales(一种用于group ,一种用于year
  2. I do not need to use d3.group or d3.nest (which isn't available in the newest version of d3 anyways) since my data is formatted in a way to have access to the group.我不需要使用d3.groupd3.nest (无论如何在最新版本的 d3 中不可用),因为我的数据格式设置为可以访问组。
  3. I will need some new padding between the groups (3 as mentioned before), and inner between the years (the first group has 3 different years for example).我需要在组之间(前面提到的 3 个)和年份之间的内部填充一些新的填充(例如,第一组有 3 个不同的年份)。

Please help me out!请帮帮我! Anything helps.什么都有帮助。

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

This might not be the cleanest solution but I have done this before and it works reliably.这可能不是最干净的解决方案,但我之前已经这样做过并且它工作可靠。 Here is super rough hack of your code that I just threw together.这是我刚刚拼凑的代码的超级粗略破解。 I left a few inline comments.我留下了一些内联评论。 ( jsfiddle ) ( 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();

The main idea is that you want a main x scale for the outer grouping (in this case states) and then an x scale that scales on the inner grouping (in this case years) that uses the bandwidth of the outer x scale as its range.主要思想是,您需要一个用于外部分组的主要 x 比例(在本例中为状态),然后在使用外部 x 比例的带宽作为其范围的内部分组(在本例中为年)上进行缩放的 x 比例.

Once you have the two x scales in place the rest of it is just your normal d3 databinding pattern.一旦你有了两个 x 尺度,剩下的就是你正常的 d3 数据绑定模式。 So in your example the steps would be:所以在你的例子中,步骤是:

  1. Bind your data grouped by state to html group tags and offset the x coordinate by the outer x scale将按状态分组的数据绑定到 html 组标签,并按外部 x 比例偏移 x 坐标
  2. For each state group, filter down to the correct state and then group by the year.对于每个州组,筛选到正确的州,然后按年份分组。 Create some sub groups here via html group tags and offset the x coordinate for each of those based off the inner x scale.通过 html 组标签在此处创建一些子组,并根据内部 x 比例偏移每个组的 x 坐标。
  3. Create the inner x axis for each of the state groups为每个状态组创建内部 x 轴
  4. For each year group call your layering function and create the stacked bar chart using the inner x scale.对于每个年份组,调用您的分层函数并使用内部 x 刻度创建堆积条形图。

See the jsfiddle I linked above for a working version of your example.有关示例的工作版本,请参阅我上面链接的 jsfiddle。 Note in your diagram it skips empty columns which makes this more tricky and less readable to a user as the x axis would not be consistent.请注意,在您的图表中,它会跳过空列,这使得这对用户来说更加棘手且可读性较差,因为 x 轴将不一致。 If you really wanted to do that you would have to create independent x scales for each set of grouped data via a loop.如果您真的想这样做,则必须通过循环为每组分组数据创建独立的 x 比例。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM