Separate scales for small multiple line charts

Explore the notebook here.

I am currently developing a small multiple line chart using d3.v5 on Observable, with the dataset organized as follows:

https://i.sstatic.net/QQblnm.png

When visualizing, the y scale utilizes the num values from the values array for the domain. With unique key values in different rows, I aimed to create distinct small multiples. The provided image displays the first key.

Upon completing the visualization of the small multiples, I observed that all the line charts share the same y scale, contrary to my initial intention. The current code snippet for this is as follows:

  const y_scale = d3
    .scaleLinear()
    .domain([0, d3.max(series, d => d3.max(d.values, m => m.num))])
    .range([width/2, width/2 - start_y - margin.bottom]);

Is there a method to adjust the domain so that each individual chart can have its own scale based on its respective num values?

Edit 1: Added link to the notebook at the beginning.

Answer №1

If you're looking for a more idiomatic D3 solution, consider utilizing local variables. Of course, there are various other effective alternatives available as well.

To implement local variables, start by declaring them:

const localScale = d3.local();
const localLine = d3.local();

Next, assign the different scales within the "enter" selection:

var enter = my_group
    .enter()
    .append("g")
    .attr("class", "chart_group")
    .each(function(d) {
        const yScale = localScale.set(this, d3
            .scaleLinear()
            .domain([0, d3.max(d.values, d => d.num)])
            .range([panel_width / 2, panel_width / 2 - start_y - margin]));

        localLine.set(this, d3
            .line()
            .x(d => x_scale(d.date))
            .y(d => yScale(d.num)));
    });

Finally, retrieve those scales:

sub_group
    .select(".chart_line")
    .attr("d", function(d) {
        return localLine.get(this)(d)
    })

Here is the full code snippet ready to be copied and pasted into your notebook:

