Skip to content

Commit

Permalink
Merge bd3b32d into cc5f22b
Browse files Browse the repository at this point in the history
  • Loading branch information
guimeira committed May 21, 2017
2 parents cc5f22b + bd3b32d commit 83dd22a
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 59 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ new Chartist.Bar('.ct-chart', data, {
| --- | --- | --- | --- |
| `className` | Adds a class to the `ul` element. | `string` | `''` |
| `clickable` | Sets the legends clickable state; setting this value to `false` disables toggling graphs on legend click. | `bool` | `true` |
| `legendNames` | Sets custom legend names. By default the `name` property of the series will be used if none are given. | `mixed` | `false` |
| `legendNames` | Sets custom legend names. By default the `name` property of the series will be used if none are given. Multiple series can be associated with a legend item using this property as well. See examples for more details. | `mixed` | `false` |
| `onClick` | Accepts a function that gets invoked if `clickable` is true. The function has the `chart`, and the click event (`e`), as arguments. | `mixed` | `false` |
| `classNames` | Accepts a array of strings as long as the chart's series, those will be added as classes to the `li` elements. | `mixed` | `false` |
| `removeAll` | Allow all series to be removed at once. | `bool` | `false` |
Expand Down
127 changes: 69 additions & 58 deletions chartist-plugin-legend.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,6 @@

Chartist.plugins.legend = function (options) {

function compareNumbers(a, b) {
return a - b;
}

// Catch invalid options
if (options && options.position) {
if (!(options.position === 'top' || options.position === 'bottom' || options.position instanceof HTMLElement)) {
Expand All @@ -57,6 +53,25 @@
}

return function legend(chart) {
function updateChart() {
var newSeries = [];
var newLabels = [];

for (var i = 0; i < seriesMetadata.length; i++) {
if(seriesMetadata[i].legend != -1 && legends[seriesMetadata[i].legend].active) {
newSeries.push(seriesMetadata[i].data);
newLabels.push(seriesMetadata[i].label);
}
}

chart.data.series = newSeries;
if (useLabels) {
chart.data.labels = newLabels;
}

chart.update();
}

var existingLegendElement = chart.container.querySelector('.ct-legend');
if (existingLegendElement) {
// Clear legend if already existing.
Expand Down Expand Up @@ -93,9 +108,6 @@
legendElement.style.cssText = 'width: ' + chart.options.width + 'px;margin: 0 auto;';
}

var removedSeries = [],
originalSeries = chart.data.series.slice(0);

// Get the right array to use for generating the legend.
var legendNames = chart.data.series,
useLabels = isPieChart && chart.data.labels;
Expand All @@ -104,21 +116,50 @@
legendNames = chart.data.labels;
}
legendNames = options.legendNames || legendNames;

var legends = [];
var seriesMetadata = new Array(chart.data.series.length);
var activeLegendCount = 0;

// Initialize the array that associates series with legends.
// -1 indicates that there is no legend associated with it.
for (var i = 0; i < chart.data.series.length; i++) {
seriesMetadata[i] = {
data: chart.data.series[i],
label: useLabels ? chart.data.labels[i] : null,
legend: -1
};
}

// Check if given class names are viable to append to legends
var classNamesViable = (Array.isArray(options.classNames) && (options.classNames.length === legendNames.length));

// Loop through all legends to set each name in a list item.
legendNames.forEach(function (legend, i) {
var legendText = legend.name || legend;
var legendSeries = legend.series || [i];

var li = document.createElement('li');
li.className = 'ct-series-' + i;
// Append specific class to a legend element, if viable classes are given
if (classNamesViable) {
li.className += ' ' + options.classNames[i];
}
li.setAttribute('data-legend', i);
li.textContent = legend.name || legend;
li.textContent = legendText;
legendElement.appendChild(li);

legendSeries.forEach(function(seriesIndex) {
seriesMetadata[seriesIndex].legend = i;
});

legends.push({
text: legendText,
series: legendSeries,
active: true
});

activeLegendCount++;
});

chart.on('created', function (data) {
Expand Down Expand Up @@ -148,66 +189,36 @@
return;
e.preventDefault();

var seriesIndex = parseInt(li.getAttribute('data-legend')),
removedSeriesIndex = removedSeries.indexOf(seriesIndex);
var legendIndex = parseInt(li.getAttribute('data-legend'));
var legend = legends[legendIndex];

if (removedSeriesIndex > -1) {
// Add to series again.
removedSeries.splice(removedSeriesIndex, 1);
if (!legends[legendIndex].active) {
legend.active = true;
activeLegendCount++;
li.classList.remove('inactive');
} else {
if (!options.removeAll) {
// Remove from series, only if a minimum of one series is still visible.
if ( chart.data.series.length > 1) {
removedSeries.push(seriesIndex);
li.classList.add('inactive');
}
// Set all series as active.
else {
removedSeries = [];
var seriesItems = Array.prototype.slice.call(legendElement.childNodes);
seriesItems.forEach(function (item) {
item.classList.remove('inactive');
});
}
}
else {
// Remove series unaffected if it is the last or not
removedSeries.push(seriesIndex);
li.classList.add('inactive');
legend.active = false;
activeLegendCount--;
li.classList.add('inactive');

if (!options.removeAll && activeLegendCount == 0) {
//If we can't disable all series at the same time, let's
//reenable all of them:
for (var i = 0; i < legends.length; i++) {
legends[i].active = true;
activeLegendCount++;
legendElement.childNodes[i].classList.remove('inactive');
}
}
}

// Reset the series to original and remove each series that
// is still removed again, to remain index order.
var seriesCopy = originalSeries.slice(0);
if (useLabels) {
var labelsCopy = originalLabels.slice(0);
}

// Reverse sort the removedSeries to prevent removing the wrong index.
removedSeries.sort(compareNumbers).reverse();

removedSeries.forEach(function (series) {
seriesCopy.splice(series, 1);
if (useLabels) {
labelsCopy.splice(series, 1);
}
});


updateChart();

if (options.onClick) {
options.onClick(chart, e);
}

chart.data.series = seriesCopy;
if (useLabels) {
chart.data.labels = labelsCopy;
}

chart.update();
});
}

};

};
Expand Down
55 changes: 55 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@
background-color: #453d3f;
border-color: #453d3f;
}

.ct-chart-line-multipleseries .ct-legend .ct-series-0:before {
background-color: #d70206;
border-color: #d70206;
}

.ct-chart-line-multipleseries .ct-legend .ct-series-1:before {
background-color: #f4c63d;
border-color: #f4c63d;
}

.ct-chart-line-multipleseries .ct-legend li.inactive:before {
background: transparent;
}

.crazyPink li.ct-series-0:before {
background-color: #C2185B;
Expand Down Expand Up @@ -341,6 +355,28 @@ <h3>Chart with <i>legendNames</i>:</h3>
legendNames: ['Custom title', 'Another one', 'And the last one'],
})
]
});</code></pre>
<h3>Chart with multiple series per item:</h3>
<span>The <code>legendNames</code> property can be used to associate multiple series with a legend item.</span>
<div class="ct-chart-line-multipleseries"></div>
<button class="button-showcode">Show Code</button>
<pre class="codeblock-hidden"><code class="javascript">new Chartist.Line('.ct-chart-line-multipleseries', {
labels: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
series: [
[12, 9, 7, 8, 5],
[2, 1, 3.5, 7, 3],
[1, 3, 4, 5, 6]
]
}, {
fullWidth: true,
chartPadding: {
right: 40
},
plugins: [
Chartist.plugins.legend({
legendNames: [{name: 'Red-ish', series: [0,1]}, {name: 'Yellow', series: [2]}],
})
]
});</code></pre>
<h3>Chart with <i>onClick</i>:</h3>
<span>Accepts a function that gets invoked if <code>clickable</code> is true. The function has the <code>chart</code>, and the click event (<code>e</code>), as arguments.</span>
Expand Down Expand Up @@ -564,6 +600,25 @@ <h3>Chart with <i>position</i>:</h3>
})
]
});

