How to position NVD3 line graph above all other area / bar graphs? - javascript

The Plunker example
In the example above you can see that the line is rendered under the orange area graph:
Was reading this trend here, then I tried this d3.select('svg#chart .lines1Wrap').moveToFront();, but got the error moveToFront is not a function.
Chart code
var data = [{
"key": "Price",
"type": "line",
"yAxis": 2,
"values": [
[1443621600000, 71.89],
[1443619800000, 75.51],
[1443618000000, 12.49],
[1443616200000, 20.72],
[1443612600000, 70.39],
[1443610800000, 59.77],
]
}, {
"key": "Quantity1",
"type": "area",
"yAxis": 1,
"values": [
[1136005200000, 1],
[1138683600000, 5],
[1141102800000, 10],
[1143781200000, 0],
[1146369600000, 1],
[1149048000000, 0],
]
}];
data = data.map(function(series) {
series.values = series.values.map(function(d) {
return {
x: d[0],
y: d[1]
}
});
return series;
});
nv.addGraph(function() {
var chart = nv.models.multiChart()
.margin({
top: 20,
right: 40,
bottom: 50,
left: 40
})
.yDomain1([0, 10])
.yDomain2([0, 100]) // hard-coded :<
.interpolate("linear") // don't smooth out the lines
.color(d3.scale.category10().range());
chart.xAxis.tickFormat(function(d) {
return d3.time.format('%I:%M')(new Date(d));
});
chart.yAxis1.tickFormat(d3.format(',f'));
chart.yAxis2.tickFormat(function(d) {
return '$' + d3.format(',f')(d)
});
d3.select('svg#chart')
.datum(data)
.transition().duration(500).call(chart);
d3.selection.prototype.moveToFront = function() {
return this.each(function() {
this.parentNode.appendChild(this);
});
};
d3.select('svg#chart .lines1Wrap').moveToFront();
chart.update();
nv.utils.windowResize(chart.update);
return chart;
});
UPDATE
Found this answer here, tried to use the solution:
d3.selection.prototype.moveToFront = function() {
return this.each(function() {
this.parentNode.appendChild(this);
});
};
d3.selection.prototype.moveToBack = function() {
return this.each(function() {
var firstChild = this.parentNode.firstChild;
if (firstChild) {
this.parentNode.insertBefore(this, firstChild);
}
});
};
d3.select('svg#chart .lines1Wrap').moveToFront();
d3.select('svg#chart .nv-areaWrap').moveToBack();
No more error, however the blue line graph still is not moved in front of all the others.

Add this to move your line(line chart) DOM element after the line(Area Chart) DOM element.
d3.select('.lines2Wrap').node().parentNode.insertBefore(d3.select('.stack1Wrap').node(), d3.select('.lines2Wrap').node());
Full working code here
Hoe this helps! :)

As I struggled with a similar problem for two days I will post my solution here in case it helps someone.
In my chart settings I have dispatch function which will append a rectangle to the chart and then lower it to the bottom:
dispatch: {
renderEnd: () => {
drawThresholds();
lowerThresholdAreas();
}
}
In the drawThresholds I draw the actual rectangle:
const drawThresholds = () => {
try {
let svgContainer = d3.select(`svg g`);
svgContainer.append('rect')
.attr('x', 0)
.attr('y', 2)
.attr('width', 200)
.attr('height', 200)
.attr('class', 'threshold_area')
.attr('fill', 'yellow');
}
catch (err) {
// Selector throws an error instead of returning empty list
// when element is not found.
// Ignore exceptions, they occur because page is not loaded yet.
}
};
And finally after rectangles are appended, I need to place them to the bottom by calling lowerFunction. use d3 selectAll to get all .threshold_area classes and then insert them before the line.
const lowerThresholdAreas = () => {
d3.selectAll('.threshold_area').each(function () {
this.parentNode.insertBefore(this, this.parentNode.firstChild);
});
};
And what is happening, is that on SVG canvas there is no z-index. It means the order of appends defines the layers, meaning if you append the rectangle as last item, it will be on top. So the order of elements needs to be changed.
Also a step where I did mistake was that I first used ES6 function declaration and it doesn't work with "this" as I supposed it would.
Read more about selection: https://github.com/d3/d3-selection/blob/master/README.md#select
Read more about lowering or raising items: https://github.com/d3/d3-selection/blob/master/README.md#selection_lower
Here is a working plunker for seeing it in action: https://plnkr.co/edit/QI2HcxcJYRAv0FzXWihd?p=preview

