0

I've created a live poll using Chart.js and want the user to be able to select their option by selecting a checkbox to the left of the y-axis label. I'm having trouble figuring out how to do this the right way with Chart.js

My first attempt was to place the input box outside of the canvas but it's highly unreliable with dynamic poll options as they grow. It's pretty bad because the height of the bar chart will vary depending on how many options there are so the input boxes become a mess as the options displayed are dynamic.

Here is an image of what I'm trying to ultimately achieve on the front-end. If the user selects the box then the poll grows to reflect the choice the user has selected.

enter image description here

Posted below is my code of where I'm currently stuck. I've been stuck trying to figure this out for quite some time now. Any help would be greatly appreciated!

import { Component, OnInit } from '@angular/core';
// import { Chart } from 'chart.js';
import * as Ably from 'ably';
import * as Chart from 'chart.js';
import { AngularFirestore } from '@angular/fire/firestore';
import { Observable, Subject } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { FirebaseService } from '../services/firebase.service';

@Component({
  selector: 'app-vote-chart',
  templateUrl: './vote-chart.component.html',
  styleUrls: ['./vote-chart.component.scss']
})
export class VoteChartComponent implements OnInit {
  // Attributes
  ably: any
  receiveChanel: any
  chart: any
  users: any
  polls:any = [];
  poll:any;
  votes:any = [];
  labels:any = [];
  poll_type:string = "";

  constructor(db: AngularFirestore, private firebaseService: FirebaseService) {}

  ngOnInit() {
    this.firebaseService.getPoll("key").subscribe(res => {
      const poll_data:any = res.payload.data();
      this.poll = {
        id: res.payload.id,
        helper_text: poll_data.helper_text,
        poll_type: poll_data.poll_type,
        scoring_type: poll_data.scoring_type,
        user: poll_data.user.id,
        choices: []
      };
      this.poll_type = this.poll.poll_type == 2 ? "Pick One" : "Pick Two";
      this.firebaseService.getChoices('key').subscribe(res => {
        res.forEach((choice) => {
          const choice_data:any = choice.payload.doc.data()
          this.poll.choices.push({
            id: choice.payload.doc.id,
            text: choice_data.text,
            votes: choice_data.votes
          });
          this.votes.push(choice_data.votes);
          this.labels.push(choice_data.text);
        });
        console.log("Poll updated!", this.poll);
      });
    });
    this.ably = new Ably.Realtime("key")
    // Attach to channel
    this.receiveChanel = this.ably.channels.get('vote-channel12')
    // Ably subscription
    this.receiveChanel.subscribe("update", (message: any) => {

      var canvas =  <HTMLCanvasElement> document.getElementById("chartjs-2")
      var ctx = canvas.getContext("2d");

      this.chart = new Chart(ctx, {
        type: 'horizontalBar',
        data: {
          labels: this.labels,
          datasets: [{
            label: this.poll_type,
            data: this.votes,
            fill: false,
            backgroundColor: [
              "rgba(255, 99, 132, 0.2)",
              "rgba(255, 159, 64, 0.2)",
              "rgba(255, 205, 86, 0.2)",
              "rgba(75, 192, 192, 0.2)",
              "rgba(54, 162, 235, 0.2)",
              "rgba(153, 102, 255, 0.2)",
              "rgba(201, 203, 207, 0.2)"

            ],
            borderColor: [
              "rgb(255, 99, 132)",
              "rgb(255, 159, 64)",
              "rgb(255, 205, 86)",
              "rgb(75, 192, 192)",
              "rgb(54, 162, 235)",
              "rgb(153, 102, 255)",
              "rgb(201, 203, 207)"
            ],
            borderWidth: 1
          }]
        },
        options: {
          events: ["touchend", "click", "mouseout"],
          onClick: function(e) {
            console.log("clicked!", e);
          },
          tooltips: {
            enabled: true
          },
          title: {
            display: true,
            text: this.poll_type,
            fontSize: 14,
            fontColor: '#666'
          },
          legend: {
            display: false
          },
          maintainAspectRatio: true,
          responsive: true,
          scales: {
            xAxes: [{
              ticks: {
                beginAtZero: true,
                precision: 0
              }
            }]
          }
        }
      })
      console.log("Poll", this.poll);
    });
  }
}

Template code, inline styled to make it easier to review

<div id="chart_and_labels_container" style="min-width:620px !important;">
  <div style="float:left; line-height: 175px; display: inline-grid; padding-top: 55px; margin: 0;">
    <input type="checkbox" (click)="vote(1)" style="width:57px; height: 30px;"/>
    <input type="checkbox" (click)="vote(-1)" style="width:57px; margin-top: 10px; height: 30px;"/>
    <input type="checkbox" (click)="vote(-1)" style="width:57px; margin-top: 10px; height: 30px;"/>
  </div>
  <div id="canvasDiv" style="width: 425px; float:left;">
  <canvas id="chartjs-2" ></canvas>
  </div>
</div>

** UPDATE **

If it's not possible to get the input boxes in the canvas can someone offer some advice on the best way to make sure the input boxes line up reliably with the dynamic choices? I.e the text size for the labels is different for 2 options vs when you have 5 options. How do I line up the input boxes outside of the canvas in an intelligent and reliable way?

