Google 甘特图轴自定义 [标签和 Date/Time]

Google Gantt charts axis customization [Labels and Date/Time]

我正在尝试处理 Google 甘特图,它总体上运行良好。然而,一些定制需求让我摸不着头脑。

我希望任务标签根据栏的宽度和位置在栏内或栏附近显示任务名称。需要从 y 轴的左侧部分删除任务名称

另外,图表变大后,时间范围(x轴)放在底部就看不到了,如果能放在顶部就好了。

有什么办法可以实现这种定制吗?

请检查下面提到的代码片段以供参考。

Gantt chart

import { Component, OnInit } from '@angular/core';
import { nightcycleDependencies } from './nightcycleData';
declare var google: any;
declare var $ :any;

@Component({
  selector: 'app-chart',
  templateUrl: './chart.component.html',
  styleUrls: ['./chart.component.css']
})
export class ChartComponent implements OnInit {

  public allTasks: any[] = [];
  public nightcycleDependencies = nightcycleDependencies;
  constructor() {
  }

  ngOnInit() {
    this.renderChartForAll();
  }

  // Render Chart for Summarized View
  renderChartForAll() {
    google.charts.setOnLoadCallback(() => this.formatDataForAll());
  }

  // Data format for Summarized View
  formatDataForAll() {
    this.allTasks = [];
    var resource = 'Job Cycles';
    const today = new Date();
    const start_day = today.getDate();
    const end_day = today.getDate();
    const month = today.getMonth() + 1;
    const year = today.getFullYear();

    nightcycleDependencies.forEach((dependency) => {

      var finalStart = 9999999999999;
      var finalEnd = 0;
      var taskId = dependency.source.split(" ").join('');
      var taskName = dependency.source;
      var dependencies = dependency.parents ? dependency.parents.toString().split(" ").join("") : "null";

      dependency.ids.forEach((job) => {
        // Date handling
        var startTime = new Date(year + '-' + month + '-' + start_day + ' ' + "12:00").getTime()
        var startDate = new Date(year + '-' + month + '-' + start_day + ' ' + job.start).getTime()
        var endDate = new Date(year + '-' + month + '-' + end_day + ' ' + job.end).getTime()
        if (startTime > startDate) {
          startDate = new Date(year + '-' + month + '-' + (start_day + 1) + ' ' + job.end).getTime()
          endDate = new Date(year + '-' + month + '-' + (end_day + 1) + ' ' + job.end).getTime()
        } else if (endDate < startDate) {
          endDate = new Date(year + '-' + month + '-' + (end_day + 1) + ' ' + job.end).getTime()
        }
        if (finalStart > startDate) {
          finalStart = startDate
        }
        if (finalEnd < endDate) {
          finalEnd = endDate
        }
      });
      var duration = finalEnd - finalStart
      this.allTasks.push([taskId, taskName, taskName, new Date(finalStart), new Date(finalEnd), duration, 100, dependencies])
    });
    this.drawChart(this.allTasks, '');
  }

  // Render Chart for Selected Cycle
  getCycleBasedData(source) {
    google.charts.setOnLoadCallback(() => this.formatCycleBasedData(source));
  }

  // Data format for Selected Cycle
  formatCycleBasedData(source) {
    this.allTasks = [];
    var resource = source;
    var dependencies = [];
    var chartSource = [];
    var jobsFromOtherSources = [];

    // Set The source to loop over
    nightcycleDependencies.forEach(dependency => {
      if (dependency.source === source) {
        chartSource = dependency.ids;
      }
    });
    chartSource.forEach((dependency) => {
      var taskId = dependency.id.split(" ").join('');
      var taskName = dependency.id;
      dependencies = [];

      // Date handling
      var date = this.formatDate(dependency.start, dependency.end);

      // Find out dependencies
      dependency.parent.forEach((parent) => {
        dependencies.push(parent.id.split(' ').join(''))
      })
      var finalDependencies = dependencies.toString();

      // Get all Parent jobs and push them to task list
      jobsFromOtherSources = this.getAllParentJobs(taskName, resource);
      jobsFromOtherSources.forEach((otherJob) => {
        var jobId = otherJob.id.split(" ").join('');
        var jobName = otherJob.id;
        var jobSource = otherJob.source;
        var jobDate = this.formatDate(otherJob.start, otherJob.end);
        this.allTasks.push([jobId, jobName, jobSource, new Date(jobDate[0]), new Date(jobDate[0] + 800000), (jobDate[0] + 800000 - jobDate[0]), 100, null])
      })

      // Get all Children jobs
      // this.getAllChildJobs(taskName, resource);

      // Push to the task list
      this.allTasks.push([taskId, taskName, resource, new Date(date[0]), new Date(date[1]), (date[1] - date[0]), 100, finalDependencies])
    });
    this.drawChart(this.allTasks, source);
  }

