Skip to content
This repository has been archived by the owner on Sep 3, 2022. It is now read-only.

A library to help visualize period intersections and gaps

License

Notifications You must be signed in to change notification settings

bakame-php/period-visualizer

Repository files navigation

Period Visualizer

Author Build Status Total Downloads Latest Stable Version Software License

This package contains a visualizer for League Period.

It is inspired from the work of @thecrypticace on the following PR Visualization Helper.

<?php

use Bakame\Period\Visualizer\Dataset;
use Bakame\Period\Visualizer\GanttChart;
use League\Period\Datepoint;
use League\Period\Period;
use League\Period\Sequence;

$sequence = new Sequence(
    Datepoint::create('2018-11-29')->getYear(Period::EXCLUDE_START_INCLUDE_END),
    Datepoint::create('2018-05-29')->getMonth()->expand('3 MONTH'),
    Datepoint::create('2017-01-13')->getQuarter(Period::EXCLUDE_ALL),
    Period::around('2016-06-01', '3 MONTHS', Period::INCLUDE_ALL)
);
$dataset = Dataset::fromSequence($sequence);
$dataset->append('gaps', $sequence->gaps());
(new GanttChart())->stroke($dataset);

results:

 A                                          (--------------------]
 B                                            [-----------)       
 C                     (----)                                     
 D    [---------]                                                 
 gaps           (------]    [---------------]  

System Requirements

You need:

  • PHP >= 7.2 but the latest stable version of PHP is recommended
  • League/Period 4.4+ but the latest stable version is recommended

Installation

$ composer require bakame/period-visualizer

Usage

Basic Usage

Generate a simple graph.

To generate a graph you need to give to the Dataset constructor a list of pairs. Each pair is an array containing 2 values:

  • the value at key 0 represents the label
  • the value at key 1 is a League\Period\Period or a League\Period\Sequence object
<?php

use Bakame\Period\Visualizer\Dataset;
use Bakame\Period\Visualizer\GanttChart;
use League\Period\Period;

$dataset = new Dataset([
    ['A', new Period('2018-01-01', '2018-02-01')],
    ['B', new Period('2018-01-15', '2018-02-01')], 
]);
(new GanttChart())->stroke($dataset);

results:

 A [----------------------------------------------)
 B                      [-------------------------)

Appending items to display

If you want to display a Sequence and some of its operations. You can append the operation results using the Dataset::append method.

<?php

use Bakame\Period\Visualizer\Dataset;
use Bakame\Period\Visualizer\GanttChart;
use League\Period\Period;
use League\Period\Sequence;

$sequence = new Sequence(
    new Period('2018-01-01', '2018-03-01'),
    new Period('2018-05-01', '2018-08-01')
);
$dataset = new Dataset();
$dataset->append('A', $sequence[0]);
$dataset->append('B', $sequence[1]);
$dataset->append('GAPS', $sequence->gaps());
(new GanttChart())->stroke($dataset);

results:

 A    [-------------)                                                         
 B                               [----------------)
 GAPS               [------------)    

The Dataset implements the Countable and the IteratorAggregate interface. It also exposes the following methods:

<?php
public function Dataset::fromSequence(Sequence $sequence, ?LabelGenerator $labelGenerator = null): self; //Creates a new Dataset from a Sequence and a LabelGenerator.
public function Dataset::fromCollection(iterable $collection): self; //Creates a new Dataset from a generic iterable structure.
public function Dataset::appendAll(iterable $pairs): void; //adds multiple pairs at once.
public function Dataset::isEmpty(): bool; //Tells whether the collection is empty.
public function Dataset::labels(): string[]; //the current labels used
public function Dataset::items(): Sequence[]; //the current objects inside the Dataset
public function Dataset::boundaries(): ?Period;  //Returns the collection boundaries or null if it is empty.
public function Dataset::labelMaxLength(): int;  //Returns the label max length.
public function Dataset::withLabels(LabelGenerator $labelGenerator): self; //Update the labels used for the dataset.

Setting the Dataset labels

By default you are required to provide a label per item added present in a Dataset object. The package provides a LabelGenerator interface that ease generating and creating labels for your visualization.