chart = {
    const panels_per_row = 4;
    const panel_width = (width - margin * 8) / panels_per_row;
    const height =
        margin + (panel_width + margin) * (parseInt(my_data.length / 2) + 1);

    const svg = d3.create("svg").attr("viewBox", [0, 0, width, height]);
    const start_x = 2;
    const start_y = panel_width / 3 + margin;

    const x_scale = d3
        .scaleBand()
        .domain(d3.set(series[0].values, d => d.date).values())
        .range([0, panel_width]);
    const localScale = d3.local();
    const localLine = d3.local();

    //join
    var my_group = svg.selectAll('.chart_group').data(series, d => d.key);

    //exit and remove
    my_group.exit().remove();
    //enter new groups
    var enter = my_group
        .enter()
        .append("g")
        .attr("class", "chart_group")
        .each(function(d) {
            const yScale = localScale.set(this, d3
                .scaleLinear()
                .domain([0, d3.max(d.values, d => d.num)])
                .range([panel_width / 2, panel_width / 2 - start_y - margin]));

            localLine.set(this, d3
                .line()
                .x(d => x_scale(d.date))
                .y(d => yScale(d.num));
        });

    //append elements to new group
    enter.append("rect").attr("class", "group_rect");
    enter.append("text").attr("class", "group_text");
    enter.append("g").attr("class", "sub_chart_group");

    //merge
    my_group = my_group.merge(enter);

    position_group_elements(my_group);

    //join
    var sub_group = my_group
        .select(".sub_chart_group")
        .selectAll('.sub_chart_elements_group')
        .data(d => [d.values]); 

    //exit and remove
    sub_group.exit().remove();
    //enter new groups
    var sub_enter = sub_group
        .enter()
        .append("g")
        .attr("class", "sub_chart_elements_group");

    //append elements to new group
    sub_enter.append("path").attr("class", "chart_line");

    //merge
    sub_group = sub_group.merge(sub_enter);

    sub_group
        .select(".chart_line")
        .attr("d", function(d) {
            return localLine.get(this)(d)
        })
        .attr("fill", "none")
        .attr("stroke", "black")
        .attr("stroke-width", 1)
        .attr("transform", "translate(" + start_x + "," + start_y + ")");

    function position_group_elements(my_group) {
        //position rectangle
        my_group
            .select(".group_rect")
            .attr("x", function(d, i) {
                var position = i % panels_per_row;
                d.x_pos = position * (panel_width + margin) + margin;
                d.y_pos =
                    parseInt(i / panels_per_row) * (panel_width + margin) + margin;
                return d.x_pos;
            })
            .attr("y", d => d.y_pos)
            .attr("fill", "#eee")
            .attr("stroke", "#aaa")
            .attr("stroke-width", 1)
            .attr("width", panel_width)
            .attr("height", panel_width);

        //then position sub groups
        my_group
            .select(".sub_chart_group")
            .attr("id", d => d.key)
            .attr("transform", "translate(" + d.x_pos + "," + d.y_pos + ")");
    }

    return svg.node();
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Adding a Scrollbar to an Extensive Organizational Chart Made with react-d3-tree

Utilizing the react-d3-tree library, I have successfully implemented an organizational chart (org chart) within my React application. The org chart functions well with smaller datasets, but as the organization expands, I am encountering the challenge of ac ...

The server is constantly sending data through Server Sent Events

I am currently a student working on a project involving a social trading platform. I am looking to incorporate a notification system and believe that SSE would be a great fit for this purpose. However, I am facing an issue where my SSE code is sending data ...

Incorrect .offset().top calculation detected

Within a webpage, I have multiple sections. Positioned between the first and second section is a navbar menu. As the user scrolls down and the navbar reaches the top of the page, a function is triggered to fix it in place at the top. While this functionali ...

Unusual behavior experienced with raycasting in Three JS when objects are are repositioned

Software Versions THREE.ObjectLoader2: 2.4.1 THREE.LoaderSupport.MeshBuilder: 1.2.1 THREE.LoaderSupport.WorkerSupport: 2.2.0 THREE.WebGLRenderer: 93 THREE.REVISION: 93 Anomalies Discovered During a raycast operation on objects within my scene, I encount ...

Node.js client encounters ENOBUFS error due to excessive number of HTTP requests

Currently, I have the following setup: An end-to-end requests system where a node.js client communicates with a node.js server. However, the issue arises when the client fails with an ENOBUFS error in less than a minute. client: (function(){ var lo ...

how to assign a value to a field automatically in PHP

Is it possible to send values such as name, email, and contact number from one URL like to another URL at ? I would like these values to be automatically displayed on the form of the target URL (http://www.otherurl.com/test.php). I do not have access to ...

Detect keypress within a BrowserWindow even when multiple BrowserView components are present

In my Electron application, I have a main BrowserWindow that contains the main user interface index.html, along with multiple BrowserView elements: browserWindow = new BrowserWindow({ width: width, height: height, frame: false }); browserWindow.webContents ...

Using v-bind:class in Vue.js does not successfully assign a value in the method

Why is the width of my div not changing when I try to bind it to a data attribute that ranges from 0 to 100? <div class="bar" :style="{ width: percentage + '%' }"></div> <script> export default { name: 'app&ap ...

Vue snapshot testing is encountering a failure with the error message "TypeError: Cannot read property of undefined"

Everything seems to be working fine with the component on the page without any errors. Storybook is also functioning well, but the problem lies in the unit test. import { mount } from '../../vue'; import { createLocalVue } from '@vue/test-u ...

Trapped in a Continuous Observing Loop with MdSnackBar in Angular Material within Angular 2

Whenever my login attempt fails, I want to display a snackbar with the message 'error connecting'. After dismissing the snackbar, I would like the login to be retried after 10 seconds. However, I'm facing an issue where my observable is runn ...

What is the reason behind Express exporting a function instead of an object in the initial stages?

In Node.js, when utilizing express, we start by using const express = require('express') to bring in the express module, which will then yield a function. Afterward, we proceed with const app = express() My inquiry is as follows: What exactly ...

The content has been successfully loaded using ajax, but it is not displaying

I've been experimenting with djax and have noticed that when I click on an anchor tag, the URL changes as expected. However, even though the page source reflects this change, the contents of the page itself remain unchanged. Any thoughts on why this m ...

Loading CSS files conditionally in Angular2's index.html

Currently, my index.html page features a dark theme: <base href="/"> <html> <head> <title>XXX</title> </head> <body> <link rel="stylesheet" type="text/css" href="assets/dark_room.css"> <my-app ...

Vue-Router 4 now automatically redirects the default URL to a "404 Page Not Found

I recently decided to upgrade my Vue application from version 2 to version 3, following the official Vue migration documentation. One of the changes I made was updating the vue-router package. However, after updating my router.js file, I noticed that when ...

Activate the submission button on AngularJS once a correctly formatted email is provided

Currently working on an AngularJS demo to better understand its functionalities. The next task on my list is to enable the submit button only when a valid email address is entered. I'm seeking guidance on how to approach this and what concepts I need ...

Determine the total of all the values displayed in the footer of a jQuery

I am looking to show the total amount in the footer of a jquery datatable. Below is a snapshot of my datatable: https://i.stack.imgur.com/z01IL.png Here is the code snippet for my jquery datatable: for (var i = 0; i < length; i++ ) { var patient = ...

The condition for the result of a jQuery $.post call

I've customized this code obtained from the jQuery website by incorporating a condition into it. However, regardless of the outcome, it consistently enters the first 'if' statement. How can I ensure that my condition is functioning correctly ...

Troubleshooting: Vue JS failing to recognize new objects and properties in HTML

Within my Vue instance, I have a method named `loadPlannedAlerts` that performs an AJAX call to retrieve JSON data. This method is called during the `mounted` lifecycle hook of my Vue JS instance. The retrieved JSON object consists of key-value pairs struc ...

Using Ajax and PHP to Trigger a Forced Download

I am trying to develop a download script that enables the Force Download of JPGs. Below is my PHP script: <?php header("Pragma: public"); // required header("Expires: 0"); header("Cache-Control: must-revalidate, post-check=0, pre-check=0"); ...

Navigate your way with Google Maps integrated in Bootstrap tabs

Trying to display a Google Map in a vanilla Bootstrap tab has been quite the challenge. I created a fiddle based on the Bootstrap docs, and followed Google's instructions for the Gmap script. The map object appears initialized when checking console.di ...