  // Function to handle date
  formatDate(start, end) {
    const today = new Date();
    var startTime = 0;
    var startDate = 0;
    var endDate = 0;
    const start_day = today.getDate();
    const end_day = today.getDate();
    const month = today.getMonth() + 1;
    const year = today.getFullYear();

    startTime = new Date(year + '-' + month + '-' + start_day + ' ' + "12:00").getTime()
    startDate = new Date(year + '-' + month + '-' + start_day + ' ' + start).getTime()
    endDate = new Date(year + '-' + month + '-' + end_day + ' ' + end).getTime()
    if (startTime > startDate) {
      startDate = new Date(year + '-' + month + '-' + (start_day + 1) + ' ' + start).getTime()
      endDate = new Date(year + '-' + month + '-' + (end_day + 1) + ' ' + end).getTime()
    } else if (endDate < startDate) {
      endDate = new Date(year + '-' + month + '-' + (end_day + 1) + ' ' + end).getTime()
    }
    return [startDate, endDate]
  }

  // Get all Parent Jobs
  getAllParentJobs(name, source) {
    var job = {};
    var jobSource = '';
    var jobsFromOtherSources = [];
    nightcycleDependencies.forEach((dependency) => {
      if (dependency.source != source) {
        jobSource = dependency.source;
        dependency.ids.forEach((job) => {
          job = job;
          job.source = jobSource
          job.children.forEach((child) => {
            if (child.id === name) {
              jobsFromOtherSources.push(job);
            }
          });
        });
      }
    });
    return jobsFromOtherSources;
  }

  // Get all children jobs
  // getAllChildJobs(name, source) {

  // }

  // Chart configuration
  drawChart(allTasks, source) {
    var nightcycleData = new google.visualization.DataTable();
    nightcycleData.addColumn('string', 'Task ID');
    nightcycleData.addColumn('string', 'Task Name');
    nightcycleData.addColumn('string', 'Resource');
    nightcycleData.addColumn('date', 'Start');
    nightcycleData.addColumn('date', 'End');
    nightcycleData.addColumn('number', 'Duration');
    nightcycleData.addColumn('number', 'Percent Complete');
    nightcycleData.addColumn('string', 'Dependencies');

    nightcycleData.addRows(this.allTasks);
    var trackHeight = 50;
    var options = {
      height: nightcycleData.getNumberOfRows() * trackHeight + 200,
      gantt: {
        labelStyle: {
          fontName: ["RobotoCondensedRegular"],
          fontSize: 15
        },
        textPosition: 'none',
        innerGridTrack: { fill: '#fff3e0' },
        innerGridDarkTrack: { fill: '#ffcc80' },
        trackHeight: trackHeight
      }
    };

    var chart = new google.visualization.Gantt(source ? document.getElementById(source.split(' ').join('')) : document.getElementById('chart_div'));
    chart.draw(nightcycleData, options);
    // Event listener which will fire on bar selection
    google.visualization.events.addListener(chart, 'select', () => this.getSelectedBarInfo(nightcycleData, chart));

  }

  // Change chart tab programmatically
  getSelectedBarInfo(nightcycleData, chart) {
    var selection = chart.getSelection();
    if (selection.length) {
      var cycle = nightcycleData.getValue(selection[0].row, 2);
      this.getCycleBasedData(cycle);
      var cycleId = cycle.split(" ").join("");
      document.querySelector('.tab-pane.fade.active.show').classList.remove('active', 'show');
      document.querySelector('.nav-link.active').classList.remove('active');
      document.getElementById(cycleId).classList.add('active', 'show');
      document.getElementById('id'+cycleId).classList.add('active', 'show');
    }
  }