A LabelGenerator implementing class is needed for the following methods

  • The Dataset::fromSequence, to create a new instance from a Sequence object;
  • The Dataset::withLabels to update the associated labels in the current instance;

By default when using Dataset::fromSequence if no LabelGenerator class is supplied the LatinLetter label generator will be used.

The current package comes bundle with the following LabelGenerator implementing class:

LatinLetter

Generates labels according the the latin alphabet.

<?php

use Bakame\Period\Visualizer\Dataset;
use Bakame\Period\Visualizer\GanttChart;
use Bakame\Period\Visualizer\LatinLetter;
use League\Period\Period;
use League\Period\Sequence;

$dataset = Dataset::fromSequence(
    new Sequence(new Period('2018-01-01', '2018-02-01'), new Period('2018-01-15', '2018-02-01')),
    new LatinLetter('aa')
);
(new GanttChart())->stroke($dataset);

results:

 aa [-----------------------------------)
 ab [----------)

The LatinLetter also exposes the following methods:

<?php

public function LatinLetter::startingAt(): string; //returns the first letter to be used
public function LatinLetter::startsWith(): self;  //returns a new object with a new starting letter

DecimalNumber

Generates labels according to the decimal number system.

<?php

use Bakame\Period\Visualizer\Dataset;
use Bakame\Period\Visualizer\DecimalNumber;
use Bakame\Period\Visualizer\GanttChart;
use League\Period\Period;
use League\Period\Sequence;

$dataset = Dataset::fromSequence(
    new Sequence(new Period('2018-01-01', '2018-02-01'), new Period('2018-01-15', '2018-02-01')),
    new DecimalNumber(42)
);
(new GanttChart())->stroke($dataset);

results:

 42 [-----------------------------------)
 43 [----------)

The DecimalNumber also exposes the following methods:

<?php

public function DecimalNumber::startingAt(): string; //returns the first decimal number to be used
public function DecimalNumber::startsWith(): self;  //returns a new object with a new starting decimal number

RomanNumber

Uses the DecimalNumber label generator class to generate Roman number labels.

<?php

use Bakame\Period\Visualizer\Dataset;
use Bakame\Period\Visualizer\DecimalNumber;
use Bakame\Period\Visualizer\GanttChart;
use Bakame\Period\Visualizer\RomanNumber;
use League\Period\Period;
use League\Period\Sequence;

$labelGenerator = new RomanNumber(new DecimalNumber(5), RomanNumber::LOWER);

$dataset = Dataset::fromSequence(
    new Sequence(new Period('2018-01-01', '2018-02-01'), new Period('2018-01-15', '2018-02-01')),
    $labelGenerator
);
(new GanttChart())->stroke($dataset);

results:

 v  [-----------------------------------)
 vi [----------)

The RomanNumber also exposes the following methods:

<?php
const RomanNumber::UPPER = 1;
const RomanNumber::LOWER = 2;
public function RomanNumber::startingAt(): string; //returns the first decimal number to be used
public function RomanNumber::startsWith(): self;  //returns a new object with a new starting decimal number
public function RomanNumber::withLetterCase(int $lettercase): self;  //returns a new object with a new letter casing
public function RomanNumber::isUpper(): bool;  //Tells whether the roman letter is upper cased.
public function RomanNumber::isLower(): bool;  //Tells whether the roman letter is lower cased.

AffixLabel

Uses any labelGenerator implementing class to add prefix and/or suffix string to the generated labels.

<?php

use Bakame\Period\Visualizer\AffixLabel;
use Bakame\Period\Visualizer\Dataset;
use Bakame\Period\Visualizer\DecimalNumber;
use Bakame\Period\Visualizer\GanttChart;
use Bakame\Period\Visualizer\RomanNumber;
use League\Period\Period;
use League\Period\Sequence;

$labelGenerator = new AffixLabel(
    new RomanNumber(new DecimalNumber(5), RomanNumber::LOWER),
    '*', //prefix
    '.)'    //suffix
);
$dataset = Dataset::fromSequence(
    new Sequence(new Period('2018-01-01', '2018-02-01'), new Period('2018-01-15', '2018-02-01')),
    $labelGenerator
);
(new GanttChart())->stroke($dataset);