I think it's just that on your particular graph, the line series is under lines2wrap, not lines1wrap. IIRC, this has to do with which series is 'first'. In your plunker, I changed the 1 to a 2 and it worked.
It's a hacky workaround (that's mine from Github, thanks for notifying me), and probably requires some tinkering to be more generally useful.

Related

How to highlight a line when mouse over it in observable plot using JavaScript?

My chart looks like so::
and here is my code:
linePlot = Plot.plot({
marginLeft: 60, // space to the left of the chart
y: {
type: "log", // set the type
},
marks: [
Plot.line(data, {x: "timestamp", y: "views", z:"artist", title: d=>`${d.artist}`,})
]
})
I want to highlight or change color of each line when the mouse is over it.
The easiest thing to do would be to attach a pointerenter event to the lines. Since you're using Observable, to use D3 to handle that process. Here's what it looks like on Observable:
https://observablehq.com/d/2e1daf099a7aaaea
To be clear, you are using two libraries: D3 and Plot, both of which are automatically available on Observable. You can use them both in vanilla Javascript pretty easily, though:
// Manufacture some data
let pt_lists = d3.range(10).map(() => {
let cur = 0;
return d3.range(1000).map(function(x) {
let step = 2 * d3.randomInt(0, 2)() - 1;
cur = cur + step;
return [x, cur];
});
});
// Plot the data
let plot = Plot.plot({
marks: pt_lists.map((pts) => Plot.line(pts, {
strokeWidth: 2
}))
});
// Here's where the action is.
// We use d3 to select all the paths in the plot
d3.select(plot)
.selectAll("path")
// React when the pointer hovers over the path.
.on("pointerenter", function() {
d3.select(plot).selectAll("path").attr("opacity", 0.2);
d3.select(this).attr("opacity", 1);
});
// Reset the appearance when the pointer leaves the SVG
d3.select(plot).on("pointerleave", function() {
d3.select(plot).selectAll("path").attr("opacity", 1);
});
// Attach the plot to the container DIV
d3.select('#chart').append(() => plot)
<script src="https://cdn.jsdelivr.net/npm/d3#7"></script>
<script src="https://cdn.jsdelivr.net/npm/#observablehq/plot#0.6"></script>
<div id="chart" style="width: 600px; height: 400px"></div>
It might also be possible to do the interaction in css:
d3.select(chart)
.append("svg:style")
.text(`
path:hover {stroke-width: 2px;}
`)

Plotly events not working with Spotfire Mods

