Vertical legend in pieChart in one column in nvd3 - legend

I need vertical legend in PieChart.
Now library provide only 2 options: top/right.
If use right - legend is in several columns. I need legend in one column.
I found one hack - correct transform value and put legend in one column.
var positionX = 30;
var positionY = 30;
var verticalOffset = 25;
d3.selectAll('.nv-legend .nv-series')[0].forEach(function(d) {
positionY += verticalOffset;
d3.select(d).attr('transform', 'translate(' + positionX + ',' + positionY + ')');
});
It works, but If I click to legend to update it - legend return to start position (several columns).
JSFiddle example

A workaround for this is to update the legend for every click and double click of .nv-legend.
(function() {
var h = 600;
var r = h / 2;
var arc = d3.svg.arc().outerRadius(r);
var data = [{
"label": "Test 1",
"value": 74
}, {
"label": "Test 2",
"value": 7
}, {
"label": "Test 3",
"value": 7
}, {
"label": "Test 4",
"value": 12
}];
var colors = [
'rgb(178, 55, 56)',
'rgb(213, 69, 70)',
'rgb(230, 125, 126)',
'rgb(239, 183, 182)'
]
nv.addGraph(function() {
var chart = nv.models.pieChart()
.x(function(d) {
return d.label
})
.y(function(d) {
return d.value
})
.color(colors)
.showLabels(true)
.labelType("percent");
d3.select("#chart svg")
.datum(data)
.transition().duration(1200)
.call(chart);
var svg = d3.select("#chart svg");
function updateLegendPosition() {
svg.selectAll(".nv-series")[0].forEach(function(d, i) {
d3.select(d).attr("transform", "translate(0," + i * 15 + ")");
})
}
svg.select('.nv-legend').on("click", function() {
updateLegendPosition();
});
svg.select('.nv-legend').on("dblclick", function() {
updateLegendPosition();
});
updateLegendPosition();
return chart;
});
}())
#import url(http://fonts.googleapis.com/css?family=Droid+Sans|Droid+Sans+Mono);
#chart svg {
height: 600px;
}
.nv-label text{
font-family: Droid Sans;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.1.15-beta/nv.d3.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.1.15-beta/nv.d3.min.js"></script>
<div id="chart">
<svg></svg>
</div>

A Quick Hack.
Modify the nv.d3.js
Line 11149 (May differ on other versions )
// Legend
if (showLegend) {
if (legendPosition === "top") {
Add another option vertical
} else if (legendPosition === "vertical") {
var legendWidth = nv.models.legend().width();
var positionY = 10;
var positionX = availableWidth - 200;
var verticalOffset = 20;
if ( margin.top != legend.height()) {
margin.top = legend.height();
availableHeight = nv.utils.availableHeight(height, container, margin);
}
legend.width(200);
// legend.height(availableHeight).key(pie.x());
positionY += verticalOffset;
wrap.select('.nv-legendWrap')
.datum(data)
.call(legend)
.attr('transform', 'translate(' + positionX + ',' + positionY + ')');
}
Also can play with variables.
Works very nice with Legends. For a very long list of legends. More logic want to apply.

I know this is an old question, but I had the same one and ended up here. None of the answers worked for me, but I was able to modfiy Ranijth's answer and get it working (and looking nice).
else if (legendPosition === "vertical") {
var pad=50;
var legendWidth=150; //might need to change this
legend.height(availableHeight).key(pie.x());
legend.width(legendWidth);
wrap.select('.nv-legendWrap')
.datum(data)
.call(legend)
.attr('transform', 'translate('+ ((availableWidth / 2)+legendWidth+pad)+','+pad+')');
}
Pie Chart with Vertical Legend

Related

Adding markerclustergroup to leaflet overlay does not update in Vue.js app

I have an overlay control on my leaflet map in a vue.js application. It contains two layers. The ILZipCodes layer renders perfectly. However, the "markers" layer does not appear when I select the checkbox in the layer control. Why is this? I reckon I may be setting up the layer control and populating the clusters in the wrong sequence.
Cheers
<template>
<div>
<div class="mx-auto my-6 loader" v-if="isSearching"></div>
<div class="tooManyResultsError" v-if="tooManyResults">
Your search brought back too many results to display. Please zoom in or refine your search with the text box and
filters.
</div>
<div id="leafletMapId"></div>
</div>
</template>
<script>
//The center coordinate and zoom level that the map should initialize to
// to capture all of the continental United States
const INITIAL_CENTER_LATITUDE = 34;
const INITIAL_CENTER_LONGITUDE = -96;
const INITIAL_ZOOM = 4;
/* Leaflet performs slowly with reactivity ("observable" data object variables).
* To work around this, I removed myMap and markers from the data object.
*/
/* If the user zooms out or pans beyond the boundaries of the previous fetch's dataset, then
* the client fetches location data from the server to replace the previous map data. If the user
* zooms or pans but stays within the boundaries of the dataset currently displayed on the map, then
* the client does not run another fetch.
* */
import axios from "axios";
import store from "../store.js";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import "leaflet.markercluster/dist/MarkerCluster.css";
import "leaflet.markercluster/dist/MarkerCluster.Default.css";
import "leaflet.markercluster/dist/leaflet.markercluster-src.js";
import "leaflet-ajax/dist/leaflet.ajax.js";
import icon from "leaflet/dist/images/marker-icon.png";
import iconShadow from "leaflet/dist/images/marker-shadow.png";
export default {
name: "ContactsMap",
mounted() {
this.fetchAggregatedDistrictStats();
},
methods: {
async fetchAggregatedDistrictStats() {
axios.get(... // fetches some statistics for the other overlay layers
);
},
createMapWithLeafletAndMapTiler() {
var $this = this;
var streetTilesLayer = L.tileLayer(MAPTILER_STREETS_URL, {
maxZoom: 18,
minZoom: 2,
attribution: MAPBOX_ATTRIBUTION,
tileSize: 512,
zoomOffset: -1
});
// eslint-disable-next-line
var satelliteTilesLayer = L.tileLayer(MAPTILER_SATELLITE_URL, {
maxZoom: 18,
minZoom: 2,
tileSize: 512,
zoomOffset: -1
});
var baseMaps = {
Satellite: satelliteTilesLayer,
Streets: streetTilesLayer
};
if (myMap != undefined) {
myMap.remove();
}
var myMap = L.map("leafletMapId", {
layers: [streetTilesLayer],
}).setView(this.center, this.zoom);
var markers = L.markerClusterGroup({
iconCreateFunction: function(cluster) {
var count = cluster.getChildCount();
var digits = (count + "").length;
return L.divIcon({
html: "<b>" + count + "</b>" + digits,
className: "cluster digits-" + digits,
iconSize: 22 + 10 * digits
});
}
});
async function fetchLocations(shouldProgrammaticallyFitBounds) {
markers.clearLayers();
markers = L.markerClusterGroup({
chunkedLoading: true,
iconCreateFunction: function(cluster) {
var count = cluster.getChildCount();
var digits = (count + "").length;
return L.divIcon({
html: "<b>" + count + "</b>",
className: "cluster digits-" + digits,
iconSize: 22 + 10 * digits
});
}
});
axios
.get("/maps/" + $this.list.list_id, {
//fetches markerclusters
})
.then(
response => {
$this.tooManyResults = false;
var addressPoints = response.data.recordsList;
for (var i = 0; i < addressPoints.length; i++) {
var a = addressPoints[i];
var title = a[2];
var marker = L.marker(L.latLng(a[0], a[1]));
marker.bindPopup(title);
markers.addLayer(marker); // This is where I'm adding the markers and markerclusters to the layer titled "markers"
// myMap.addLayer(markers); //If I uncomment this, then the markers layer is always on the map, i.e. not as an overlay layer
}
},
error => {
$this.isSearching = false;
document.getElementById("leafletMapId").style.display = "block";
store.setErrorMessage("Network error searching list", error);
}
);
}
myMap.on("zoomend", handlerFunction);
myMap.on("dragend", handlerFunction);
var defaultPolygonStyle = {
color: "black",
weight: 1,
opacity: 0.8,
fillOpacity: 0.1
};
var NationalCongressional = new L.geoJson.ajax(
"http://localhost:8080/assets/geo/NationalCongressional.json",
{
style: defaultPolygonStyle,
onEachFeature: function(feature, layer) {
layer.bindPopup(feature.properties.NAMELSAD);
if (feature.properties.NAMELSAD == "Congressional District 8") {
layer.setStyle({ color: "orange" });
}
}
}
);
function getColorFromRedBlueRange(d) {
return d > 0.8
? "#FF0000"
: d > 0.7
? "#FF006F"
: d > 0.55
? "#FF00EF"
: d > 0.45
? "#DE00FF"
: d > 0.3
? "#BC00FF"
: d > 0.2
? "#6600FF"
: "#00009FF";
}
var ILZipCodes = new L.geoJson.ajax(
"https://raw.githubusercontent.com/OpenDataDE/State-zip-code-GeoJSON/master/il_illinois_zip_codes_geo.min.json",
{
// style: defaultPolygonStyle,
onEachFeature: function(feature, layer) {
layer.bindPopup(
feature.properties.ZCTA5CE10 +
", " +
$this.zipData[feature.properties.ZCTA5CE10]
);
layer.setStyle({
color: getColorFromRedBlueRange(
$this.zipData[feature.properties.ZCTA5CE10]
),
weight: 0,
fillOpacity: 0.3
});
}
}
);
var overlays = {
Voters: voterGroup,
ILZipCodes: ILZipCodes,
};
L.control.layers(baseMaps, overlays).addTo(myMap);
fetchLocations(true);
}
}
};
You add voterGroup to your Layers Control, instead of your markers.
Then simply do not re-assign a MarkerClusterGroup into your markers variable (after you use clearLayers) and you should be fine.

Customize the tooltip of Chart.js

I'm using the last version of vue and vue-chart.js.
I'd like to customize the tooltip which is displayed when hovering a point.
Issue
The tooltip is not changed from the default one
Question
How to customize the tooltip. Ultimately I'd like to be able to click on a link in the tool tip to trigger a dialog that would display details taken from data contained in my vue component.
Vue.component('line-chart', {
extends: VueChartJs.Line,
mounted () {
this.renderChart({
labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'],
datasets: [
{
label: 'Data One',
backgroundColor: '#f87979',
data: [40, 39, 10, 40, 39, 80, 40]
}
]
}, {
tooltips: {
custom: function(tooltipModel) {
// Tooltip Element
var tooltipEl = document.getElementById('chartjs-tooltip');
// Create element on first render
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.id = 'chartjs-tooltip';
tooltipEl.innerHTML = "<table></table>"
document.body.appendChild(tooltipEl);
}
// Hide if no tooltip
if (tooltipModel.opacity === 0) {
tooltipEl.style.opacity = 0;
return;
}
// Set caret Position
tooltipEl.classList.remove('above', 'below', 'no-transform');
if (tooltipModel.yAlign) {
tooltipEl.classList.add(tooltipModel.yAlign);
} else {
tooltipEl.classList.add('no-transform');
}
function getBody(bodyItem) {
return bodyItem.lines;
}
// Set Text
if (tooltipModel.body) {
var titleLines = tooltipModel.title || [];
var bodyLines = tooltipModel.body.map(getBody);
var innerHtml = '<thead>';
titleLines.forEach(function(title) {
innerHtml += '<tr><th>' + title + '</th></tr>';
});
innerHtml += '</thead><tbody>';
bodyLines.forEach(function(body, i) {
var colors = tooltipModel.labelColors[i];
var style = 'background:' + colors.backgroundColor;
style += '; border-color:' + colors.borderColor;
style += '; border-width: 2px';
var span = '<span class="chartjs-tooltip-key" style="' + style + '"></span>';
innerHtml += '<tr><td>' + span + body + '</td></tr>';
});
innerHtml += '</tbody>';
var tableRoot = tooltipEl.querySelector('table');
tableRoot.innerHTML = innerHtml;
}
// `this` will be the overall tooltip
var position = this._chart.canvas.getBoundingClientRect();
// Display, position, and set styles for font
tooltipEl.style.opacity = 1;
tooltipEl.style.left = position.left + tooltipModel.caretX + 'px';
tooltipEl.style.top = position.top + tooltipModel.caretY + 'px';
tooltipEl.style.fontFamily = tooltipModel._fontFamily;
tooltipEl.style.fontSize = tooltipModel.fontSize;
tooltipEl.style.fontStyle = tooltipModel._fontStyle;
tooltipEl.style.padding = tooltipModel.yPadding + 'px ' + tooltipModel.xPadding + 'px';
}
},
responsive: true,
maintainAspectRatio: false
})
}
})
var vm = new Vue({
el: '.app',
data: {
message: 'Hello World'
}
})
https://codepen.io/anon/pen/ooyMMG
Remove this from html=
tooltips: {
Add this one line = enabled:false,
custom: function(tooltipModel)

c3.js total count in title of pie chart

I have a question about the pie chart in c3.js.
How can I add the total count of a pie chart in the title??
var title = new Array('data1.sql','data2.sql')
var dtitle = new Array('title1','title2')
var chart = new Array('chart0', 'chart1')
for(var i = 0; i < title.length; i++){
chart[i] = c3.generate({
bindto : '#chart' + i,
size: {
height: 550,
width: 800
},
data : {
url : '/json/sql/data/test/' + title[i],
mimeType : 'json',
type : 'donut'
},
donut: {
title: dtitle[i] + ' - Total:' ,
label: {
format: function(value, ratio, id) {
return value;
}
}
}
});
}
The annoying thing here is that the title option can take a function, but the chart variable is not initialised within it so using the c3 api methods can't be done at this point.
So the best (maybe only) way is to add an onrendered callback that adds up the data as you'd need to anyways and then replace the text in the chart's title text using a spot of d3:
onrendered: function () {
var data = this.api.data();
var total = data.reduce (function (subtotal, t) {
return subtotal + t.values.reduce (function (subsubtotal,b) { return subsubtotal + b.value; }, 0);
}, 0);
d3.select(this.config.bindto + " .c3-chart-arcs-title").text("Total: "+total);
}
Edit: If you want it to keep track of a total as you hide/show series use this
var data = this.api.data.shown.call (this.api);
instead of
var data = this.api.data();

KineticJs-how to update x and y position of the multiple images after resizing the stage layer

As I am new to KineticJs so, I have tried implementing the functionality using Kinectic js for drawing the multiple image on different- different x and y. Now I wanted to resize the stage layer or canvas. I have done that by using the code given below
window.onresize = function (event) {
stage.setWidth(($('#tab' + tabId).innerWidth() / 100) * 80);
var _images = layer.getChildren();
for (var i = 0; i < _images.length; i++) {
if (typeof _images[i].getId() != 'undefined') {
//alert(stage.getScale().x);
_images[i].setX(_images[i].getX() * stage.getScale().x);
layer.draw();
}
}
}
but now the problem is the are being defined and now if browser resize than stage is resized but the images on the prev x and y are fixed . I would like to keep them fixed on the position on resizing of stage layer or canvas.Here are the link of the image before resize and after resizing.beforeresize and afterResize .
Here is my entire code given below:-
$("#tabs li").each(function () {
$(this).live("click", function () {
clearInterval(_timer);
var tabname = $(this).find("a").attr('name');
tabname = $.trim(tabname.replace("#tab", ""));
var tabId = $(this).find("a").attr('href');
tabId = $.trim(tabId.replace("#", ""));
$.ajax({
url: "/Home/GetTabsDetail",
dataType: 'json',
type: 'GET',
data: { tabId: tabId },
cache: false,
success: function (data) {
var bayStatus = [];
var i = 0;
var image_array = [];
var BayExist = false;
var BayCondition;
var imgSrc;
var CanvasBacgroundImage;
var _X;
var _bayNumber;
var _Y;
var _ZoneName;
$(data).each(function (i, val) {
i = i + 1;
if (!BayExist) {
bayStatus = val.BayStatus;
CanvasBacgroundImage = val.TabImageLocation;
BayExist = true;
}
$.each(val, function (k, v) {
if (k == "BayNumber") {
BayCondition = bayStatus[v];
_bayNumber = v;
if (BayCondition == "O")
imgSrc = "../../images/Parking/OccupiedCar.gif"
else if (BayCondition == "N")
imgSrc = "../../images/Parking/OpenCar.gif";
}
if (k == "BayX")
_X = v;
if (k == "BayY")
_Y = v;
if (k == "ZoneName")
_ZoneName = v;
});
image_array.push({ img: imgSrc, xAxis: _X, yAxis: _Y, toolTip: _bayNumber, ZoneName: _ZoneName });
});
var imageUrl = CanvasBacgroundImage;
if ($('#tab' + tabId).length) {
// $('#tab' + tabId).css('background-image', 'url("../../images/Parking/' + imageUrl + '")');
var stage = new Kinetic.Stage({
container: 'tab' + tabId,
width: ($('#tab' + tabId).innerWidth() / 100) * 80, // 80% width of the window.
height: 308
});
window.onresize = function (event) {
stage.setWidth(($('#tab' + tabId).innerWidth() / 100) * 80);
}
$('#tab' + tabId).find('.kineticjs-content').css({ 'background-image': 'url("../../images/Parking/' + imageUrl + '")', 'background-repeat': ' no-repeat', 'background-size': '100% 100%' });
var layer = new Kinetic.Layer();
var planetOverlay;
function writeMessage(message, _x, _y) {
text.setX(_x + 20);
text.setY(_y + 1);
text.setText(message);
layer.draw();
}
var text = new Kinetic.Text({
fontFamily: 'Arial',
fontSize: 14,
text: '',
fill: '#000',
width: 200,
height: 60,
align: 'center'
});
var opentooltip = new Opentip(
"div#tab" + tabId, //target element
"dummy", // will be replaced
"", // title
{
showOn: null // I'll manually manage the showOn effect
});
Opentip.styles.win = {
borderColor: "black",
shadow: false,
background: "#EAEAEA"
};
Opentip.defaultStyle = "win";
// _timer = setInterval(function () {
for (i = 0; i < image_array.length; i++) {
img = new Image();
img.src = image_array[i].img;
planetOverlay = new Kinetic.Image({
x: image_array[i].xAxis,
y: image_array[i].yAxis,
image: img,
height: 18,
width: 18,
id: image_array[i].toolTip,
name: image_array[i].ZoneName
});
planetOverlay.on('mouseover', function () {
opentooltip.setContent("<span style='color:#87898C;'><b>Bay:</b></span> <span style='color:#25A0D3;'>" + this.getId() + "</span><br> <span style='color:#87898C;'><b>Zone:</b></span><span style='color:#25A0D3;'>" + this.getName() + "</span>");
//writeMessage("Bay: " + this.getId() + " , Zone: " + this.getName(), this.getX(), this.getY());//other way of showing tooltip
opentooltip.show();
$("#opentip-1").offset({ left: this.getX(), top: this.getY() });
});
planetOverlay.on('mouseout', function () {
opentooltip.hide();
// writeMessage('');
});
planetOverlay.createImageHitRegion(function () {
layer.draw();
});
layer.add(planetOverlay);
layer.add(text);
stage.add(layer);
}
// clearInterval(_timer);
//$("#tab3 .kineticjs-content").find("canvas").css('background-image', 'url("' + imageUrl + '")');
// },
// 500)
}
}
,
error: function (result) {
alert('error');
}
});
});
});
I want to keep the icons on the position where they were before resizing. I have tried but could not get the right solution to get this done.
How can How can I update x,y position for the images . Any suggestions would be appreciated.
Thanks is advance.
In window.resize, you're changing the stage width by a scaling factor.
Save that scaling factor.
Then multiply the 'x' coordinate of your images by that scaling factor.
You can reset the 'x' position of your image like this:
yourImage.setX( yourImage.getX() * scalingFactor );
layer.draw();
In the above mentioned code for window.onresize. The code has been modified which as follow:-
window.onresize = function (event) {
_orignalWidth = stage.getWidth();
var _orignalHeight = stage.getHeight();
// alert(_orignalWidth);
// alert($('#tab' + tabId).outerHeight());
stage.setWidth(($('#tab' + tabId).innerWidth() / 100) * 80);
//stage.setHeight(($('#tab' + tabId).outerHeight() / 100) * 80);
_resizedWidth = stage.getWidth();
_resizedHeight = stage.getHeight();
// alert(_resizedWidth);
_scaleFactorX = _resizedWidth / _orignalWidth;
var _scaleFactorY = _resizedHeight / _orignalHeight;
//alert(_scaleFactor);
var _images = layer.getChildren();
for (var i = 0; i < _images.length; i++) {
if (typeof _images[i].getId() != 'undefined') {
//alert(stage.getScale().x);
_images[i].setX(_images[i].getX() * _scaleFactorX);
//_images[i].setY(_images[i].getY() * _scaleFactorY);
layer.draw();
}
}
}

How to add a function to another jQuery function?

I'm trying to add this short function - which swaps images according to the active tab in jQuery UI Tabs - below to the larger function below, which is the jQuery Address plugin that adds forward/back and #URL functions to UI Tabs: http://www.asual.com/jquery/address/
I need to add the shorter function so it fires when the tab changes - so it changes the image in #headerwrapper - but I can't quite tell exactly where the tab change is fired in the main address function. Any ideas on how to add this shorter function to jQuery Address?
Image change function I need to add to the main function below to run when the tab change fires:
var img = $(ui.panel).data("image");
$("#headerwrapper")
.animate({ opacity: 'toggle' }, function() {
$(this).css("background-image", "url(" + img + ")")
.animate({ opacity: 'toggle' });
});
}
Main jQuery Tabs Address function:
<script type="text/javascript">
var tabs,
tabulation = false,
initialTab = 'home',
navSelector = '#tabs .ui-tabs-nav',
navFilter = function(el) {
return $(el).attr('href').replace(/^#/, '');
},
panelSelector = '#tabs .ui-tabs-panel',
panelFilter = function() {
$(panelSelector + ' a').filter(function() {
return $(navSelector + ' a[title=' + $(this).attr('title') + ']').size() != 0;
}).each(function(event) {
$(this).attr('href', '#' + $(this).attr('title').replace(/ /g, '_'));
});
};
if ($.address.value() == '') {
$.address.value(initialTab);
}
$.address.init(function(event) {
$(panelSelector).attr('id', initialTab);
$(panelSelector + ' a').address(function() {
return navFilter(this);
});
panelFilter();
tabs = $('#tabs')
.tabs({
load: function(event, ui) {
$(ui.panel).html($(panelSelector, ui.panel).html());
panelFilter();
},
fx: {
opacity: 'toggle',
duration: 'fast'}
})
.css('display', 'block');
$(navSelector + ' a').click(function(event) {
tabulation = true;
$.address.value(navFilter(event.target));
tabulation = false;
return false;
});
}).change(function(event) {
var current = $('a[href=#' + event.value + ']:first');
$.address.title($.address.title().split(' - ')[0] + ' - ' + current.text());
if (!tabulation) {
tabs.tabs('select', current.attr('href'));
}
}).history(true);
document.write('<style type="text/css"> #tabs { display: none; } </style>');
</script>