new Chartist.Line('.ct-chart-line-multipleseries', {
labels: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
series: [
[12, 9, 7, 8, 5],
[2, 1, 3.5, 7, 3],
[1, 3, 4, 5, 6]
]
}, {
fullWidth: true,
chartPadding: {
right: 40
},
plugins: [
Chartist.plugins.legend({
legendNames: [{name: 'Red-ish', series: [0,1]}, {name: 'Yellow', series: [2]}],
})
]
});

new Chartist.Line('.ct-chart-line-onclick', {
labels: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'],
Expand Down
119 changes: 119 additions & 0 deletions test/test.legend.js
Original file line number Diff line number Diff line change
Expand Up @@ -402,9 +402,128 @@ describe('Chartist plugin legend', function() {

click(seriesB);
expect(chart.legendClicked).to.equal(true);

//Clicking on an inactive series should also call the function.
chart.legendClicked = false;
click(seriesB);
expect(chart.legendClicked).to.equal(true);
});
});

describe('clickable with multiple series per legend item', function() {
before(function(done) {
chart = generateChart('Line', chartDataLine, {
clickable: true,
onClick: function(chart,e) {
chart.legendClicked = true;
},
legendNames: [{name: 'Red-ish', series: [0,1]}, {name: 'Yellow', series: [2]}]
});

chart.on('created', function() {
chart.off('created');
done();
});
});

after(destroyChart);

it('should enforce a className for each series', function() {
expect(chart.data.series[0].className).to.equal('ct-series-a');
expect(chart.data.series[1].className).to.equal('ct-series-b');
expect(chart.data.series[2].className).to.equal('ct-series-c');
});

it('should hide a series after a click on the legend item', function() {
var seriesA = chart.container.querySelector('ul.ct-legend > .ct-series-0');
var seriesB = chart.container.querySelector('ul.ct-legend > .ct-series-1');

expect(chart.data.series.length).to.equal(3);

// Clicking on the second legend item should hide the last series:
click(seriesB);
expect(chart.data.series.length).to.equal(2);
expect(chart.data.series[0].name).to.equal('Blue pill');
expect(chart.data.series[1].name).to.equal('Red pill');
var svgSeries = chart.container.querySelectorAll('g.ct-series');
expect(svgSeries.length).to.equal(2);
expect(svgSeries[0].className.baseVal).to.contain('ct-series-a');
expect(svgSeries[1].className.baseVal).to.contain('ct-series-b');

// A second click should show the corresponding series again.
click(seriesB);
var svgSeries2 = chart.container.querySelectorAll('g.ct-series');
expect(svgSeries2.length).to.equal(3);
expect(svgSeries2[0].className.baseVal).to.contain('ct-series-a');
expect(svgSeries2[1].className.baseVal).to.contain('ct-series-b');
expect(svgSeries2[2].className.baseVal).to.contain('ct-series-c');

// Clicking on the first legend item should hide the two first series:
click(seriesA);
expect(chart.data.series.length).to.equal(1);
expect(chart.data.series[0].name).to.equal('Purple pill');
var svgSeries = chart.container.querySelectorAll('g.ct-series');
expect(svgSeries.length).to.equal(1);
expect(svgSeries[0].className.baseVal).to.contain('ct-series-c');

// A second click should show the both series again.
click(seriesA);
var svgSeries2 = chart.container.querySelectorAll('g.ct-series');
expect(svgSeries2.length).to.equal(3);
expect(svgSeries2[0].className.baseVal).to.contain('ct-series-a');
expect(svgSeries2[1].className.baseVal).to.contain('ct-series-b');
expect(svgSeries2[2].className.baseVal).to.contain('ct-series-c');

// A click in the last active series should set all series active again.
click(seriesA);
expect(chart.data.series.length).to.equal(1);
expect(chart.data.series[0].name).to.equal('Purple pill');
click(seriesB);
expect(svgSeries2.length).to.equal(3);
expect(svgSeries2[0].className.baseVal).to.contain('ct-series-a');
expect(svgSeries2[1].className.baseVal).to.contain('ct-series-b');
expect(svgSeries2[2].className.baseVal).to.contain('ct-series-c');
});

it('should update the legend item classes', function() {
var seriesA = chart.container.querySelector('ul.ct-legend > .ct-series-0');
var seriesB = chart.container.querySelector('ul.ct-legend > .ct-series-1');

// The first click should hide the corresponding series.
click(seriesB);
var legendItems = chart.container.querySelectorAll('ul.ct-legend > li');
expect(legendItems[0].className).to.equal('ct-series-0');
expect(legendItems[1].className).to.equal('ct-series-1 inactive');

// A second click should show the corresponding series again.
click(seriesB);
var inactiveItem = chart.container.querySelectorAll('ul.ct-legend > li.inactive');
expect(inactiveItem.length).to.equal(0);

// A click in the last active series should set all series active again.
click(seriesA);
var legendItems = chart.container.querySelectorAll('ul.ct-legend > li');
expect(legendItems[0].className).to.equal('ct-series-0 inactive');
expect(legendItems[1].className).to.equal('ct-series-1');
click(seriesB);
var inactiveItem = chart.container.querySelectorAll('ul.ct-legend > li.inactive');
expect(inactiveItem.length).to.equal(0);

});

it('should call a function after a click on the legend item', function() {
var seriesB = chart.container.querySelector('ul.ct-legend > .ct-series-1');

click(seriesB);
expect(chart.legendClicked).to.equal(true);

//Clicking on an inactive series should also call the function.
chart.legendClicked = false;
click(seriesB);
expect(chart.legendClicked).to.equal(true);
});
});

describe('clickable for a pie', function() {
before(function(done) {
chart = generateChart('Pie', chartDataPie, {
Expand Down

0 comments on commit 83dd22a

Please sign in to comment.