Is there a way to put x-axis and y-axis labels on a Time-based chart?

I need to put x and y-axis labels on Time-based charts. There is no way to do so when using the ‘Simple’ or ‘SQL’ config options.

This is for a Ruby on Rails 6 setup.

This is the first time trying to create charts, and I would really appreciate any suggestions.

Thanks!

Hey @muz, and welcome back to our community :wave:

This is indeed not supported out of the box, so I’m pushing this as a suggestion on our product board.

The only workaround I can think of would be to create a Smart Chart, which may be a bit code heavy but would allow more chart customization in a long term.

Using an API-based chart wouldn’t allow to add label on axis, but let you have more control on the data if required.

Let me know if that helps

Thanks @jeffladiray, I appreciate it.

I also looked into using Smart Chart to do that, but unfortunately there isn’t an example implementation of a Time-based chart. So I am now looking trying to quickly learn D3.js and use the other examples as a guide to create the time-based smart chart.

I also need to use data from our transaction model, and I am guessing I need to create a forest route, something like:
get ‘/stats/transactions-per-day’ => ‘charts#transactions_per_day’

Am I on the right path?

Edit:

I think there is a typo in the Time-based API-based Chart Rails example code (charts_controller.rb):

else
++entry[:values][:value]
end

should be:

else
entry[:values][:value] += 1
end

I think that is why the example chart has a flat line?

Thanks for all your help!

Hi @jeffladiray,

So I tried to create a time-based (line chart) smart chart, but I wasn’t able to get it to work.

I used the example bar chart as a template, and here is what I have so far:

import Component from '@glimmer/component';
import { loadExternalStyle, loadExternalJavascript } from 'client/utils/smart-view-utils';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';

export default class extends Component {
   constructor(...args) {
    super(...args);

    this.loadPlugin();
  }

  @tracked chart;
  @tracked loaded = false;

  async loadPlugin() {
    await loadExternalJavascript('https://d3js.org/d3.v6.min.js');

    this.loaded = true;
    this.renderChart()
  }

  async fetchData() {
    const response = await this.lianaServerFetch.fetch('/transactions');
    const data = await response.json();
    return data;
  }

  @action
  async renderChart() {
    if (!this.loaded) { return; }

    const color = 'steelblue';
    
    // Don't comment the lines below if you want to fetch data from your Forest server
    // const usersData = await this.fetchData();
    const data = await this.fetchData();
    // const data = Object.assign(usersData.sort((a, b) => d3.descending(a.points, b.points)), {format: "%", y: "↑ Frequency"})
    
// const data = 
// [{label: "15/11/21", value: 2},
// {label: "05/12/21", value: 1},
// {label: "14/12/21", value: 1},
// {label: "13/12/21", value: 1},
// {label: "12/12/21", value: 3}];

    const height = 500;
    const width = 800;
    const margin = ({top: 30, right: 0, bottom: 30, left: 40})

    const x = d3.scaleTime()
      .domain(d3.range(data.length))
      .range([margin.left, width - margin.right])
      .padding(0.1)
    const y = d3.scaleLinear()
      .domain([0, d3.max(data, d => d.value)]).nice()
      .range([height - margin.bottom, margin.top])
      
    const valueline = d3
      .line()
      .x((d) => { return x(d.label); })
      .y((d) => { return y(d.value); })
      .curve(d3.curveCardinal);

    const xAxis = g => g
    .attr("transform", `translate(0,${height - margin.bottom})`)
    .call(d3.axisBottom(x).tickFormat(i => data[i].label).tickSizeOuter(0))

    const yAxis = g => g
    .attr("transform", `translate(${margin.left},0)`)
    .call(d3.axisLeft(y).ticks(null, data.format))
    .call(g => g.select(".domain").remove())
    .call(g => g.append("text")
        .attr("x", -margin.left)
        .attr("y", 10)
        .attr("fill", "currentColor")
        .attr("text-anchor", "start")
        .text("↑ Transactions")
        .attr("dy", "1em"))

    const svg = d3.create("svg")
        .attr("viewBox", [0, 0, width, height]);

    svg.append("g")
      .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
      .attr("fill", color);
      // .data(data);
        
            
    const linePath = svg
        .append("path")
        .data(data)
        .attr("class", "line")
        .attr("d", valueline)
    
    const pathLength = linePath.node().getTotalLength();
    
    linePath
        .attr("stroke-dasharray", pathLength)
        .attr("stroke-dashoffset", pathLength)
        .attr("stroke-width", 3)
        .attr("stroke-width", 0)
        .attr("stroke-dashoffset", 0);
    

    svg.append("g").call(xAxis);

    svg.append("g").call(yAxis);

    this.chart = svg.node();
  }
}

Any ideas as to what I am doing wrong?

Thanks again!

Edit:
I found this tutorial very helpful:
“D3.js Line Chart Tutorial”

Hey @muz

Sorry for the delayed response.
After your edit, did you manage to resolve your issue ?

