Pseudo Logarithmic Scales

I’ve been working on custom functions to provide pseudo logarithmic scales for a d3 line chart. In my implementation you pass a split point then the lower half is log and the upper half linear. This is useful for visualising frequency response graphs.

WhatsApp Image 2024-09-18 at 01.05.27_36e423cb

I’ve been struggling to implement zoom functionality. It zooms at the minute but doesn’t pan as the visual range of each half needs adjusting based on the new location of the split point. This isn’t a necessity, but would be nice if anyone has the time to look at it.

This is the code I’ve got so far:

// Create Pseudo Logarithmic Scale
// -------------------------------
function pseudoLogScale(splitPoint = 1000) {
    let domain = [20, 20000];  // Default domain with split at 1000
    let range = [0, 100];      // Default range

    // Core scale function that handles value conversion based on domain and range
    function scale(value) {
        const log = d3.scaleLog()
            .domain([domain[0], splitPoint])
            .range([range[0], (range[0] + range[1]) / 2]);
        const linear = d3.scaleLinear()
            .domain([splitPoint, domain[1]])
            .range([(range[0] + range[1]) / 2, range[1]]);

        return value <= splitPoint ? log(value) : linear(value);
    }

    // Method to set/get domain
    scale.domain = function(_) {
        if (!arguments.length) return domain;
        domain = _;
        return scale;
    };

    // Method to set/get range
    scale.range = function(_) {
        if (!arguments.length) return range;
        range = _;
        return scale;
    };

scale.rescaleX = function(transform) {
    // Always use the original domain for the transformation
    const originalLogDomain = [20, splitPoint];  // Assuming 20 is the lower bound of the original domain
    const originalLinearDomain = [splitPoint, 20000];  // Assuming 20000 is the upper bound of the original domain

    const log = d3.scaleLog()
        .domain(originalLogDomain)
        .range([range[0], (range[0] + range[1]) / 2]);

    const linear = d3.scaleLinear()
        .domain(originalLinearDomain)
        .range([(range[0] + range[1]) / 2, range[1]]);

    // Apply the transform to both sections (log and linear)
    const newLogDomain = transform.rescaleX(log).domain();
    const newLinearDomain = transform.rescaleX(linear).domain();

    console.log("Original log domain:", originalLogDomain);
    console.log("Transformed log domain:", newLogDomain);
    console.log("Original linear domain:", originalLinearDomain);
    console.log("Transformed linear domain:", newLinearDomain);

    // Ensure the splitPoint remains constant and clamp the values based on the transformed original domain
    const clampedLogMin = Math.max(newLogDomain[0], originalLogDomain[0]);
    const clampedLogMax = Math.min(newLogDomain[1], splitPoint);
    const clampedLinearMin = Math.max(newLinearDomain[0], splitPoint);
    const clampedLinearMax = Math.min(newLinearDomain[1], originalLinearDomain[1]);

    console.log("Clamped log domain min:", clampedLogMin);
    console.log("Clamped log domain max:", clampedLogMax);
    console.log("Clamped linear domain min:", clampedLinearMin);
    console.log("Clamped linear domain max:", clampedLinearMax);

    // Update the domain with the new values
    domain = [clampedLogMin, clampedLinearMax];

    console.log("Updated domain after clamping:", domain);

    // Return the updated scale
    return scale.domain([domain[0], domain[1]]);
};


// Generate grid lines for both log and linear sections
scale.gridLines = function(transform = null) {
    const domainMin = domain[0];
    const domainMax = domain[1];

    if (transform) {
        // Log the current transform details
        console.log("Transform details:", transform);
        console.log("Scale factor (k):", transform.k);
        console.log("Translate X:", transform.x);
        console.log("Translate Y:", transform.y);
        
        // Use custom rescaleX to update the domain
        console.log("Before custom rescaleX, domain:", domain);
        scale.rescaleX(transform);  // This updates the domain
        console.log("After custom rescaleX, updated domain:", domain);
    }

    if (transform) {
        // Log the domain before applying the transformation
        console.log("Before custom rescaleX, domain:", domain);
        // Use custom rescaleX to update the domain
        scale.rescaleX(transform);
        // Log the updated domain after the transformation
        console.log("After custom rescaleX, updated domain:", domain);
    }

    // Generate grid lines for the log section
    const logGridLines = [];
    for (let base = 10; base < splitPoint; base *= 10) {
        for (let i = 1; i <= 10; i++) {
            const logValue = base * i;
            if (logValue >= domain[0] && logValue <= splitPoint) {
                logGridLines.push(logValue);
            }
        }
    }

    console.log("Generated log grid lines:", logGridLines);

    // Generate grid lines for the linear section
    const linearGridLines = d3.scaleLinear()
        .domain([splitPoint, domain[1]])
        .ticks(4);

    console.log("Generated linear grid lines:", linearGridLines);

    // Combine log and linear grid lines
    const combinedGridLines = Array.from(new Set(logGridLines.concat(linearGridLines)));

    console.log("Combined grid lines:", combinedGridLines);

    return combinedGridLines;
};


    // Generate only preferred ticks for the log and linear sections
    scale.ticks = function(count) {
        const domainMin = domain[0];
        const domainMax = domain[1];

        let ticks = [domainMin];

        // Add preferred log ticks
        const preferredLogTicks = [100, 200, 500, 1000].filter(d => d > domainMin);
        ticks = ticks.concat(preferredLogTicks);

        // Generate linear ticks
        const linearTicks = d3.scaleLinear()
            .domain([splitPoint, domainMax])
            .ticks(count - preferredLogTicks.length - 1);

        ticks = ticks.concat(linearTicks);
        ticks = Array.from(new Set(ticks)).sort((a, b) => a - b);

        return ticks;
    };

    // Method to format ticks
    scale.tickFormat = function() {
        return function(d) {
            if (d <= splitPoint) {
                return kFormatter(parseFloat(d.toFixed()));
            }
            return kFormatter(d);
        };
    };

    // Invert method
    scale.invert = function(value) {
        const log = d3.scaleLog()
            .domain([domain[0], splitPoint])
            .range([range[0], (range[0] + range[1]) / 2]);

        const linear = d3.scaleLinear()
            .domain([splitPoint, domain[1]])
            .range([(range[0] + range[1]) / 2, range[1]]);

        return value <= (range[0] + range[1]) / 2 ? log.invert(value) : linear.invert(value);
    };

    // Method to create a copy of the scale
    scale.copy = function() {
        return pseudoLogScale(splitPoint).domain(domain).range(range);
    };

    return scale;
}

To create an x scale use x = pseudoLogScale() you can assign your own domain and range like this:

.domain([20, 20000])
.range([marginLeft, width - marginRight]);

In zoomed use:

new_x = x.rescaleX(transform);  // Use the custom rescaleX method for pseudo-log

Then this to update the grid and xaxis:

 // Pseudo-log scale
        // Update the x-axis grid lines (logarithmic section)
        xAxisGrid.call(d3.axisBottom(new_x)
            .tickValues(new_x.gridLines(transform))  // Pass the transform for dynamic grid lines
            .tickSize(originalXAxisTickSize)  // Draw grid lines
            .tickFormat(""));  // No labels for the grid lines

        // Update the x-axis with preferred tick values
        xaxis.call(d3.axisBottom(new_x)
            .ticks(8)  // Use the 8 tick count for the overall axis
            .tickFormat(new_x.tickFormat()));  // Use the embedded tick formatter for labels

Many thanks

It’s an interesting problem. But, would you mind opening a new GitHub discussion instead, we’re trying to move the conversation about D3 to a central place. Thanks!

Thanks Fil I’ve done that now. Pseudo Logarithmic Scales · d3/d3 · Discussion #3920 · GitHub