Ben Frain Ben Frain - 1 year ago 99
Javascript Question

d3 v4 why are there duplicate ticks on the X Axis (how to work tickValues)?

Here is my reduction:



var CreateChart = (function() {
var chtChartConstrictor = document.getElementById("chtChartConstrictor");
var myChart;

var drawChart = function(options) {
// Set dimensions and margins of the graph
var margin = {
top: 20,
right: 0,
bottom: 0,
left: 0,
};
var chtChartConstrictorMetrics = chtChartConstrictor.getBoundingClientRect();
var width = chtChartConstrictorMetrics.width;
var height = chtChartConstrictorMetrics.height;

var parseTime = d3.timeParse("%Y-%m-%d");
var formatTime = d3.timeFormat("%Y-%m-%d");
// format the data
options.line1.forEach(function(d) {
d.date = parseTime(d.date);
d.close = +d.close;
});
// parse the date / time

if (options.chartType === "line") {

// set the ranges
let x = d3.scaleTime().range([0, width]).domain(
d3.extent(options.line1, function(d) {
return d.date;
})
);
let y = d3.scaleLinear().range([height - margin.top - margin.bottom, 0]).domain([
0,
d3.max(options.line1, function(d) {
return d.close;
}),
]);

// define the area
var area = d3
.area()
.x(function(d) {

return x(d.date);
})
.y0(height)
.y1(function(d) {

return y(d.close);
});
// define the line
var valueline = d3
.line()
.x(function(d) {

return x(d.date);
})
.y(function(d) {
return y(d.close);
});

var svg = d3
.select("#chtChartConstrictor")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("path").attr("class", "cht-LineChart_Line").attr("d", valueline(options.line1));
// add the area
svg.append("path").data(options.line1).attr("class", "cht-LineChart_Fill").attr("d", area(options.line1));

// add the X Axis
svg.append("g").attr("class", "cht-LineChart_XAxis").attr("transform", "translate(0," + (height - 40) + ")").call(customXAxis);
function customXAxis(g) {
g.call(d3.axisBottom(x).tickFormat(d3.timeFormat("%Y-%m-%d")));
g.select(".domain").remove();
g.selectAll("line").remove();
g.selectAll("text").attr("class", "cht-LineChart_XAxisNumber");
}
// add the Y Axis
svg.append("g").attr("class", "cht-LineChart_YAxis").attr("transform", "translate(-5,0)").call(customYAxis);
function customYAxis(g) {
g.call(d3.axisRight(y));
g.select(".domain").remove();
g.selectAll("line").remove();
g.select(".cht-LineChart_YAxis text:first-of-type").remove();
g.selectAll("text").attr("class", "cht-LineChart_YAxisNumber");
}

// Gridline
var gridlines = d3.axisLeft().tickFormat("").tickSize(-width).scale(y);

svg.append("g").attr("class", "cht-LineChart_GridLineContainer").call(gridlines);
svg.selectAll(".cht-LineChart_GridLineContainer line").attr("class", "cht-LineChart_GridLine");
// Tidy up the generation by removing the text and path as we don't use them
svg.selectAll(".cht-LineChart_GridLineContainer text").remove();
svg.selectAll(".cht-LineChart_GridLineContainer path").remove();
}
};

var destroyChart = function() {};

var publicAPI = {
DrawChart: drawChart,
DestroyChart: destroyChart,
};
return publicAPI;
})();

CreateChart.DrawChart({
chartType: "line",
line1: [
{ date: "2016-12-25", close: 43 },
{ date: "2016-12-26", close: 57 },
{ date: "2016-12-27", close: 55 },
{ date: "2016-12-28", close: 44 },
{ date: "2016-12-29", close: 45 },
{ date: "2016-12-30", close: 30 },
{ date: "2016-12-31", close: 36 },
],
line2: [],
});

#chtChartConstrictor {
height: 320px;
width: 100%;
}

.cht-LineChart_Line {
fill: transparent;
stroke: #f90;
stroke-width: 2px;
}

.cht-LineChart_Fill {
fill: rgba(73,255,255,.5);
}

.cht-LineChart_GridLine {
stroke-dasharray: 4px 4px;
stroke: #ddd;
}

<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<div id="chtChartConstrictor"></div>





I cannot fathom why I am getting multiple duplicate ticks (dates) on the X Axis? Could anyone send me on the right path? I'd like to see a tick for each of the data points and nothing more.

Answer Source

In D3, an axis built using a time scale has its ticks automatically generated. That is, the axis generator decides how many ticks it will create: they do not correspond to the data points.

That being said, the ticks you are seeing are not duplicated: they actually correspond to different moments in time... however, since some of them belong to the same day, those ticks seem to be duplicated. But they are not.

