2
votes

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.

2

2 Answers

3
votes

(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, in an ordinal scale two identical string input values are considered the same (the same input value will always be mapped to the same output value). 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

(Forth solution)

As this was a typical XY problem, it took me some time to realize what you really want. This forth solution is probably the best one for your porposes. You said:

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

But this is not correct. You can put anything you want on the ticks. So, this solution uses unique IDs to make the chart, but displays different ticks, based on the unique IDs. This is your data with unique IDs:

const chartData = [
    { xValue: 'M', ID: "foo", yValue: 1 },
    { xValue: 'T', ID: "bar", yValue: 4 },
    { xValue: 'W',  ID: "baz",yValue: 2 },
    { xValue: 'T',  ID: "foobar",yValue: 2 },
    { xValue: 'F',  ID: "foobaz",yValue: 2 },
    { xValue: 'S',  ID: "barfoo",yValue: 3 },
    { xValue: 'S',  ID: "barbaz",yValue: 1 }];

We use the IDs to make the chart:

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

But, on the axis generator, we use xValue to make the ticks:

.call(d3.axisLeft(scaleX).tickFormat(function(d) {
    var filtered = chartData.filter(function(e) {
        console.log(d);
        return e.ID === d
    });
    return filtered[0].xValue;
}));

Here is the corresponding Pen: http://codepen.io/anon/pen/gLgjyW?editors=0010

0
votes

My solution was to just use axis.tickFormat(d => d.slice(0,1)).

In the code from the question it would be...

mainGroup.append('g')
    .attr('class', 'axis axis--x')
    .call(d3.axisBottom(scaleX).tickFormat(d => d.slice(0,1)))

Gave me a real headache wondering what was going on, but then saw (as the above poster stated) that d3 will group inputs with the same name. So keep the name full length in your scales and just format the tick!