While trying to create a funnel chart Mod in Spotfire using Plotly.js library, somehow plotly events are not working. Following the plotly documentation, we tried to add "plotly_selected", "plotly_hover" and "plotly_unhover" events which are triggered in the connecting area between the points rather than actual points.
We notice that the events are triggered when the mouse pointer is outside of the plotly chart elements. However, when we move the mouse pointer over the chart elements, the events are not triggered. We are unsure as to how to troubleshoot further. The code we have written works in a web browser, but not inside our application (Spotfire).
Below is Data preparation for chart:
for (const colorLeaf of colorLeafNodes) {
data.push({
type: "funnel",
name: colorLeaf.formattedPath(),
y: colorLeaf.rows().map((y) => y.categorical("Category").formattedValue()),
x: colorLeaf.rows().map((x) => x.continuous("Value Axis").value()),
textposition: "inside",
hoverinfo: "none",
textinfo: "value+percent initial",
textfont: {
family: styling.general.font.fontFamily,
size: styling.general.font.fontSize,
color: styling.general.font.color.bgBlack
},
marker: {
color: colorLeaf.rows().map((row) => row.color().hexCode)
}
});
}
Here is the Chart Div:
var chartDiv = document.getElementById("mod-container");
Plotly marking event
chartDiv
.on("plotly_selected", function (eventData) {
console.log("plotly_selected");
console.log(eventData.points);
let categories = [];
let colorValues = [];
if (eventData != null) {
if (eventData.points.length == 0) {
dataView.clearMarking();
return;
}
eventData.points.forEach((d) => {
console.log(d);
categories.push(d.y);
colorValues.push(d.data.name);
});
let rowsToMark = rows.filter(
(d) =>
categories.includes(d.categorical("Category").formattedValue()) &&
colorValues.includes(d.categorical("Color").formattedValue())
);
dataView.mark(rowsToMark);
}
})
After deeper troubleshooting, we found that CSS code was the troublemaker. After removing .points g path { pointer-events: all; } from CSS, plotly events were working smoothly with Mods.

Chart js is it possible to assign space between the legend and the chart? [duplicate]