There are several ways to fix this. One of the simplest ones is using tickValues (as you suggest in your question's title) to pass only the dates in your data array:

.tickValues(options.line1.map(function(d){
    return d.date
}))

Here is your code with that change:

var CreateChart = (function() {
    var chtChartConstrictor = document.getElementById("chtChartConstrictor");
    var myChart;

    var drawChart = function(options) {
        // Set dimensions and margins of the graph
        var margin = {
            top: 20,
            right: 0,
            bottom: 0,
            left: 0,
        };
        var chtChartConstrictorMetrics = chtChartConstrictor.getBoundingClientRect();
        var width = chtChartConstrictorMetrics.width;
        var height = chtChartConstrictorMetrics.height;

        var parseTime = d3.timeParse("%Y-%m-%d");
        var formatTime = d3.timeFormat("%Y-%m-%d");
        // format the data
        options.line1.forEach(function(d) {
            d.date = parseTime(d.date);
            d.close = +d.close;
        });
        // parse the date / time

        if (options.chartType === "line") {
            
            // set the ranges
            let x = d3.scaleTime().range([0, width]).domain(
                d3.extent(options.line1, function(d) {
                    return d.date;
                })
            );
            let y = d3.scaleLinear().range([height - margin.top - margin.bottom, 0]).domain([
                0,
                d3.max(options.line1, function(d) {
                    return d.close;
                }),
            ]);

            // define the area
            var area = d3
                .area()
                .x(function(d) {
                    
                    return x(d.date);
                })
                .y0(height)
                .y1(function(d) {
                    
                    return y(d.close);
                });
            // define the line
            var valueline = d3
                .line()
                .x(function(d) {
                    
                    return x(d.date);
                })
                .y(function(d) {
                    return y(d.close);
                });

            var svg = d3
                .select("#chtChartConstrictor")
                .append("svg")
                .attr("width", width + margin.left + margin.right)
                .attr("height", height + margin.top + margin.bottom)
                .append("g")
                .attr("transform", "translate(" + margin.left + "," + margin.top + ")");
            svg.append("path").attr("class", "cht-LineChart_Line").attr("d", valueline(options.line1));
            // add the area
            svg.append("path").data(options.line1).attr("class", "cht-LineChart_Fill").attr("d", area(options.line1));

            // add the X Axis
            svg.append("g").attr("class", "cht-LineChart_XAxis").attr("transform", "translate(0," + (height - 40) + ")").call(customXAxis);
            function customXAxis(g) {
                g.call(d3.axisBottom(x)
                .tickValues(options.line1.map(function(d){return d.date}))
                .tickFormat(d3.timeFormat("%Y-%m-%d")));
                g.select(".domain").remove();
                g.selectAll("line").remove();
                g.selectAll("text").attr("class", "cht-LineChart_XAxisNumber");
            }
            // add the Y Axis
            svg.append("g").attr("class", "cht-LineChart_YAxis").attr("transform", "translate(-5,0)").call(customYAxis);
            function customYAxis(g) {
                g.call(d3.axisRight(y));
                g.select(".domain").remove();
                g.selectAll("line").remove();
                g.select(".cht-LineChart_YAxis text:first-of-type").remove();
                g.selectAll("text").attr("class", "cht-LineChart_YAxisNumber");
            }

            // Gridline
            var gridlines = d3.axisLeft().tickFormat("").tickSize(-width).scale(y);

            svg.append("g").attr("class", "cht-LineChart_GridLineContainer").call(gridlines);
            svg.selectAll(".cht-LineChart_GridLineContainer line").attr("class", "cht-LineChart_GridLine");
            // Tidy up the generation by removing the text and path as we don't use them
            svg.selectAll(".cht-LineChart_GridLineContainer text").remove();
            svg.selectAll(".cht-LineChart_GridLineContainer path").remove();
        }
    };

    var destroyChart = function() {};

    var publicAPI = {
        DrawChart: drawChart,
        DestroyChart: destroyChart,
    };
    return publicAPI;
})();

CreateChart.DrawChart({
    chartType: "line",
    line1: [
        { date: "2016-12-25", close: 43 },
        { date: "2016-12-26", close: 57 },
        { date: "2016-12-27", close: 55 },
        { date: "2016-12-28", close: 44 },
        { date: "2016-12-29", close: 45 },
        { date: "2016-12-30", close: 30 },
        { date: "2016-12-31", close: 36 },
    ],
    line2: [],
});
#chtChartConstrictor {
	height: 320px;
	width: 100%;
}

.cht-LineChart_Line {
	fill: transparent;
	stroke: #f90;
   stroke-width: 2px;
}

.cht-LineChart_Fill {
	fill: rgba(73,255,255,.5);
}

.cht-LineChart_GridLine {
    stroke-dasharray: 4px 4px;
    stroke: #ddd;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<div id="chtChartConstrictor"></div>

Be advised that this solution has its problems: if your dates are not evenly spaced (for instance, [01-Dec, 02-Dec, 03-Dec, 06-Dec, 07-Dec]), the ticks will not be evenly spaced, which is visually unpleasant.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download