results:

 * v .)  [-----------------------------------)
 * vi .) [----------)

The AffixLabel also exposes the following methods:

<?php

public function AffixLabel::prefix(): string; //returns the current prefix
public function AffixLabel::suffix(): string;  //returns the current suffix
public function AffixLabel::withPrefix(string $prefix): self;  //returns a new object with a new prefix
public function AffixLabel::withSuffix(string $suffix): self;  //returns a new object with a new suffix

ReverseLabel

Uses any labelGenerator implementing class to reverse the generated labels order.

<?php

use Bakame\Period\Visualizer\AffixLabel;
use Bakame\Period\Visualizer\Dataset;
use Bakame\Period\Visualizer\DecimalNumber;
use Bakame\Period\Visualizer\GanttChart;
use Bakame\Period\Visualizer\ReverseLabel;
use Bakame\Period\Visualizer\RomanNumber;
use League\Period\Period;
use League\Period\Sequence;

$labelGenerator = new DecimalNumber(5);
$labelGenerator = new RomanNumber($labelGenerator, RomanNumber::LOWER);
$labelGenerator = new AffixLabel($labelGenerator, '', '.');
$labelGenerator = new ReverseLabel($labelGenerator);

$dataset = Dataset::fromSequence(
    new Sequence(new Period('2018-01-01', '2018-02-01'), new Period('2018-01-15', '2018-02-01')),
    $labelGenerator
);
(new GanttChart())->stroke($dataset);

results:

 vi. [-----------------------------------)
 v.  [----------)

Custom LabelGenerator

You can create your own label generator by implementing the Bakame\Period\Visualizer\LabelGenerator interface like shown below:

<?php

use Bakame\Period\Visualizer\AffixLabel;
use Bakame\Period\Visualizer\Dataset;
use Bakame\Period\Visualizer\GanttChart;
use Bakame\Period\Visualizer\LabelGenerator;
use League\Period\Period;
use League\Period\Sequence;

$samelabel = new class implements LabelGenerator {
    public function generate(int $nbLabels): array
    {
        return array_fill(0, $nbLabels, $this->format('foobar'));
    }
        
    public function format($str): string
    {
        return (string) $str;
    }
};

$labelGenerator = new AffixLabel($samelabel, '', '.');
$dataset = Dataset::fromSequence(
    new Sequence(new Period('2018-01-01', '2018-02-01'), new Period('2018-01-15', '2018-02-01')),
    $labelGenerator
);
(new GanttChart())->stroke($dataset);

results:

 foobar. [-----------------------------------)
 foobar. [----------)

Displaying the Dataset

The GanttChart class is responsible for generating the graph from the Dataset by implementing the Graph interface for the console.

The GanttChart::stroke methods expects a Dataset object as its unique argument.

If you wish to present the graph on another medium like a web browser or an image, you will need to implement the interface for your implementation.

<?php

use Bakame\Period\Visualizer\Dataset;
use Bakame\Period\Visualizer\GanttChart;
use League\Period\Period;

$graph = new GanttChart();
$graph->stroke(new Dataset([
    ['first', new Period('2018-01-01 08:00:00', '2018-01-01 12:00:00')],
    ['last', new Period('2018-01-01 10:00:00', '2018-01-01 14:00:00')],
]));

results:

 first [---------------------------)
 last            [------------------------------)

Customized the graph looks

The GanttChart class can be customized by providing a GanttChartConfig which defines:

  • the output medium via a OutputWriter implementing class.
  • the graph settings. (How the intervals will be stroked)
    • sets the graph width
    • sets the graph colors
    • sets the gap between the labels and the rows
    • sets the label alignment
  • the output settings (How the intervals will be created)
    • sets single characters to represent the boundary types
    • sets single characters to represent the body and space

You can easily create a OutputWriter implementing class with libraries like League CLImate or Symfony Console to output the resulting graph. If you don't, the package ships with a minimal ConsoleOutput class which is used if you do not provide you own implementation.

<?php