I have a bar chart where I have drawn 3 vertical lines, each with it's own label at the top. I would like those labels to be above the top of the y-axis (above the 30% line in the example) but below the legend. I can't figure out how to increase the space between the top legend and the chart such that I can have my vertical line labels (15, 24 & 33) be off of the chart itself but below the legend. Any ideas?
If you want do increase spacing in all charts you can put this code before creating :
Chart.Legend.prototype.afterFit = function() {
this.height = this.height + 50;
};
Of course, I don't try but i think you can change it (or copy the original Chart object before, to keep the original padding).
Bye,
If you want to apply padding below legend for some charts only in your app:
ChartJS >= 2.1.0
Chart.plugins.register({
id: 'paddingBelowLegends',
beforeInit: function(chart, options) {
chart.legend.afterFit = function() {
this.height = this.height + 50;
};
}
});
// ----------------------------------
// disable the plugin only for charts
// where you DO NOT WANT the padding
// ----------------------------------
// for raw ChartJS use:
var chart = new Chart(ctx, {
config: {
plugins: {
paddingBelowLegends: false
}
}
});
// for angular-chartjs:
$scope.myChart.options.plugins = { paddingBelowLegends: false }
// then in template:
// <canvas class="chart ..." chart-options="myChart.options" ... />
ChartJS >= 2.5.0
Specific plugins for each chart are supported, it should be possible to do:
var chart = new Chart(ctx, {
plugins: [{
beforeInit: function(chart, options) {
chart.legend.afterFit = function() {
this.height = this.height + 50;
};
}
}]
});
See ChartJS documentation + inspired by this other answer
Unfortunately, since there is no config option to handle this the only way you can achieve the desired result is to extend Chart.Legend and implement the afterFit() callback.
Here is a quick codepen showing how to do just that. To change the spacing, just change the value in line 9 (currently set to 50). Also, this of course only works with the legend at the top. Hopefully, the example is clear enough for you to modify in case you want to move your legend elsewhere.
If anyone is wondering why the afterFit solution is not working in Chart.js 3.3.0 it is because afterFit function was removed from the legend plugin.
If you want to make this work anyway by taking advantage over the fit function, you can try this hacky solution / workaround:
const plugin = {
beforeInit(chart) {
// Get reference to the original fit function
const originalFit = chart.legend.fit;
// Override the fit function
chart.legend.fit = function fit() {
// Call original function and bind scope in order to use `this` correctly inside it
originalFit.bind(chart.legend)();
// Change the height as suggested in another answers
this.height += 15;
}
};
}
I know that this is not an ideal solution, but until we have native support for this legend padding, I'm afraid this is as good we can do right now.
This helped me after 2 days of research.
Chart.Legend.prototype.afterFit = function() {
this.height = this.height + 50;
};
update this in module.ts file
I'm using react-chartjs-2 (but this is just a port and uses the same configurations object) and I was able to achieve that by changing the labels configuration nested on legend configuration:
chartOptions = {
legend: {
labels: {
padding: 50 -> this one.
}
},
You can check the property description here:
https://www.chartjs.org/docs/latest/configuration/legend.html
Hope it helps.
I have tried the above approaches on my react(react-chartjs-2) project but no luck. Here I have one different approach like creating custom legends outside the chart. so you can get more control over it
Hide default legend
Get legend object using any ref method.
Loop and make a custom legend using html and css.
Sample codesanbox
I am aware that OP is not about reactjs component, but as it is a common issue it will help someone.
Orginal reference
.
For ng2-charts#^3.1.0, following this answer works with an addition:
this.options.labels.padding = 40;
//this.height += 15;
or the title.padding config (this'll create an invisible title under the graph so it's a bit hacky):
plugins: {
legend: {
display: true,
position: 'bottom',
title: {
display: true,
padding: 10,
},
Stackblitz
After searching the chart.js file I found out that the height of the labels bar is defined there in
height = this._fitRows(titleHeight, fontSize, boxWidth, itemHeight)
OR
this.height = Math.min(height, options.maxHeight || this.maxHeight)
in the fit() function
fit() {
const {options, ctx} = this;
if (!options.display) {
this.width = this.height = 0;
return;
}
const labelOpts = options.labels;
const labelFont = toFont(labelOpts.font);
const fontSize = labelFont.size;
const titleHeight = this._computeTitleHeight();
const {boxWidth, itemHeight} = getBoxSize(labelOpts, fontSize);
let width, height;
ctx.font = labelFont.string;
if (this.isHorizontal()) {
width = this.maxWidth;
height = this._fitRows(titleHeight, fontSize, boxWidth, itemHeight);
} else {
height = this.maxHeight;
width = this._fitCols(titleHeight, fontSize, boxWidth, itemHeight);
}
this.width = Math.min(width, options.maxWidth || this.maxWidth);
this.height = Math.min(height, options.maxHeight || this.maxHeight) + 40;}
And I changed it to +40 as shown above. and it worked fine for me so I wanted to share.
If you want to apply padding below legend for some charts only in your app:
ChartJS >= 2.1.0
Note: make sure to add this in plugins and not inside options.plugins.
Chart.plugins.register({
id: 'paddingBelowLegends',
beforeInit: function(chart, options) {
chart.legend.afterFit = function() {
this.height = this.height + 50;
};
}
});
// ----------------------------------
// disable the plugin only for charts
// where you DO NOT WANT the padding
// ----------------------------------
// for raw ChartJS use:
var chart = new Chart(ctx, {
config: {
plugins: {
paddingBelowLegends: false
}
}
});
For React Users using react-chartjs-2:
import { Line } from "react-chartjs-2";
import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend } from "chart.js";
ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip, Legend);
<Line
data={{
datasets: trendsData?.map((trend, idx) => ({
type: "line",
label: trend.domainName,
data: trend.domainTrends.map(d => d.value),
backgroundColor: getDomainColor(idx).backgroundColor,
borderColor: getDomainColor(idx).color,
pointRadius: 0,
tension: 0.3
})),
labels: trendsData?.[0]?.domainTrends.map(d => d.date)
}}
options={{
plugins: {
legend: {
display: true,
align: "start",
labels: {
font: { size: 14 }
}
}
}
}}
plugins={[
{
id: "increase-legend-spacing",
beforeInit(chart) {
// Get reference to the original fit function
const originalFit = (chart.legend as any).fit;
// Override the fit function
(chart.legend as any).fit = function fit() {
// Call original function and bind scope in order to use `this` correctly inside it
originalFit.bind(chart.legend)();
this.height += 20;
};
}
}
]}
/>
I know this is not what OP really wants but at least for newer version, and the one I use - 4.0.1 - there is no way to increase the margin between the legend box and the chart, that I have been able to find at least, so this is an obvious workaround. So in order to avoid this problem:
I had to change the position of the leyend box below the chart with this option configurations:
options = {
layout: {
padding: 30
},
parsing: {
key: 'nested.value'
},
plugins: {
datalabels: {
color: '#36A2EB'
},
tooltip: {
enabled: true
},
legend: {
position: 'bottom',
align: 'center',
labels: {
padding: 20,
}
}
}
};
Note: To add the values at the top of the bars I had to add the npm package chartjs-plugin-datalabels with the following features inside your datasets config:
datasets: [
{
label: "Ejercido",
data: source.map( s => s.ejercidoTotal),
datalabels: {
anchor: 'end',
clamp: true,//this makes one datalabel not visible if it crashed with other one
display: 'auto',
align: 'top',
color: '#333333',
formatter
}
},
...
]
With this as the end result:
If you are using react-chartjs-2 library to show chart in a React app. You can use the below solution:
const plugin = {
beforeInit: function (chart) {
// Get reference to the original fit function
const originalFit = chart.legend.fit
// Override the fit function
chart.legend.fit = function fit() {
// Bind scope in order to use `this` correctly inside it
originalFit.bind(chart.legend)()
this.height += 20 // Change the height
}
}
}
export const ReactChart2Example = (props) => {
const { data = [] } = props;
return (
<div>
<Chart plugins={[plugin]} type ="bar" data={data}/>
</div>
);
};
We have to pass plugins prop separately in React js Chart component and not inside options.
Reference: https://github.com/chartjs/Chart.js/issues/10388#issuecomment-1217363379
You can use layout property under options. This helped me:
layout: {
padding: {
left: 50,
right: 130,
top: 0,
bottom: 0
}
}

d3 tooltip | passing in a variable

I have a webpage with 4 d3 charts for each of 11 different regions. One of those charts is an area chart and the code snippet is:
for (mm=0; mm<regions.length; mm++) {
areas.append("path")
.attr("class",function(d,i){ return "area cons ar"+i+" region"+mm;} )
.attr("id", function(d,i) { return "area_"+i+"_"+mm})
.attr("d", function(d,i) { return area(d)} );
var test = d3.selectAll("path.region"+mm)
.call(d3.helper.tooltip()
.attr("class", function(d, i) { return "tooltip"; })
.text(function(d, i){
console.log(mm);
return "i "+consSubProducts[i]+', i: '+i;}));
}
I want to add tooltips to the charts. In the area plot, each region has different products. Some have 7 products, others have 5. I need to use the mm variable at its runtime values (0-10) to call the correct product array where consSubProducts is currently. (ConsSubProducts is set to a different product array at the top of the for...loop, but as with mm, the code can only see the finally-set array and not the runtime arrays.)
In this code, mm always returns 11, i.e. it returns the final value of mm rather than the values at runtime.
I have tried passing mm in within tooltip() and within .text(function(d,i,mm) - the latter clearly doesn't work as it's expecting a j. I've also tried attaching mm to the class or ID of an object, but within the call() console.log(this) logs object.window.
I've tried modifying tooltip.js but although I can generate the label I want I can't work out how to override the text. Tooltip.js:
d3.helper = {};
d3.helper.tooltip = function(){
var tooltipDiv;
var bodyNode = d3.select('body').node();
var attrs = {};
var text = '';
var styles = {};
function tooltip(selection){
selection.on('mouseover.tooltip', function(pD, pI){
var name, value;
// Clean up lost tooltips
d3.select('body').selectAll('div.tooltip').remove();
// Append tooltip
tooltipDiv = d3.select('body').append('div');
tooltipDiv.attr(attrs);
tooltipDiv.style(styles);
var absoluteMousePos = d3.mouse(bodyNode);
tooltipDiv.style({
left: (absoluteMousePos[0] + 10)+'px',
top: (absoluteMousePos[1] - 15)+'px',
position: 'absolute',
'z-index': 1001
});
// Add text using the accessor function, Crop text arbitrarily
tooltipDiv.style('width', function(d, i){ return (text(pD, pI).length > 80) ? '300px' : null; })
.html(function(d, i){return text(pD, pI);});
})
.on('mousemove.tooltip', function(pD, pI){
// Move tooltip
var absoluteMousePos = d3.mouse(bodyNode);
tooltipDiv.style({
left: (absoluteMousePos[0] + 10)+'px',
top: (absoluteMousePos[1] - 15)+'px'
});
// Keep updating the text, it could change according to position
tooltipDiv.html(function(d, i){ return text(pD, pI); });
})
.on('mouseout.tooltip', function(pD, pI){
// Remove tooltip
tooltipDiv.remove();
});
}
tooltip.attr = function(_x){
if (!arguments.length) return attrs;
attrs = _x;
return this;
};
tooltip.style = function(_x){
if (!arguments.length) return styles;
styles = _x;
return this;
};
tooltip.text = function(_x){
if (!arguments.length) return text;
text = d3.functor(_x);
return this;
};
return tooltip;
};
Any help appreciated!
It looks like this is way over the top for something that is super simple. Unless I'm missing something. Let me post some code for how I handle tooltips.
Predefine the tooltip div in the page HTML and class it .tooltip and apply the css of display:none; position:absolute; and add .tooltip.show{display:block;}
var tooltip = d3.select('.tooltip');
function tipUpdate(d,i) {
tooltip.classed('show',true).html(d+" : "+i);
}
areas.append('path')
//all your path attributs
.on('mouseover', tipUpdate)
.on('mouseout', function() { tooltip.classed('show',false); });
areas.on('mousemove', function(d,i) {
var mouse = d3.mouse('body').map( function(d) { return parseInt(d); });
tooltip.attr("style", "left:"+(mouse[0]+10)+"px;top:"+(mouse[1]-15)+"px");
});
This will show the tooltip div on mouseover of the path and move the tooltip relative to it's position in the body and hide the tooltip when the mouse leaves the path. It will display the data and index of the point in the path that it is currently over. You might want to change that but it should be easy.
One problem I thought about later is you cannot do a working tooltip like this on a path. It will always show the data from the last point in the path.
I have posted a working fiddle. D3 tooltip

Wijmo BarGraph - using an image on the axis

I am using Wijmo barcharts and am trying to create a graph which has images instead of labels on the x axis.
This is the code I have currently got, however, the image source is being printed out as a string rather than showing the image. Does anybody know of a way around this?
$(document).ready(function () {
defaultPalette = ['#e11a00', '#ddcd0e', '#005698'];
$("#wijbarchart").wijbarchart({
horizontal:false,
hint: {
content: function () {
return this.data.label + '\n ' + this.y + '';
}
},
seriesList: [{
label: "Entries",
legendEntry: true,
data: {
x: [<img src="photos/image1.jpg" />,<img src="photos/image2.jpg" />,<img src="photos/image3.jpg" />],
y: [22,10,65]
}
}],
painted: function (args) {
var bars = $(this).data('fields').chartElements.bars
if (bars.length > 0) {
for (var i in bars) {
bars[i].attr({ fill: defaultPalette[i % defaultPalette.length]});
}
}
},
mouseOut: function (e, data) {
data.bar.attr({ fill: defaultPalette[data.index % defaultPalette.length]});
}
});
});
Thanks!
Currently, there is no support for displaying images in place of valuelabels. However, I have made an enhancement for same to the development team and hopefully, it will be supported in future builds.
Regards
Ashish

Categories