Smooth
  • 956
  • 1
  • 15
  • 37
  • I suspect that putting an input checkbox in a canvas is not possible, just as this Q&A says it's not for a text box: https://stackoverflow.com/questions/16052024/how-to-draw-text-box-inside-the-canvas I suggest you find a way to make it work with your inputs outside the canvas. – Marc Mar 21 '19 at 22:58

1 Answers1

1

Assuming Marc comment is right (which seems pretty much the case), to answer your "what's the alternative solution to align inputs"...

Updated Answer

Using display: flex makes it easier to automatically adjust the size and/or distance between the checkboxes if the height of the container that wrap the bars in the canvas is always the same no matter how many options there are.

.chart-container {
  display: flex;
}

.chart {
  height: 200px;
}

.checkbox-container {
  display: flex;
  flex-direction: column;
  height: 128px;    /* height - adjust as needed */
  margin-top: 38px; /* positioning - adjust as needed */
  
  background: white;  /* demo purpose only - hide image checkboxes */
  position: absolute; /* demo purpose only - hide image checkboxes */
  left: 10px;         /* demo purpose only - hide image checkboxes */
  padding: 0 8px;     /* demo purpose only - hide image checkboxes */
  z-index: 1;         /* demo purpose only - hide image checkboxes */
} 

.checkbox {
  cursor: pointer;
  height: 100%;
  width: 100%;
  min-width: 16px; /* minimum checkbox size - adjust as needed */
}
<!-- 3 bars example -->
<div class="chart-container">
  <div class="checkbox-container">
    <input class="checkbox" type="checkbox">
    <input class="checkbox" type="checkbox">
    <input class="checkbox" type="checkbox">
  </div>
  <img class="chart" src="https://i.stack.imgur.com/440p0.png">
</div>

<!-- 4 bars examples -->
<div class="chart-container">
  <div class="checkbox-container">
    <input class="checkbox" type="checkbox">
    <input class="checkbox" type="checkbox">
    <input class="checkbox" type="checkbox">
    <input class="checkbox" type="checkbox">
  </div>
  <img class="chart" src="https://i.stack.imgur.com/440p0.png">
</div>

<!-- 5 bars examples -->
<div class="chart-container">
  <div class="checkbox-container">
    <input class="checkbox" type="checkbox">
    <input class="checkbox" type="checkbox">
    <input class="checkbox" type="checkbox">
    <input class="checkbox" type="checkbox">
    <input class="checkbox" type="checkbox">
  </div>
  <img class="chart" src="https://i.stack.imgur.com/440p0.png">
</div>

Original Answer

You could...

  • wrap the canvas in a div container with position: relative
  • create a container for your checkboxes with position: absolute
  • add your checkboxes into the position: absolute container and use margin-bottom so they align perfectly with your graph

If the number of options affects the height of the chart bars you would have to make the CSS values change as well accordingly either with...

  • javascript: document.querySelector('.checkbox').style.height = ...
  • using Angular [style] directive: [style.height.px]="..."
  • using CSS classes depending on number of checkboxes/siblings inside the checkbox container: https://stackoverflow.com/a/12198561/5583283

Below is a working example using the 3rd option above, the CSS classes that change values depending on the number of checkboxes/siblings inside the checkbox-container.

The whole idea is to keep the checkboxes height equals to the chart bars height and the distance between the checkboxes equals to the distance of the chart bars distance.

Please note that all pixel values for alignments are relative to what you expect and can be played with but I guess it will give you the big picture.

Hope it helps!

.chart-container {
  position: relative;
}

.chart {
  height: 200px;
}

.checkbox-container {
  background: white;  /* for demo purpose - hides image checkboxes */
  padding-right: 8px; /* for demo purpose - hides image checkboxes */
  position: absolute;
  top: 44px;          /* adjust as needed */
  left: 8px;          /* adjust as needed */
} 

.checkbox {
  display: block;
  margin-bottom: 15px; /* adjust as needed */
  height: 28px;        /* desired checkbox height */
  width: 28px;         /* desired checkbox width */
  cursor: pointer;
}

/* 5 checkboxes */
.checkbox:first-child:nth-last-child(5),
.checkbox:first-child:nth-last-child(5) ~ .checkbox {
  margin-bottom: 6px; /* adjust as needed */
  height: 18px;       /* desired checkbox height */
  width: 18px;        /* desired checkbox width */
}
<!-- 3 bars example -->
<div class="chart-container">
  <img class="chart" src="https://i.stack.imgur.com/440p0.png">
  <div class="checkbox-container">
    <input class="checkbox" type="checkbox">
    <input class="checkbox" type="checkbox">
    <input class="checkbox" type="checkbox">
  </div>
</div>

<!-- 5 bars examples - sorry didn't have an image with 5 bars -->
<div class="chart-container">
  <img class="chart" src="https://i.stack.imgur.com/440p0.png">
  <div class="checkbox-container">
    <input class="checkbox" type="checkbox">
    <input class="checkbox" type="checkbox">
    <input class="checkbox" type="checkbox">
    <input class="checkbox" type="checkbox">
    <input class="checkbox" type="checkbox">
  </div>
</div>
j3ff
  • 5,719
  • 8
  • 38
  • 51
  • 1
    My pleasure! If you are using SCSS it could be easy to create the siblings CSS classes (those that affect the height of checkbox and distance) with a mixin function and calculate pixel values mathematically... see https://stackoverflow.com/a/19088399/5583283 if not familiar with it – j3ff Mar 22 '19 at 02:20