javascript - 创建分组堆积条形图

标签 javascript d3.js

Stacked Bar Chart

嘿大家。再次向大家请教更多有关D3的问题。

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

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

Result I'm looking for

几个小时以来,我一直在用头撞墙,试图调整 X 轴以将它们适本地分组。谁能帮我吗?

这是我所做的一些假设

  1. 我需要两个不同的 xScale(一个用于,另一个用于)
  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>
        );
    }
}

最佳答案

这可能不是最干净的解决方案,但我之前已经这样做过并且它工作可靠。这是我刚刚拼凑在一起的代码的 super 粗略的破解。我留下了一些内嵌评论。 ( 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 数据绑定(bind)模式。因此,在您的示例中,步骤将是:

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

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

关于javascript - 创建分组堆积条形图,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/68505047/

相关文章:

javascript - 如何将 JavaScript 变量传递到 Rails Controller 中?

javascript - 如何在 d3.js 中行走一棵树

javascript - 转换条形图中的数字标签

javascript - 如何在 d3js 中创建一个又一个散点图并删除上一个散点图?

JavaScript : How to filter an array's contents into two resulting arrays

javascript - 在 React 中删除另一个对象中的一个对象

javascript - 如何使用 d3 从 JSON 数组元素渲染表格

javascript - 错误: Invalid value for <circle> attribute cx ="NaN" in dimple.js

javascript - D3 中 Force Layout 的分层边缘捆绑

javascript - 使用 JavaScript 访问 CSS3 translate 转换偏移量