  //For Responsiveness on resizing
  onResize(event) {
    this.drawChart(this.allTasks, event.target.id);
  }
}
.btn-holders{
    padding:10px;
}
.chart-tabs{
    padding: 20px 0px;
}
.chart-btn{
    margin: 5px;
}
.chart-item{
    float:left;
    padding:20px 20px 20px 0px;
    list-style: none;
}
.chart-list{
    padding: 0px;
}
.chart-anchor{
    background-color: #c0b5d0;
    padding: 20px;
    border-radius: 10px;
    color: #0c5460;
}
.chart-anchor.active {
    background-color: dodgerblue;
    color:#fff;
}
.tab-content>.tab-pane {
  height: 1px;
  overflow: hidden;
  display: block;
 visibility: hidden;
}
.tab-content>.active {
  height: auto;
  overflow: hidden;
  visibility: visible;
}
<div class="container-fluid">
  <!-- Tabs -->
  <div class="chart-tabs">
    <ul class="nav nav-pills" role="tablist">
      <li class="nav-item">
        <a [attr.id]="'idall'" class="nav-link active" data-toggle="pill" href="#all" role="tab">All</a>
      </li>
      <li class="nav-item" *ngFor="let dependency of nightcycleDependencies" (click)="getCycleBasedData(dependency.source)">
        <a [attr.id]="'id'+dependency.source.split(' ').join('')" class="nav-link" data-toggle="pill" [attr.href]="'#'+dependency.source.split(' ').join('')" role="tab">{{dependency.source}}</a>
      </li>
    </ul>
  </div>
  <!-- Tab Content -->
  <div class="tab-content">
    <div id="all" class="tab-pane fade in active show" role="tabpanel">
      <div id="chart_div" style="width:100%;" (window:resize)="onResize($event)"></div>
    </div>
    <div id="DailyProcessing" style="width:100%;" class="tab-pane fade" role="tabpanel">
      <div (window:resize)="onResize($event)"></div>
    </div>
    <div id="TradeMgmtandProcessing" style="width:100%;" class="tab-pane fade" role="tabpanel">
      <div (window:resize)="onResize($event)"></div>
    </div>
    <div id="DailyAnalyticsCycle" style="width:100%;" class="tab-pane fade" role="tabpanel">
      <div (window:resize)="onResize($event)"></div>
    </div>
    <div id="EuropeCycle" style="width:100%;" class="tab-pane fade" role="tabpanel">
      <div (window:resize)="onResize($event)"></div>
    </div>
    <div id="USCycle" style="width:100%;" class="tab-pane fade" role="tabpanel">
      <div (window:resize)="onResize($event)"></div>
    </div>
  </div>
</div>

没关系,我通过一点DOM操作得到了答案:

添加一个事件侦听器,该侦听器应在图表准备就绪后触发

google.visualization.events.addListener(chart, 'ready', () => this.addLabelText(chartId, this.allTasks));

函数体应该是这样的,

addLabelText(chartId, allTasks) {
    var toContainer = $('#' + chartId + ' > div > div');
    $("#" + chartId + " g:eq(5) rect").each(function ($index) {

      if($(this).attr('width') < (allTasks[$index][1].length * 7)) {

        if((Number($(this).attr('x')) + Number($(this).attr('width')) + 100) > document.body.clientWidth) {
          toContainer.append("<div style='top:" + $(this).attr('y') + "px; left: " + (Number($(this).attr('x')) - (allTasks[$index][1].length * 7)) +
          "px; text-align: left;position:absolute;line-height:2; padding-left: 5px; color:#111; font-size:14px;'>" + allTasks[$index][1] + "</div>");
        } else {
          toContainer.append("<div style='top:" + $(this).attr('y') + "px; left: " + (Number($(this).attr('x')) + Number($(this).attr('width'))) +
          "px; text-align: left;position:absolute;line-height:2; padding-left: 5px; color:#111; font-size:14px;'>" + allTasks[$index][1] + "</div>");
        }

      } else {
        toContainer.append("<div style='top:" + $(this).attr('y') + "px; left: " + Number($(this).attr('x')) +
        "px; text-align: left;position:absolute;line-height:2; padding-left: 5px; color:#fff; font-size:14px;'>" + allTasks[$index][1] + "</div>");
      }
    });
  }

根据您的需要进行一些自定义,应该可以正常工作。