Is it possible to add an image to a specific data point in an X-Series Highchart graph?
For example, I have the following chart:
/**
* Highcharts X-range series plugin
*/
(function (H) {
var defaultPlotOptions = H.getOptions().plotOptions,
columnType = H.seriesTypes.column,
each = H.each,
extendClass = H.extendClass,
pick = H.pick,
Point = H.Point,
Series = H.Series;
defaultPlotOptions.xrange = H.merge(defaultPlotOptions.column, {
tooltip: {
pointFormat: '<span style="color:{point.color}">\u25CF</span> {series.name}: <b>{point.yCategory}</b><br/>'
}
});
H.seriesTypes.xrange = H.extendClass(columnType, {
pointClass: extendClass(Point, {
// Add x2 and yCategory to the available properties for tooltip formats
getLabelConfig: function () {
var cfg = Point.prototype.getLabelConfig.call(this);
cfg.x2 = this.x2;
cfg.yCategory = this.yCategory = this.series.yAxis.categories && this.series.yAxis.categories[this.y];
return cfg;
}
}),
type: 'xrange',
forceDL: true,
parallelArrays: ['x', 'x2', 'y'],
requireSorting: false,
animate: H.seriesTypes.line.prototype.animate,
/**
* Borrow the column series metrics, but with swapped axes. This gives free access
* to features like groupPadding, grouping, pointWidth etc.
*/
getColumnMetrics: function () {
var metrics,
chart = this.chart;
function swapAxes() {
each(chart.series, function (s) {
var xAxis = s.xAxis;
s.xAxis = s.yAxis;
s.yAxis = xAxis;
});
}
swapAxes();
this.yAxis.closestPointRange = 1;
metrics = columnType.prototype.getColumnMetrics.call(this);
swapAxes();
return metrics;
},
/**
* Override cropData to show a point where x is outside visible range
* but x2 is outside.
*/
cropData: function (xData, yData, min, max) {
// Replace xData with x2Data to find the appropriate cropStart
var crop = Series.prototype.cropData.call(this, this.x2Data, yData, min, max);
// Re-insert the cropped xData
crop.xData = xData.slice(crop.start, crop.end);
return crop;
},
translate: function () {
columnType.prototype.translate.apply(this, arguments);
var series = this,
xAxis = series.xAxis,
metrics = series.columnMetrics,
minPointLength = series.options.minPointLength || 0;
H.each(series.points, function (point) {
var plotX = point.plotX,
plotX2 = xAxis.toPixels(H.pick(point.x2, point.x + (point.len || 0)), true),
width = plotX2 - plotX,
widthDifference;
if (minPointLength) {
widthDifference = width < minPointLength ? minPointLength - width : 0;
plotX -= widthDifference / 2;
plotX2 += widthDifference / 2;
}
plotX = Math.max(plotX, -10);
plotX2 = Math.min(Math.max(plotX2, -10), xAxis.len + 10);
point.shapeArgs = {
x: plotX,
y: point.plotY + metrics.offset,
width: plotX2 - plotX,
height: metrics.width
};
point.tooltipPos[0] += width / 2;
point.tooltipPos[1] -= metrics.width / 2;
});
}
});
/**
* Max x2 should be considered in xAxis extremes
*/
H.wrap(H.Axis.prototype, 'getSeriesExtremes', function (proceed) {
var axis = this,
dataMax,
modMax;
proceed.call(this);
if (this.isXAxis) {
dataMax = pick(axis.dataMax, Number.MIN_VALUE);
each(this.series, function (series) {
each(series.x2Data || [], function (val) {
if (val > dataMax) {
dataMax = val;
modMax = true;
}
});
});
if (modMax) {
axis.dataMax = dataMax;
}
}
});
}(Highcharts));
// THE CHART
Highcharts.chart('container', {
chart: {
type: 'xrange'
},
title: {
text: 'Item List'
},
xAxis: {
type: 'datetime',
min: Date.UTC(2014, 11, 3)
},
yAxis: {
title: '',
categories: ['Item 1', 'Item 2'],
reversed: true
},
series: [{
name: 'Project 1',
// pointPadding: 0,
// groupPadding: 0,
borderRadius: 5,
pointWidth: 10,
data: [{
x: Date.UTC(2014, 11, 3),
x2: Date.UTC(2014, 11, 3),
y: 0
}, {
x: Date.UTC(2014, 11, 6),
x2: Date.UTC(2014, 11, 7),
y: 0
},
{
x: Date.UTC(2014, 11, 9),
x2: Date.UTC(2014, 11, 11),
y: 0
}], color: '#BF0B23'
}]
});
JSFiddle Example
In the first data point:
{
x: Date.UTC(2014, 11, 3),
x2: Date.UTC(2014, 11, 3),
y: 0
},
I want to add a marker image as the date range renders it too small to actually be displayed on the graph (like the "sun" image from this example) but I can't work out where the marker would need to be placed in my JSFiddle example.
The marker is placed on your point:
{
x: Date.UTC(2014, 11, 3),
x2: Date.UTC(2014, 11, 3),
y: 0,
// Like this:
marker: {
symbol: 'url(...)'
}
},
Related
I'm trying to insert additional data into the doughnut chart.
The controller pass to the view an array like this:
[
0 => array:3 [
"profit" => 20
"sex" => array:3 [
0 => 0
1 => 8
2 => 0
]
"count" => 8
]
1 => array:3 [
"profit" => 101.5
"sex" => array:3 [
0 => 4
1 => 4
2 => 0
]
"count" => 8
]
...
]
Using chartjs and the fied profit of all array elements I create this doughnut chart:
But I would customize the content of the tooltip so that the datas of the "sex" fieds are visible. I try with the following code but the varible data contains only the values contained in the chart.
config.options.tooltips.callbacks = {
title: (tooltipItem, data) => {
return data['labels'][tooltipItem[0]['index']];
},
label: (tooltipItem, data) => {
return data['datasets'][0]['data'][tooltipItem['index']];
},
afterLabel: (tooltipItem, data) => {
var dataset = data['datasets'][0];
var percent = Math.round((dataset['data'][tooltipItem['index']] / dataset._meta[4].total) * 100)
return `${percent} %`;
},
backgroundColor: '#FFF',
titleFontSize: 16,
titleFontColor: '#0066ff',
bodyFontColor: '#000',
bodyFontSize: 14,
displayColors: false
}
I pass the data in the config object in this way: config.data.datasets[0].data = data.map(el => el.profit);
How do I add more data to the tooltip to get something like this?
This is my code:
function createDonatsChart(ctx, title, data, labels, middleText, type) {
Chart.pluginService.register({
beforeDraw: function(chart) {
if (chart.config.options.elements.center) {
// Get ctx from string
const ctx = chart.chart.ctx;
// Get options from the center object in options
const centerConfig = chart.config.options.elements.center;
const fontStyle = centerConfig.fontStyle || 'Asap';
const txt = centerConfig.text;
const color = centerConfig.color || '#000';
const maxFontSize = centerConfig.maxFontSize || 75;
const sidePadding = centerConfig.sidePadding || 20;
const sidePaddingCalculated = (sidePadding / 100) * (chart.innerRadius * 2)
// Start with a base font of 30px
ctx.font = `30px ${fontStyle}`;
// Get the width of the string and also the width of the element minus 10 to give it 5px side padding
const stringWidth = ctx.measureText(txt).width;
const elementWidth = (chart.innerRadius * 2) - sidePaddingCalculated;
// Find out how much the font can grow in width.
const widthRatio = elementWidth / stringWidth;
const newFontSize = Math.floor(30 * widthRatio);
const elementHeight = (chart.innerRadius * 2);
// Pick a new font size so it will not be larger than the height of label.
const fontSizeToUse = Math.min(newFontSize, elementHeight, maxFontSize);
const minFontSize = centerConfig.minFontSize;
const lineHeight = centerConfig.lineHeight || 25;
const wrapText = false;
if (minFontSize === undefined) {
minFontSize = 20;
}
if (minFontSize && fontSizeToUse < minFontSize) {
fontSizeToUse = minFontSize;
wrapText = true;
}
// Set font settings to draw it correctly.
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const centerX = ((chart.chartArea.left + chart.chartArea.right) / 2);
const centerY = ((chart.chartArea.top + chart.chartArea.bottom) / 2);
ctx.font = `${fontSizeToUse}px ${fontStyle}`;
ctx.fillStyle = color;
if (!wrapText) {
ctx.fillText(txt, centerX, centerY);
return;
}
const words = txt.split(' ');
let line = '';
let lines = [];
// Break words up into multiple lines if necessary
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = ctx.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > elementWidth && n > 0) {
lines.push(line);
line = words[n] + ' ';
} else {
line = testLine;
}
}
// Move the center up depending on line height and number of lines
centerY -= (lines.length / 2) * lineHeight;
for (let n = 0; n < lines.length; n++) {
ctx.fillText(lines[n], centerX, centerY);
centerY += lineHeight;
}
//Draw text in center
ctx.fillText(line, centerX, centerY);
}
}
});
let config = {
type: 'doughnut',
data: {
datasets: [{
borderColor: '#121212',
borderWidth: 8,
backgroundColor: [
'#49C6E5',
'#EFC7C2',
'#00BD9D',
'#EF476F',
'#FFD166',
]
}],
labels: labels
},
options: {
responsive: true,
tooltips: {
},
legend: {
position: 'top',
onClick: null
},
title: {
display: true,
color: '#6c757d',
text: title,
fontFamily: "'Asap', san-serif",
fontSize: 20,
},
animation: {
animateScale: true,
animateRotate: true,
},
elements: {
center: {
text: middleText,
color: '#6c757d',
fontFamily: "'Asap', san-serif",
sidePadding: 20,
minFontSize: 12,
lineHeight: 25,
}
},
}
};
if ( type == 0 ) {
config.options.events = [];
config.data.datasets[0].data = data;
}
else {
// config.data.datasets[0].data = data.map(el => el.profit);
// config.options.tooltips.enabled = true;
// config.options.tooltips.callbacks = {
// title: (tooltipItem, data) => {
// return data['labels'][tooltipItem[0]['index']];
// },
// label: (tooltipItem, data) => {
// return data['datasets'][0]['data'][tooltipItem['index']];
// },
// afterLabel: (tooltipItem, data) => {
// var dataset = data['datasets'][0];
// var percent = Math.round((dataset['data'][tooltipItem['index']] / dataset._meta[4].total) * 100)
// return `${percent} %`;
// },
// backgroundColor: '#FFF',
// titleFontSize: 16,
// titleFontColor: '#0066ff',
// bodyFontColor: '#000',
// bodyFontSize: 14,
// displayColors: false
// }
config.data.datasets[0].data = data.map(el => el.profit);
}
Chart.defaults.global.defaultFontFamily = 'Asap';
Chart.defaults.doughnut.cutoutPercentage = 80;
new Chart(ctx, config);
}
const data = [
{
count: 8,
profit: 20,
sex: [0, 8, 0]
},
{
count: 8,
profit: 101.5,
sex: [4, 4, 0]
},
{
count: 1,
profit: 12.5,
sex: [1, 0, 0]
},
{
count: 2,
profit: 4,
sex: [2, 0, 0]
},
{
count: 5,
profit: 56.5,
sex: [5, 0, 0]
}
];
createDonatsChart(
document.getElementById('profitPerTarget').getContext('2d'),
'Target (di chi compra)',
data,
['14-17', '18-24', '25-30', '31-40', 'Over 40'],
`Totale ${(data.map(el => el.profit).reduce((a, b) => a + b, 0))} \u20AC`,
1
);
html, body {
background-color: #121212;
}
<script src="https://cdn.jsdelivr.net/npm/chart.js#2.8.0"></script>
<canvas id="profitPerTarget" height="500" style="padding: 10px"></canvas>
You can use closure in createDonatsChart functions. Set const as const originalData = [...data] and then you can access to data in afterLabel callback (as example):
tooltips: {
callbacks: {
afterLabel: function(tooltipItem, data) {
const sexArray = originalData[tooltipItem['index']].sex
const precent = sexArray.reduce((a, b) => a + b, 0) // your calculation here
return '(' + precent + '%)';
}
}
}
See example in playground: https://jsfiddle.net/denisstukalov/upw6asjm/63/#&togetherjs=3CN0LJDjbl
Can I put a blinking pointer at the end?
Or do I have to use another plugin?
I want a pointer that follows the end of the line like an image.
I want to build like a binaryoption or expertoption.
Here is my simple demo:
$(function() {
Highcharts.setOptions({
global: {
useUTC: false
}
});
// Create the chart
$('#container').highcharts('StockChart', {
chart: {
events: {
load: function() {
// set up the updating of the chart each second
var series = this.series[0];
var hasPlotLine = false,
$button = $('#button'),
chart = $('#container').highcharts();
setInterval(function() {
chart.yAxis[0].removePlotLine('plot-line-1');
var x = (new Date()).getTime(), // current time
y = Math.round(Math.random() * 100);
series.addPoint([x, y], true, true);
chart.yAxis[0].addPlotLine({
value: y,
color: 'red',
width: 2,
id: 'plot-line-1'
});
}, 1000);
}
}
},
rangeSelector: {
buttons: [{
count: 1,
type: 'minute',
text: '1M'
}, {
count: 5,
type: 'minute',
text: '5M'
}, {
type: 'all',
text: 'All'
}],
inputEnabled: false,
selected: 0
},
title: {
text: 'Live random data'
},
exporting: {
enabled: false
},
series: [{
name: 'Random data',
data: (function() {
// generate an array of random data
var data = [],
time = (new Date()).getTime(),
i;
for (i = -999; i <= 0; i += 1) {
data.push([
time + i * 1000,
Math.round(Math.random() * 100)
]);
}
return data;
}())
}]
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://code.highcharts.com/stock/highstock.js"></script>
<script src="https://code.highcharts.com/stock/modules/exporting.js"></script>
<div id="container" style="height: 400px; min-width: 310px"></div>
Also available on JsFiddle: Sample demo link
You can achieve the required result by using Highcharts. SVGRenderer class. You need to draw a path and three circles and animate them by changing their positions. For example:
chart: {
events: {
load: function() {
// set up the updating of the chart each second
var chart = this,
series = chart.series[0],
lastPoint = series.points[series.points.length - 1],
xPos = lastPoint.plotX + chart.plotLeft,
yPos = lastPoint.plotY + chart.plotTop,
pointerSize = 16;
var animatedPointGroup = this.renderer.g().attr({
translateX: xPos - pointerSize / 2,
translateY: yPos - pointerSize / 2
}).add();
var plotLine = this.renderer.path([
'M', chart.plotLeft, yPos,
'L', xPos, yPos
])
.attr({
stroke: '#008dc4',
'stroke-width': 1
})
.add();
var shadowCircle = this.renderer.circle(
pointerSize / 2,
pointerSize / 2,
pointerSize
).attr({
fill: '#d3d7de'
}).add(animatedPointGroup);
this.renderer.circle(
pointerSize / 2,
pointerSize / 2,
6
).attr({
fill: '#ffffff'
}).add(animatedPointGroup);
this.renderer.circle(
pointerSize / 2,
pointerSize / 2,
3
).attr({
fill: '#008dc4'
}).add(animatedPointGroup);
setInterval(function() {
var x = (new Date()).getTime(), // current time
y = Math.round(Math.random() * 100),
point,
xPos,
yPos;
series.addPoint([x, y], true, true);
point = series.points[series.points.length - 1];
xPos = point.plotX + chart.plotLeft;
yPos = point.plotY + chart.plotTop;
animatedPointGroup.animate({
translateX: xPos - pointerSize / 2,
translateY: yPos - pointerSize / 2
});
plotLine.animate({
d: [
'M', chart.plotLeft, yPos,
'L', xPos, yPos
]
});
shadowCircle.animate({
r: shadowCircle.attr('r') === pointerSize ? pointerSize / 2 : pointerSize
});
}, 1000);
}
}
}
Live demo: http://jsfiddle.net/BlackLabel/z4c0or3u/
API Reference:
https://api.highcharts.com/class-reference/Highcharts.SVGRenderer
https://api.highcharts.com/class-reference/Highcharts.SVGElement#animate
https://api.highcharts.com/class-reference/Highcharts.SVGElement#attr
For some reason no matter what we try the labels on our area chart series seem to have a mind of their own. Even though a short label looks like it could fit inside the area of the data, it puts it right on the end line, bleeding out of the area.
We suspect it might be due to having min and max dates that are beyond the series min and max, but these buffer zones are a requirement.
Is there an option to make labels be contained to their own series and not bleed off into whitespace?
Below is the example chart configuration and here is the JSFiddle: http://jsfiddle.net/sLqu34cn/
Highcharts.chart('container', {
chart: {
type: "area",
height: 200
},
legend: {
enabled: false
},
plotOptions: {
area: {
stacking: "percent",
pointPlacement: "on"
},
series: {
lineWidth: 0,
fillOpacity: 1,
marker: {
enabled: false
},
label: {
style: {
color: "white",
textOutline: "1px black"
}
}
}
},
series: [
{
name: "Two",
data: [[1532217600000, 1], [1532822400000, 0]],
color: "#41B6E6"
},
{
name: "Three",
data: [[1532217600000, 0], [1532822400000, 2]],
color: "#0072CE"
}
],
xAxis: {
tickWidth: 1,
title: {
enabled: false
},
labels: {
format: "{value: %b %e}"
},
max: 1533243166375,
min: 1530478366375,
type: "datetime"
},
yAxis: {
tickInterval: 20,
title: {
text: null
},
labels: {
format: "{value}%"
},
max: 100,
min: 0
},
tooltip: {}
});
Probably not the perfect solution, but you can create some customization to position the series labels. This is an example how to manually calculate the center of area in triangle shape:
Highcharts.wrap(Highcharts.Chart.prototype, 'drawSeriesLabels', function(proceed) {
proceed.apply(this, Array.prototype.slice.call(arguments, 1));
var chart = this,
plotTop = chart.plotTop,
plotLeft = chart.plotLeft,
series = chart.series,
height = chart.yAxis[0].height,
x1,
x2,
y1,
y2;
x1 = ((series[0].graphPath[1] + plotLeft) * 2 + series[0].graphPath[4] + plotLeft) / 3;
y1 = (height + plotTop + series[0].graphPath[2] + plotTop + series[0].graphPath[5] + plotTop) / 3;
x2 = (series[1].graphPath[1] + plotLeft + (series[1].graphPath[4] + plotLeft) * 2) / 3;
y2 = ((series[1].graphPath[2] + plotTop) * 2 + series[1].graphPath[5] + plotTop) / 3;
series[0].labelBySeries.attr({
x: x1,
y: y1,
align: 'center'
});
series[1].labelBySeries.attr({
x: x2,
y: y2,
align: 'center'
});
});
Live demo: http://jsfiddle.net/BlackLabel/34z8od5f/
Docs: https://www.highcharts.com/docs/extending-highcharts/extending-highcharts
I am charting different data with RickshawJS. But I need a way to update the chart when a user clicks the #search button. Right now it just creates a new chart below the old one, and that is pretty messy.
The user enters the page and enters some details and clicks the button to chart it. So ideally I'd like to start with an empty chart that isn't shown, but I can't really figure out how to remove the data from the chart and axes and then update it.
I could call $('#chart svg').remove(); on the chart and axes but it seems messy.
$('#search').click(function(event){
event.preventDefault();
var data = utils.malletData();
var graph = new Rickshaw.Graph( {
element: document.querySelector("#chart"),
width: 800,
height: 250,
series: [ {
name: data['name'],
color: 'steelblue',
data: data['series']
} ]
} );
graph.render();
var hoverDetail = new Rickshaw.Graph.HoverDetail( {
graph: graph,
xFormatter: function(x) {
var date = new Date(x).getTime();
return moment(x).format('MMMM Do YYYY, h:mm:ss a');
},
yFormatter: function(y) { return Math.floor(y) + " users" }
} );
var xAxis = new Rickshaw.Graph.Axis.X( {
graph: graph,
orientation: 'bottom',
element: document.getElementById('x_axis'),
tickFormat: function(x) { return moment(x).fromNow(); },
ticks: 7,
tickSize: 1,
} );
xAxis.render();
var ticksTreatment = 'glow';
var yAxis = new Rickshaw.Graph.Axis.Y( {
graph: graph,
orientation: 'left',
tickFormat: Rickshaw.Fixtures.Number.formatKMBT,
ticksTreatment: ticksTreatment,
element: document.getElementById('y_axis'),
} );
yAxis.render();
});
There's no official way to do so. However, you could leverage the fact that arrays in javascript are passed by reference and then update the graph.
Have a look at this demo on fiddle
var data = [
{
data: [ { x: 0, y: 120 }, { x: 1, y: 890 }, { x: 2, y: 38 }, { x: 3, y: 70 }, { x: 4, y: 32 } ],
color: "#c05020"
}, {
data: [ { x: 0, y: 80 }, { x: 1, y: 200 }, { x: 2, y: 100 }, { x: 3, y: 520 }, { x: 4, y: 133 } ],
color: "#30c020"
}
];
var graph = new Rickshaw.Graph( {
element: document.getElementById("chart"),
renderer: 'line',
height: 300,
width: 800,
series: data
} );
var y_ticks = new Rickshaw.Graph.Axis.Y( {
graph: graph,
orientation: 'left',
tickFormat: Rickshaw.Fixtures.Number.formatKMBT,
element: document.getElementById('y_axis'),
} );
graph.render();
$('button#add').click(function() {
data.push({
data: [ { x: 0, y: 200 }, { x: 1, y: 390 }, { x: 2, y: 1000 }, { x: 3, y: 200 }, { x: 4, y: 230 } ],
color: "#6060c0"
});
graph.update();
});
With a bar chart like this one, is is possible to change the width of the bars to represent another data attribute, say the weight of the fruits. The heavier the fruit is, the thicker the bar.
You play with the script here. I am open to other javascript plotting libraries that could do that as long as they are free.
$(function () {
var chart;
$(document).ready(function() {
chart = new Highcharts.Chart({
chart: {
renderTo: 'container',
type: 'column'
},
title: {
text: 'Column chart with negative values'
},
xAxis: {
categories: ['Apples', 'Oranges', 'Pears', 'Grapes', 'Bananas']
},
tooltip: {
formatter: function() {
return ''+
this.series.name +': '+ this.y +'';
}
},
credits: {
enabled: false
},
series: [{
name: 'John',
data: [5, 3, 4, 7, 2]
// I would like something like this (3.5, 6 etc is the width) :
// data: [[5, 3.4], [3, 6], [4, 3.4], [7, 2], [2, 5]]
}, {
name: 'Jane',
data: [2, -2, -3, 2, 1]
}, {
name: 'Joe',
data: [3, 4, 4, -2, 5]
}]
});
});
});
pointWidth is what you require to set the width of the bars. try
plotOptions: {
series: {
pointWidth: 15
}
}
This display bars with the width of 15px. Play around here. Just made an edit to the already existing code.
I use a set of area charts to simulate a variable-width-column/bar-chart. Say, each column/bar is represented by a rectangle area.
See my fiddle demo (http://jsfiddle.net/calfzhou/TUt2U/).
$(function () {
var rawData = [
{ name: 'A', x: 5.2, y: 5.6 },
{ name: 'B', x: 3.9, y: 10.1 },
{ name: 'C', x: 11.5, y: 1.2 },
{ name: 'D', x: 2.4, y: 17.8 },
{ name: 'E', x: 8.1, y: 8.4 }
];
function makeSeries(listOfData) {
var sumX = 0.0;
for (var i = 0; i < listOfData.length; i++) {
sumX += listOfData[i].x;
}
var gap = sumX / rawData.length * 0.2;
var allSeries = []
var x = 0.0;
for (var i = 0; i < listOfData.length; i++) {
var data = listOfData[i];
allSeries[i] = {
name: data.name,
data: [
[x, 0], [x, data.y],
{
x: x + data.x / 2.0,
y: data.y,
dataLabels: { enabled: true, format: data.x + ' x {y}' }
},
[x + data.x, data.y], [x + data.x, 0]
],
w: data.x,
h: data.y
};
x += data.x + gap;
}
return allSeries;
}
$('#container').highcharts({
chart: { type: 'area' },
xAxis: {
tickLength: 0,
labels: { enabled: false}
},
yAxis: {
title: { enabled: false}
},
plotOptions: {
area: {
marker: {
enabled: false,
states: {
hover: { enabled: false }
}
}
}
},
tooltip: {
followPointer: true,
useHTML: true,
headerFormat: '<span style="color: {series.color}">{series.name}</span>: ',
pointFormat: '<span>{series.options.w} x {series.options.h}</span>'
},
series: makeSeries(rawData)
});
});
Fusioncharts probably is the best option if you have a license for it to do the more optimal Marimekko charts…
I've done a little work trying to get a Marimekko charts solution in highcharts. It's not perfect, but approximates the first Marimekko charts example found here on the Fusion Charts page…
http://www.fusioncharts.com/resources/chart-tutorials/understanding-the-marimekko-chart/
The key is to use a dateTime axis, as that mode provides you more flexibility for the how you distribute points and line on the X axis which provides you the ability to have variably sized "bars" that you can construct on this axis. I use 0-1000 second space and outside the chart figure out the mappings to this scale to approximate percentage values to pace your vertical lines. Here ( http://jsfiddle.net/miken/598d9/2/ ) is a jsfiddle example that creates a variable width column chart.
$(function () {
var chart;
Highcharts.setOptions({
colors: [ '#75FFFF', '#55CCDD', '#60DD60' ]
});
$(document).ready(function() {
var CATEGORY = { // number out of 1000
0: '',
475: 'Desktops',
763: 'Laptops',
1000: 'Tablets'
};
var BucketSize = {
0: 475,
475: 475,
763: 288,
1000: 237
};
chart = new Highcharts.Chart({
chart: {
renderTo: 'container',
type: 'area'
},
title: {
text: 'Contribution to Overall Sales by Brand & Category (in US$)<br>(2011-12)'
},
xAxis: {
min: 0,
max: 1000,
title: {
text: '<b>CATEGORY</b>'
},
tickInterval: 1,
minTickInterval: 1,
dateTimeLabelFormats: {
month: '%b'
},
labels: {
rotation: -60,
align: 'right',
formatter: function() {
if (CATEGORY[this.value] !== undefined) {
return '<b>' + CATEGORY[this.value] + ' (' +
this.value/10 + '%)</b>';
}
}
}
},
yAxis: {
max: 100,
gridLineWidth: 0,
title: {
text: '<b>% Share</b>'
},
labels: {
formatter: function() {
return this.value +'%'
}
}
},
tooltip: {
shared: true,
useHTML: true,
formatter: function () {
var result = 'CATEGORY: <b>' +
CATEGORY[this.x] + ' (' + Highcharts.numberFormat(BucketSize[this.x]/10,1) + '% sized bucket)</b><br>';
$.each(this.points, function(i, datum) {
if (datum.point.y !== 0) {
result += '<span style="color:' +
datum.series.color + '"><b>' +
datum.series.name + '</b></span>: ' +
'<b>$' + datum.point.y + 'K</b> (' +
Highcharts.numberFormat(
datum.point.percentage,2) +
'%)<br/>';
}
});
return (result);
}
},
plotOptions: {
area: {
stacking: 'percent',
lineColor: 'black',
lineWidth: 1,
marker: {
enabled: false
},
step: true
}
},
legend: {
layout: 'vertical',
align: 'right',
verticalAlign: 'top',
x: 0,
y: 100,
borderWidth: 1,
title: {
text : 'Brand:'
}
},
series: [ {
name: 'HP',
data: [
[0,298],
[475,109],
[763,153],
[1000,153]
]
}, {
name: 'Dell',
data: [
[0,245],
[475,198],
[763,120],
[1000,120]
]
}, {
name: 'Sony',
data: [
[0,335],
[475,225],
[763,164],
[1000,164]
]
}]
},
function(chart){
// Render bottom line.
chart.renderer.path(['M', chart.plotLeft, chart.plotHeight + 66, 'L', chart.plotLeft+chart.plotWidth, chart.plotHeight + 66])
.attr({
'stroke-width': 3,
stroke: 'black',
zIndex:50
})
.add();
for (var category_idx in CATEGORY) {
chart.renderer.path(['M', (Math.round((category_idx / 1000) * chart.plotWidth)) + chart.plotLeft, 66, 'V', chart.plotTop + chart.plotHeight])
.attr({
'stroke-width': 1,
stroke: 'black',
zIndex:4
})
.add();
}
});
});
});
It adds an additional array to allow you to map category names to second tic values to give you a more "category" view that you might want. I've also added code at the bottom that adds vertical dividing lines between the different columns and the bottom line of the chart. It might need some tweaks for the size of your surrounding labels, etc. that I've hardcoded in pixels here as part of the math, but it should be doable.
Using a 'percent' type accent lets you have the y scale figure out the percentage totals from the raw data, whereas as noted you need to do your own math for the x axis. I'm relying more on a tooltip function to provide labels, etc than labels on the chart itself.
Another big improvement on this effort would be to find a way to make the tooltip hover area and labels to focus and be centered and encompass the bar itself instead of the right border of each bar that it is now. If someone wants to add that, feel free to here.
If I got it right you want every single bar to be of different width. I had same problem and struggled a lot to find a library offering this option. I came to the conclusion - there's none.
Anyways, I played with highcharts a little, got creative and came up with this:
You mentioned that you'd like your data to look something like this: data: [[5, 3.4], [3, 6], [4, 3.4]], with the first value being the height and the second being the width.
Let's do it using the highcharts' column graph.
Step 1:
To better differentiate the bars, input each bar as a new series. Since I generated my data dynamically, I had to assign new series dynamically:
const objects: any = [];
const extra = this.data.length - 1;
this.data.map((range) => {
const obj = {
type: 'column',
showInLegend: false,
data: [range[1]],
animation: true,
borderColor: 'black',
borderWidth: 1,
color: 'blue'
};
for (let i = 0; i < extra; i++) {
obj.data.push(null);
}
objects.push(obj);
});
this.chartOptions.series = objects;
That way your different series would look something like this:
series: [{
type: 'column',
data: [5, 3.4]
}, {
type: 'column',
data: [3, 6]
}, {
type: 'column',
data: [4, 3.4]
}]
Step 2:
Assign this as plot options for highcharts:
plotOptions: {
column: {
pointPadding: 0,
borderWidth: 0,
groupPadding: 0,
shadow: false
}
}
Step 3:
Now let's get creative - to have the same starting point for all bars, we need to move every single one to the graph's start:
setColumnsToZero() {
this.data.map((item, index) => {
document.querySelector('.highcharts-series-' + index).children[0].setAttribute('x', '0');
});
}
Step 4:
getDistribution() {
let total = 0;
// Array including all of the bar's data: [[5, 3.4], [3, 6], [4, 3.4]]
this.data.map(item => {
total = total + item[0];
});
// MARK: Get xAxis' total width
const totalWidth = document.querySelector('.highcharts-axis-line').getBoundingClientRect().width;
let pos = 0;
this.data.map((item, index) => {
const start = item[0];
const width = (start * totalWidth) / total;
document.querySelector('.highcharts-series-' + index).children[0].setAttribute('width', width.toString());
document.querySelector('.highcharts-series-' + index).children[0].setAttribute('x', pos.toString());
pos = pos + width;
this.getPointsPosition(index, totalWidth, total);
});
}
Step 4:
Let's get to the xAxis' points. In the first functions modify the already existing points, move the last point to the end of the axis and hide the others. In the second function we clone the last point, modify it to have either 6 or 3 total xAxis points and move each of them to the correct position
getPointsPosition(index, totalWidth, total) {
const col = document.querySelector('.highcharts-series-' + index).children[0];
const point = (document.querySelector('.highcharts-xaxis-labels').children[index] as HTMLElement);
const difference = col.getBoundingClientRect().right - point.getBoundingClientRect().right;
const half = point.getBoundingClientRect().width / 2;
if (index === this.data.length - 1) {
this.cloneNode(point, difference, totalWidth, total);
} else {
point.style.display = 'none';
}
point.style.transform = 'translateX(' + (+difference + +half) + 'px)';
point.innerHTML = total.toString();
}
cloneNode(ref: HTMLElement, difference, totalWidth, total) {
const width = document.documentElement.getBoundingClientRect().width;
const q = total / (width > 1000 && ? 6 : 3);
const w = totalWidth / (width > 1000 ? 6 : 3);
let val = total;
let valW = 0;
for (let i = 0; i < (width > 1000 ? 6 : 3); i++) {
val = val - q;
valW = valW + w;
const clone = (ref.cloneNode(true) as HTMLElement);
document.querySelector('.highcharts-xaxis-labels').appendChild(clone);
const half = clone.getBoundingClientRect().width / 2;
clone.style.transform = 'translateX(' + (-valW + difference + half) + 'px)';
const inner = Math.round(val * 100) / 100;
clone.innerHTML = inner.toString();
}
}
In the end we have a graph looking something like this (not the data from this given example, but for [[20, 0.005], [30, 0.013333333333333334], [20, 0.01], [30, 0.005555555555555555], [20, 0.006666666666666666]] with the first value being the width and the second being the height):
There might be some modifications to do to 100% fit your case. F.e. I had to adjust the xAxis' points a specific starting and end point - I spared this part.