Arthez Arthez - 20 days ago 7
Javascript Question

How to create x-axis with duplicated string values in d3js?

I want to create x-axis where I can set certain number of duplicated STRING values.

Here is example of data I could use for it (first letters of weekday names):

const chartData = [
{ xValue: 'M', yValue: 1 },
{ xValue: 'T', yValue: 2 },
{ xValue: 'W', yValue: 2 },
{ xValue: 'T', yValue: 4 },
{ xValue: 'F', yValue: 2 },
{ xValue: 'S', yValue: 1 },
{ xValue: 'S', yValue: 3 }];


Here is the code for creating bar-chart:

const chartTicks = 5;
const margin = { top: 30, right: 30, bottom: 50, left: 140 };
const yScaleMaxValuePercentage = 110;

const xValue = 'xValue';
const yValue = 'yValue';
const svg = d3.select('.bar-chart-horizontal');
const marginWidth = margin.left + margin.right;
const marginHeight = margin.top + margin.bottom;
const width = +svg.attr('width') - marginWidth;
const height = +svg.attr('height') - marginHeight;
const scaleX = d3.scaleBand().rangeRound([height, 0]);
const scaleY = d3.scaleLinear().rangeRound([0, width]);
const mainGroup = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
let paddingBetweenBars = 0.7;

function calculateScaleY(chartData) {
const maxValueY = d3.max(chartData, (d) => (d[yValue]));
return [0, Math.ceil(maxValueY * yScaleMaxValuePercentage / 100)];
}

function drawBarChart(chartData) {
// Calculating - X and Y axis scale
scaleY.domain(calculateScaleY(chartData));
scaleX.domain(chartData.map((d) => (d[xValue])))
.padding(paddingBetweenBars);

// Draw - Y axis
mainGroup.append('g')
.attr('class', 'axis axis--y')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(scaleY).ticks(chartTicks));

// Draw - X axis
mainGroup.append('g')
.attr('class', 'axis axis--x')
.call(d3.axisLeft(scaleX))
.append('text')
.attr('transform', 'rotate(-90)')
.attr('text-anchor', 'end');

// Draw - Bars
mainGroup.append('g')
.selectAll('.bar-chart-horizontal .bar')
.data(chartData)
.enter().append('rect')
.attr('class', 'bar')
.attr('y', (d) => (scaleX(d[xValue])))
.attr('height', scaleX.bandwidth())
.attr('x', 0)
.attr('width', (d) => (scaleY(d[yValue])));
}

drawBarChart(chartData);


CodePen: http://codepen.io/Arthe/pen/qqRpgj

My problem is, that with current version of code, it creates two bars on the same label (they overlap each other), because we got double 'S' and double 'T' in the data. I want x-axis to have two different 'S' and 'T' (7 x-axis labels instead of 5). I tried some ideas from stackoverflow, but none of the answer let you use string values on the axis. Linear scale as far as I know, don't let you use strings. If I add IDs to data and use them as sorting variables, the bars dont overlap anymore but I have IDs on the x axis instead of strings. I looked on all scales d3js can provide and I didn't find better scale for it than ordinal one.

EDIT:
Maybe its worth mentioning, that I don't know what type of strings I will get in data, and how many of them there will be, I just know that duplicated strings can't be stacked, but have separate bars.

Answer

(first solution)

A possible (out of many) solution: Use different values for the days, like this:

const chartData = [{ xValue: 'Mon', yValue: 1 },
    { xValue: 'Tue', yValue: 2 },
    { xValue: 'Wed', yValue: 2 },
    { xValue: 'Thu', yValue: 4 },
    { xValue: 'Fri', yValue: 2 },
    { xValue: 'Sat', yValue: 1 },
    { xValue: 'Sun', yValue: 3 }];

And then, in the axis generator, show just the first letter, using tickFormat and substring:

.call(d3.axisLeft(scaleX).tickFormat(d=>d.substring(0,1)))

Here is your Pen: http://codepen.io/anon/pen/zoNRYZ?editors=1010

(second solution)

Another solution is using an object to define your ticks:

var myTicks = {Mon: "M", Tue: "T", Wed: "W",Thu: "T", Fri: "F", Sat: "S", Sun: "S"};

And then, in your axis, using that object to define the ticks:

.call(d3.axisLeft(scaleX).tickFormat(d=>myTicks[d]))

Here is the demo:

const chartData = [
  { xValue: 'Mon', yValue: 1 },
   { xValue: 'Tue', yValue: 2 },
  { xValue: 'Wed', yValue: 2 },
   { xValue: 'Thu', yValue: 4 },
  { xValue: 'Fri', yValue: 2 },
   { xValue: 'Sat', yValue: 1 },
   { xValue: 'Sun', yValue: 3 }];

var myTicks = {Mon: "M", Tue: "T", Wed: "W",Thu: "T", Fri: "F", Sat: "S", Sun: "S"};
                   
const chartTicks = 5;
const margin = { top: 30, right: 30, bottom: 50, left: 140 };
const yScaleMaxValuePercentage = 110;

const xValue = 'xValue';
const yValue = 'yValue';
const svg = d3.select('.bar-chart-horizontal');
const marginWidth = margin.left + margin.right;
const marginHeight = margin.top + margin.bottom;
const width = +svg.attr('width') - marginWidth;
const height = +svg.attr('height') - marginHeight;
const scaleX = d3.scaleBand().rangeRound([height, 0]);
const scaleY = d3.scaleLinear().rangeRound([0, width]);
const mainGroup = svg.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
let paddingBetweenBars = 0.7;

function calculateScaleY(chartData) {
  const maxValueY = d3.max(chartData, (d) => (d[yValue]));
  return [0, Math.ceil(maxValueY * yScaleMaxValuePercentage / 100)];
}

function drawBarChart(chartData) {
  // Calculating - X and Y axis scale
  scaleY.domain(calculateScaleY(chartData));
  scaleX.domain(chartData.map((d) => (d[xValue])))
    .padding(paddingBetweenBars);

  // Draw - Y axis
  mainGroup.append('g')
    .attr('class', 'axis axis--y')
    .attr('transform', `translate(0,${height})`)
    .call(d3.axisBottom(scaleY).ticks(chartTicks));

  // Draw - X axis
  mainGroup.append('g')
    .attr('class', 'axis axis--x')
    .call(d3.axisLeft(scaleX).tickFormat(d=>myTicks[d]))
    .append('text')
    .attr('transform', 'rotate(-90)')
    .attr('text-anchor', 'end');

  // Draw - Bars
  mainGroup.append('g')
    .selectAll('.bar-chart-horizontal .bar')
    .data(chartData)
    .enter().append('rect')
    .attr('class', 'bar')
    .attr('y', (d) => (scaleX(d[xValue])))
    .attr('height', scaleX.bandwidth())
    .attr('x', 0)
    .attr('width', (d) => (scaleY(d[yValue])));
}

drawBarChart(chartData);
<script src="https://d3js.org/d3.v4.min.js"></script>
<div>
<svg class="bar-chart-horizontal" width="600" height="400"></svg>
</div>

(third solution)

EDIT: to answer your edit: what you're asking is not possible. If you have duplicated strings and you don't know the strings beforehand, there are few alternatives. One of them is using the indices to define your domain:

var ind = d3.range(chartData.length);
scaleX.domain(ind)

Then, in the bars:

.attr('y', (d,i) => (scaleX(i)))

And, finally, in the axis generator:

.call(d3.axisLeft(scaleX).tickFormat((d,i)=>chartData[i].xValue))

Here is another Pen: http://codepen.io/anon/pen/XNpZMg?editors=0010