use Bakame\Period\Visualizer\AffixLabel;
use Bakame\Period\Visualizer\ConsoleOutput;
use Bakame\Period\Visualizer\Dataset;
use Bakame\Period\Visualizer\DecimalNumber;
use Bakame\Period\Visualizer\GanttChart;
use Bakame\Period\Visualizer\GanttChartConfig;
use Bakame\Period\Visualizer\ReverseLabel;
use Bakame\Period\Visualizer\RomanNumber;
use League\Period\Datepoint;
use League\Period\Period;
use League\Period\Sequence;

$config = GanttChartConfig::createFromRainbow()
    ->withOutput(new ConsoleOutput(STDOUT))
    ->withStartExcluded('πŸ•')
    ->withStartIncluded('πŸ…')
    ->withEndExcluded('🎾')
    ->withEndIncluded('πŸ”')
    ->withWidth(30)
    ->withSpace('πŸ’©')
    ->withBody('😊')
    ->withGapSize(2)
    ->withLeftMarginSize(1)
    ->withLabelAlign(GanttChartConfig::ALIGN_RIGHT)
;

$labelGenerator = new DecimalNumber(42);
$labelGenerator = new RomanNumber($labelGenerator, RomanNumber::UPPER);
$labelGenerator = new AffixLabel($labelGenerator, '', '.');
$labelGenerator = new ReverseLabel($labelGenerator);

$sequence = new Sequence(
    Datepoint::create('2018-11-29')->getYear(Period::EXCLUDE_START_INCLUDE_END),
    Datepoint::create('2018-05-29')->getMonth()->expand('3 MONTH'),
    Datepoint::create('2017-01-13')->getQuarter(Period::EXCLUDE_ALL),
    Period::around('2016-06-01', '3 MONTHS', Period::INCLUDE_ALL)
);
$dataset = Dataset::fromSequence($sequence, $labelGenerator);
$dataset->append($labelGenerator->format('gaps'), $sequence->gaps());
$graph = new GanttChart($config);
$graph->stroke($dataset);

result:

   XLV.  πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ•πŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ”
  XLIV.  πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ…πŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ˜ŠπŸŽΎπŸ’©πŸ’©πŸ’©
 XLIII.  πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ•πŸ˜ŠπŸ˜ŠπŸŽΎπŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©
  XLII.  πŸ…πŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ”πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©
  GAPS.  πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ•πŸ˜ŠπŸ˜ŠπŸ”πŸ’©πŸ’©πŸ…πŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ˜ŠπŸ”πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©πŸ’©

On a POSIX compliant console all lines have different colors

The GanttChartConfig class exposes the following additional constants and methods:

<?php
const GanttChartConfig::ALIGN_LEFT = 1;
const GanttChartConfig::ALIGN_RIGHT = 0;
const GanttChartConfig::ALIGN_CENTER = 2;
public function GanttChartConfig::__construct(OutputWriter $output);
public function GanttChartConfig::output(): OutputWriter;  //Returns the OutputWriter instance.
public function GanttChartConfig::startExcluded(): string; //Retrieves the excluded start block character.
public function GanttChartConfig::startIncluded(): string; //Retrieves the included start block character.
public function GanttChartConfig::endExcluded(): string;   //Retrieves the excluded end block character.
public function GanttChartConfig::endIncluded(): string;   //Retrieves the included end block character.
public function GanttChartConfig::width(): int;            //Retrieves the max size width.
public function GanttChartConfig::body(): string;          //Retrieves the body block character.
public function GanttChartConfig::space(): string;         //Retrieves the space block character.
public function GanttChartConfig::colors(): string[];      //The selected colors for each row.
public function GanttChartConfig::gapSize(): int;          //Retrieves the gap sequence between the label and the line.
public function GanttChartConfig::labelAlign(): int;       //Returns how label should be aligned.
public function GanttChartConfig::leftMarginSize(): int;   //Retrieves the margin between the label and the console left side.

GanttChartConfig is immutable, modifying its properties returns a new instance with the updated values.

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Contributions are welcome and will be fully credited. Please see CONTRIBUTING for details.

Testing

The library has a :

  • a PHPUnit test suite
  • a coding style compliance test suite using PHP CS Fixer.
  • a code analysis compliance test suite using PHPStan.

To run the tests, run the following command from the project folder.

$ composer test

Security

If you discover any security related issues, please email [email protected] instead of using the issue tracker.

Credits

License

The MIT License (MIT). Please see License File for more information.