Hi @jeffladiray

Unfortunately I am still not able to get it to work. Any ideas?

You should have an error in your browser console indicating the issue. Would you mind sharing it here so I can investigate on my end ?

You are right, there is an error and I am trying to find the relevant error, and will post as soon as I find it.

Thanks.

1 Like

@jeffladiray
Is it possible to do a screen share? I have a couple of questions that could help me clarify this?

@muz,

Our community support is mainly done here in order to centralize the knowledge around the issues/potentials fixes around our product. Sorry about that.

I’m currently trying to take the code your provided to see if I’m able to have a clearer idea about that.
If you find the relevant error (Which should be the last one once you clicked on “Run”), share it here so I can have a look :pray:

Totally understand, and would have posted the solution here in any case.

"Uncaught (in promise) " error here:

_createClass(_class2, [{
key: “loadPlugin”,
value: async function loadPlugin() {
await (0, _smartViewUtils.loadExternalJavascript)(‘https://d3js.org/d3.v6.min.js’);
this.loaded = true;
this.renderChart();
}

It looks like you are missing a service import (Most likely an issue in our documentation as well, I’ll check)

@service lianaServerFetch

Since I don’t have access to your transactions data I can’t really check, but at least that’s a good start :slight_smile:

I added the service import and still seeing the uncaught (in promise) error at the same spot.

Hi @muz :wave: You can take exemple of this code to achieve your goal

import Component from '@glimmer/component';
import { loadExternalStyle, loadExternalJavascript } from 'client/utils/smart-view-utils';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';

export default class extends Component {
  @service lianaServerFetch;

   constructor(...args) {
    super(...args);

    this.loadPlugin();
  }

  @tracked chart;
  @tracked loaded = false;

  async loadPlugin() {
    await loadExternalJavascript('https://d3js.org/d3.v6.min.js');

    this.loaded = true;
    this.renderChart()
  }

  async fetchData() {
    const response = await this.lianaServerFetch.fetch('/forest/transactions');
    const data = await response.json();
    return data;
  }

  @action
  async renderChart() {
    if (!this.loaded) { return; }

    const color = 'steelblue';
    
    const parseDate = d3.timeParse("%m/%d/%Y");
    const formatDate = d3.timeFormat("%b %d");

    // const data = await this.fetchData();

    const data = [
      {label: parseDate("11/15/2021"), value: 2},
      {label: parseDate("12/05/2021"), value: 1},
      {label: parseDate("12/12/2021"), value: 3},
      {label: parseDate("12/13/2021"), value: 1},
      {label: parseDate("12/14/2021"), value: 1},
    ];
   
    const height = 500;
    const width = 800;
    const margin = ({top: 30, right: 0, bottom: 30, left: 40})

    const x = d3.scaleTime()
      .domain([d3.min(data, d => d.label), d3.max(data, d => d.label)])
      .range([margin.left, width - margin.right])
    const y = d3.scaleLinear()
      .domain([0, d3.max(data, d => d.value)]).nice()
      .range([height - margin.bottom, margin.top])

    const valueline = d3
      .line()
      .x((d) => { return x(d.label); })
      .y((d) => { return y(d.value); })
      .curve(d3.curveCardinal);

    const svg = d3
      .create("svg")
      .attr("width", width + margin.left + margin.right)
      .attr("height", height + margin.top + margin.bottom)
    
    const linePath = svg
      .append("path")
      .data([data])
      .attr("class", "line")
      .attr("d", valueline)
    
    const pathLength = linePath.node().getTotalLength();
    
    linePath
      .attr("stroke-dasharray", pathLength)
      .attr("stroke-dashoffset", pathLength)
      .attr("stroke-width", 3)
      .attr("stroke-width", 0)
      .attr("stroke-dashoffset", 0);
    
    const xAxis = g => g
    .attr("transform", `translate(0,${height - margin.bottom})`)
    .call(d3.axisBottom(x).tickFormat(i => formatDate(i)).tickSizeOuter(0));

    const yAxis = g => g
    .attr("transform", `translate(${margin.left},0)`)
    .call(d3.axisLeft(y).ticks(null, data.format))
    .call(g => g.select(".domain").remove())
    .call(g => g.append("text")
    .attr("x", -margin.left)
    .attr("y", 10)
    .attr("fill", "currentColor")
    .attr("text-anchor", "start")
    .text("↑ Transactions")
    .attr("dy", "1em"))

    svg.append("g").call(xAxis);

    svg.append("g").call(yAxis);

    this.chart = svg.node();
  }
}

Hi @Arnaud_Moncel,

I will give that a try and will let you know how it goes.

Thanks!

Hi @Arnaud_Moncel

I apologize for the delayed response.

The code above was missing the import component line at the top:

import Component from '@glimmer/component';

and after adding that it produced a chart, but not a decent one. It is a good starting point though, and I will keep working on it until I get a good one, and will post the final result here.

Thank you for all your help, I really appreciate it!

1 Like