I am building a project using React with a doughnut and bar chart. Working with Chart.js 3.xx.
I am trying to make my custom legend functional. I want to make data fractions disappear when the user clicks my legend items - like in the native legend, and optimally also remove the data and make the chart present it's updated data after removal.
I also use data labels to present percentage of the data on the fractions.
import ChartDataLabels from 'chartjs-plugin-datalabels';
I came across this topic: ChartJS - Show/hide data individually instead of entire dataset on bar/line charts
and used this suggested code there:
function chartOnClick(evt) {
let chart = evt.chart
const points = chart.getElementsAtEventForMode(evt, 'nearest', {}, true);
if (points.length) {
const firstPoint = points[0];
//var label = myChart.data.labels[firstPoint.index];
//var value = myChart.data.datasets[firstPoint.datasetIndex].data[firstPoint.index];
let datasetIndex = firstPoint.datasetIndex, index = firstPoint.index;
if (firstPoint.element.hidden != true) {
chart.hide(datasetIndex, index);
} else {
chart.show(datasetIndex, index);
}
}
}
options: { // chart options
onClick: chartOnClick
}
It almost works, but the hide() method doesn't remove the fraction's percentage data label when activated, whereas when clicking the native legend it does remove it entirely.
I tried looking in the plugin's docs but didn't manage to find how to remove a single label.
How can I achieve what I am looking for?
EDIT:
Options Object:
export const doughnutOptsObj = {
onClick: chartOnClick,
responsive: true,
maintainAspectRatio: false,
layout: { padding: { top: 16, bottom: 16 } },
hoverOffset: 32,
plugins: {
legend: {
display: true,
position: 'bottom',
},
datalabels: {
formatter: (value, dnct1) => {
let sum = 0;
let dataArr = dnct1.chart.data.datasets[0].data;
dataArr.map((data) => {
sum += Number(data);
});
let percentage = ((value * 100) / sum).toFixed() + '%';
return percentage;
},
color: ['#fbfcfd'],
font: { weight: 'bold' },
// display: false, <-- this works and makes all of the data labels disappear
},
},
};
It seems that the onClick function is working properly.
I have tried the attached code, leveraging on toggleDataVisibility API, and it's working as requested (codepen: https://codepen.io/stockinail/pen/abKNJqJ):
function chartOnClick(evt) {
let chart = evt.chart
const points = chart.getElementsAtEventForMode(evt, 'nearest', {}, true);
if (points.length) {
const firstPoint = points[0];
chart.toggleDataVisibility(firstPoint.index);
chart.update();
}
}
Related
Hi I am making an app which uses google maps to select some location.
I am using Drawing Manager of google map to draw a polygon.
Now I want to get the coordinates of the polygon.
Is there any way to do it?
Here is the code:
<DrawingManager
defaultDrawingMode={window.google.maps.drawing.OverlayType.POLYGON}
defaultOptions={{
drawingControl: true,
drawingControlOptions: {
position: window.google.maps.ControlPosition.TOP_CENTER,
drawingModes: [
window.google.maps.drawing.OverlayType.MARKER,
window.google.maps.drawing.OverlayType.POLYGON,
],
},
polygonOptions: { editable: true },
}}
/>
You can add a listener to the drawing manager for the "polygoncomplete" event then you can use the getPath() method to get the coordinates. If you are using a reactjs library for google maps, you can check how that library is using the polygoncomplete event.
Here is a snippet where I put all the points from the polygon.getpath().getArray() to the array and log it in the console in a simple code:
const drawingManager = new google.maps.drawing.DrawingManager({
drawingMode: google.maps.drawing.OverlayType.MARKER,
drawingControl: true,
drawingControlOptions: {
position: window.google.maps.ControlPosition.TOP_CENTER,
drawingModes: [
window.google.maps.drawing.OverlayType.MARKER,
window.google.maps.drawing.OverlayType.POLYGON
]
},
polygonOptions: { editable: true }
});
drawingManager.setMap(map);
google.maps.event.addListener(
drawingManager,
"polygoncomplete",
function(polygon) {
let polygonCoordsArray = [];
let coords = polygon.getPath().getArray();
for (let i = 0; i < coords.length; i++) {
// console.log(coords[i].lat() + "," + coords[i].lng());
polygonCoordsArray.push(
coords[i].lat() + "," + coords[i].lng()
);
}
console.log(polygonCoordsArray);
}
);
const [LocationList, setLocationList] = useState([]); //define state
//method to store location in state
const onPolygonComplete = (polygon) => {
setLocationList(
polygon
.getPath()
.getArray()
.map((point) => ({
lat: point.lat(),
lng: point.lng(),
}))
)}
// define component
<DrawingManager
onPolygonComplete={onPolygonComplete}//link method
/>
TL;DR: In #react-google-maps/api, I want to be able to make dynamic cluster icons/symbol in the style of pie charts, based on the markers in the cluster, but it seems I can only make icons from a static array, and cannot pass the the markers as parameters.
Full Description:
I am using typescript react with the package #react-google-maps/api, and I'm trying to find a way with the ClustererComponent/MarkerClusterer to take a callback or similar in order to be able to be able to create an svg for each cluster based on the markers in the given cluster.
The current issue is that the way I understand it, I am limited to a static array of urls to icons, and thought I can make an svg in those, I have no way to pass parameters into those svgs, as the only way the package allows me to chose a style is thought index in the style array.
I have read thought the following material, but have not been able to get find a way to make an icon dynamically based on the markers:
Documentation for #react-google-maps/api: https://react-google-maps-api-docs.netlify.app/#markerclustere
Documentation for google maps markerclusterer: https://developers.google.com/maps/documentation/javascript/marker-clustering
I have found libraries like this: https://github.com/hassanlatif/google-map-chart-marker-clusterer, that should be able to be used as a solution, but they don't seem to work with the #react-google-maps/api, only with earlier versions of google map. If this is not the case, and these can be used directly, then I would be more then happy with an answer describing how to use libraries like the one above with #react-google-maps/api, as that should allow be to make clusters in the same way as the picture below.
EDIT: as I got reminded in the comments, here is the code I have so far:
What I've tried: I have tried to find any way to set in an svg element instead of a url, but have since just decided to make a url with the svg data, as shown below. I have tried to edit the url of the clusters under the MarkerClusterer thought the callback for onClusteringBegin, onClusteringEnd and onLoad, but so far, no luck.
How I make the svg into url-data, so it can be used for img src
/*
* Pie Chart SVG Icon in URL form
*
* Inspiration taken from: https://medium.com/hackernoon/a-simple-pie-chart-in-svg-dbdd653b6936
*
* Note: As of right now, I am identifying the difference in marker types by setting the type-number I use in the title of the marker
*/
const serializeXmlNode = (xmlNode: any) => {
if (typeof window.XMLSerializer != "undefined") {
return (new window.XMLSerializer()).serializeToString(xmlNode);
} else if (typeof xmlNode.xml != "undefined") {
return xmlNode.xml;
}
return "";
}
function getCoordinatesForPercent(percent: number) {
const x = Math.cos(2 * Math.PI * percent);
const y = Math.sin(2 * Math.PI * percent);
return [x, y];
}
const makePieChartIcon = (slices: any[]) => {
const svgNS = 'http://www.w3.org/2000/svg';
var svg = document.createElementNS(svgNS, 'svg')
svg.setAttribute('viewBox', '-1.1 -1.1 2.2 2.2')
svg.setAttribute('style', 'transform: rotate(-90deg)')
svg.setAttribute('height', '60')
var circle = document.createElementNS(svgNS, 'circle')
circle.setAttribute('r', '1.1')
circle.setAttribute('fill', 'white')
svg.appendChild(circle);
let cumulativePercent = 0;
slices.map((slice: any) => {
const [startX, startY] = getCoordinatesForPercent(cumulativePercent);
cumulativePercent += slice.percent;
const [endX, endY] = getCoordinatesForPercent(cumulativePercent);
const largeArcFlag = slice.percent > .5 ? 1 : 0;
const pathData = [
`M ${startX} ${startY}`, // Move
`A 1 1 0 ${largeArcFlag} 1 ${endX} ${endY}`, // Arc
`L 0 0`, // Line
].join(' ');
const path = document.createElementNS(svgNS, 'path');
path.setAttribute('d', pathData);
path.setAttribute('fill', slice.color);
svg.appendChild(path);
})
var svgUrl = 'data:image/svg+xml;charset=UTF-8,' + serializeXmlNode(svg)
return svgUrl
}
const makeDynamicClusterIcon = (markers: any[]) => {
var numMarkers = markers.length;
var slices = markers.reduce((acc: any, marker: any) => {
acc[parseInt(marker.title)].percent += 1 / numMarkers;
return acc;
}, [
{ percent: 0, color: 'Green' },
{ percent: 0, color: 'Blue' },
{ percent: 0, color: 'Red' },
])
var newIconURL = makePieChartIcon(slices)
return newIconURL;
}
How I use the MarkerClusterer Component
<MarkerClusterer
options={{
averageCenter: true,
styles: clusterStyles,
}}
>
{(clusterer) =>
markerData.map((marker: any) => (
<Marker
key={marker.key}
title={String(marker.type)}
position={{ lat: marker.lat, lng: marker.lng }}
clusterer={clusterer}
/>
))
}
</MarkerClusterer>
Right now, I can only use some static styles, but I have them as the following for testing:
const clusterStyles = [
{
height: 50, textColor: '#ffffff', width: 50,
url: 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns="http://www.w3.org/2000/svg" height="50" width="100"%3E%3Ccircle cx="25" cy="25" r="20" stroke="black" stroke-width="3" fill="green" /%3E%3C/svg%3E',
},
{
height: 50, textColor: '#ffffff', width: 50,
url: 'data:image/svg+xml;charset=UTF-8,%3Csvg xmlns="http://www.w3.org/2000/svg" height="50" width="100"%3E%3Ccircle cx="25" cy="25" r="20" stroke="black" stroke-width="3" fill="red" /%3E%3C/svg%3E',
}
];
I found a solution, by finding out that the style array for each cluster (ClusterStyles) can be changed, and then I have change it with the data from the specific markers in the given cluster. I ended up doing this in the callback onClusteringEnd, as here:
{/* Added to the MarkerClusterer */}
onClusteringEnd={(clusterer) => {
clusterer.clusters.map((cluster) => {
cluster.clusterIcon.styles = makeDynamicClusterIcon(cluster.markers)
})
}}
And I changed the last line with return of the makeDynamicClusterIcon function I showed above to instead say:
return [{ url: newIconURL, height: 60, width: 60, textColor: '#FFFFFF', textSize: 22 }];
I am referring this example to export a worksheet https://github.com/SheetJS/js-xlsx/issues/817. How to do cell
styling like background coloring,font size and increasing the width of the
cells to make the data fit exactly.I have gone through the documentation but couldn't find any proper examples to use fill etc.Is there a way to do the formatting?
Below is the code snippet:
/* make the worksheet */
var ws = XLSX.utils.json_to_sheet(data);
/* add to workbook */
var wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, "People");
/* write workbook (use type 'binary') */
var wbout = XLSX.write(wb, {bookType:'xlsx', type:'binary'});
/* generate a download */
function s2ab(s) {
var buf = new ArrayBuffer(s.length);
var view = new Uint8Array(buf);
for (var i=0; i!=s.length; ++i) view[i] = s.charCodeAt(i) & 0xFF;
return buf;
}
saveAs(new Blob([s2ab(wbout)],{type:"application/octet-
stream"}),"sheetjs.xlsx");
Styling is only available in Pro Version of SheetJS. But I think you are using community version(free version). In Community version styling is not available.
You can check here official information:
We also offer a pro version with performance enhancements, additional
features like styling, and dedicated support.
There are a bunch of community forks that allow styling. My personal favorite is xlsx-js-style. It is up to date and works well compared to other libraries.
sheetjs-style is also up to date, but i had some problems with it. See: Styles not working
xlsx-style is not up to date. Currently 397 commits behind SheetJS:master. I would not use it if possible.
All of these libraries share the same styling options. Here is a bunch of examples:
for (i in ws) {
if (typeof(ws[i]) != "object") continue;
let cell = XLSX.utils.decode_cell(i);
ws[i].s = { // styling for all cells
font: {
name: "arial"
},
alignment: {
vertical: "center",
horizontal: "center",
wrapText: '1', // any truthy value here
},
border: {
right: {
style: "thin",
color: "000000"
},
left: {
style: "thin",
color: "000000"
},
}
};
if (cell.c == 0) { // first column
ws[i].s.numFmt = "DD/MM/YYYY HH:MM"; // for dates
ws[i].z = "DD/MM/YYYY HH:MM";
} else {
ws[i].s.numFmt = "00.00"; // other numbers
}
if (cell.r == 0 ) { // first row
ws[i].s.border.bottom = { // bottom border
style: "thin",
color: "000000"
};
}
if (cell.r % 2) { // every other row
ws[i].s.fill = { // background color
patternType: "solid",
fgColor: { rgb: "b2b2b2" },
bgColor: { rgb: "b2b2b2" }
};
}
}
I used sheetjs-style (which is a fork of sheetjs) to add formatting to cells in excel file.
ws["A1"].s = // set the style for target cell
font: {
name: '宋体',
sz: 24,
bold: true,
color: { rgb: "FFAA00" }
},
};
It's very easy. However, you have to add style to each individual cell. It's not convenient to add style to a range of cells.
UPDATE: The official example use color "FFFFAA00". But I removed the first "FF" and it still works as before. The removed part is used for transparency (see COLOR_SPEC in Cell Styles), but somehow it has no effect when I change it or remove it.
After testing all the above options. For ReactJS I finally found a package that worked perfectly.
https://github.com/ShanaMaid/sheetjs-style
import XLSX from 'sheetjs-style';
var workbook = XLSX.utils.book_new();
var ws = XLSX.utils.aoa_to_sheet([
["A1", "B1", "C1"],
["A2", "B2", "C2"],
["A3", "B3", "C3"]
])
ws['A1'].s = {
font: {
name: 'arial',
sz: 24,
bold: true,
color: "#F2F2F2"
},
}
XLSX.utils.book_append_sheet(workbook, ws, "SheetName");
XLSX.writeFile(workbook, 'FileName.xlsx');
Note following points while adding styling:-
Cell should not be empty
First add data into the cell, then add styling to that cell.
For 2 days I was struck and did not got any styling appearing on my excel file since I was just adding styling before adding the data.Don't do that it won't appear.
I used xlsx-js-style Package and added the styles to my excel in the following way :-
XLSX.utils.sheet_add_aoa(worksheet, [["Firstname"]], { origin: "A1"
});
const LightBlue = {
fgColor: { rgb: "BDD7EE" }
};
const alignmentCenter = { horizontal: "center", vertical: "center", wrapText: true };
const ThinBorder = {
top: { style: "thin" },
bottom: { style: "thin" },
left: { style: "thin" },
right: { style: "thin" }
};
const fillAlignmentBorder = {
fill: LightBlue,
alignment: alignmentCenter,
border: ThinBorder
};
worksheet["A1"].s = fillAlignmentBorder;
Hope this helps.....Happy Coding :-)
I'm using the grid from http://ui-grid.info/ in a project. I've created a hierarchical grid, which works nicely, but when I do an export, it only exports the data from the top-level grid.
This is by design and is standard functionality for the grid, so there's no point in me putting up any example code. Any example from http://ui-grid.info/docs/#/tutorial will do.
Is there a way to export the subgrid (preferably both the main grid AND the subgrid together as they appear in the grid)?
Sadly the answer is no.
As you can see here the function getData iterates through all rows and then through all columns, adding to an array of extractedFields the columns to be extracted and aggregating those in an array of extractedRows.
This means that no data, other than what's defined in gridOptions' columnDef will be read, converted and extracted.
By design, subgrid information are stored inside a property of any row entity's subGridOptions but this property is never accessed inside of the exporter feature.
The motivation behind this behaviour is that expandable grid feature is still at an alpha stage, so, supporting this in other features is not a compelling priority.
Furthermore, adding subgrid to a CSV could be quite hard to design, if we wanted to provide a general solution (for example I don't even think it would be compliant to CSV standard if you had different number of columns in the main grid and in subgrids).
That said, ui-grid is an open source project, so, if you have a working design in mind, feel free to open a discussion about it on the project gitHub page or, even better, if you can design a working (and tested) solution and create a pull request, even better!
I managed to get it working, although if I had the time I would do it a bit better than what I've done by actually creating a branch of the code and doing it properly, but given time constraints, what I've got is working nicely.
FYI, here's the way I ended up getting it to do what I wanted:
In my grid options, I turned off the CSV export options in the grid menu (because I've only implemented the changes for PDF).
I made a copy of exporter.js, named it custom.exporter.js and changed my reference to point to the new file.
In custom.exporter.js, I made a copy of the getData function and named it getGridRows. getGridRows is the same as getData, except it just returns the rows object without all the stuff that gets the columns and so on. For now, I'm coding it to work with a known set of columns, so I don't need all that.
I modified the pdfExport function to be as follows:
pdfExport: function (grid, rowTypes, colTypes) {
var self = this;
var exportData = self.getGridRows(grid, rowTypes, colTypes);
var docContent = [];
$(exportData).each(function () {
docContent.push(
{
table: {
headerRows: 1,
widths: [70, 80, 150, 180],
body: [
[{ text: 'Job Raised', bold: true, fillColor: 'lightgray' }, { text: 'Job Number', bold: true, fillColor: 'lightgray' }, { text: 'Client', bold: true, fillColor: 'lightgray' }, { text: 'Job Title', bold: true, fillColor: 'lightgray' }],
[formattedDateTime(this.entity.JobDate,false), this.entity.JobNumber, this.entity.Client, this.entity.JobTitle],
]
}
});
var subGridContentBody = [];
subGridContentBody.push([{ text: 'Defect', bold: true, fillColor: 'lightgray' }, { text: 'Vendor', bold: true, fillColor: 'lightgray' }, { text: 'Status', bold: true, fillColor: 'lightgray' }, { text: 'Sign off', bold: true, fillColor: 'lightgray' }]);
$(this.entity.Defects).each(function () {
subGridContentBody.push([this.DefectName, this.DefectVendor, this.DefectStatus, '']);
});
docContent.push({
table: {
headerRows: 1,
widths: [159, 150, 50, 121],
body: subGridContentBody
}
});
docContent.push({ text: '', margin: 15 });
});
var docDefinition = {
content: docContent
}
if (self.isIE()) {
self.downloadPDF(grid.options.exporterPdfFilename, docDefinition);
} else {
pdfMake.createPdf(docDefinition).open();
}
}
No, there is no direct way to export subgrid. rather you can create youur own json data to generate csv file.
Please check the below code
function jsonToCsvConvertor(JSONData, reportTitle) {
//If JSONData is not an object then JSON.parse will parse the JSON string in an Object
var arrData = typeof JSONData !== 'object' ? JSON.parse(JSONData) : JSONData,
csv = '',
row,
key1,
i,
subGridData;
//Set Report title in first row or line
csv += reportTitle + '\r\n\n';
row = '';
for (key1 in arrData[0]) {
if(key1 !== 'subGridOptions' && key1 !== '$$hashKey'){
row += key1 + ',';
}
}
csv += row + '\r\n';
for (i = 0; i < arrData.length; i++) {
row = '';
subGridData = '';
for (key1 in arrData[i]) {
if(key1 !== 'subGridOptions' && key1 !== '$$hashKey'){
row += '"' + arrData[i][key1] + '",';
}
else if(key1 === 'subGridOptions'){
//csv += row + '\r\n';
subGridData = writeSubGridData(arrData[i][key1].data);
}
}
csv += row + '\r\n';
csv = subGridData ? csv + subGridData + '\r\n' : csv;
}
if (csv === '') {
console.log('Invalid data');
}
return csv;
}
//Generates subgrid Data to exportable form
function writeSubGridData(subgridData){
var j,
key2,
csv = '',
row = '';
for (key2 in subgridData[0]){
if(key2 !== '$$hashKey'){
row += key2 + ',';
}
}
csv = row + '\r\n';
for (j=0; j < subgridData.length ; j++){
row = '';
for(key2 in subgridData[j]){
if(key2 !== '$$hashKey'){
row += '"' + subgridData[j][key2]+ '",';
}
}
csv += row + '\r\n';
}
return csv;
}
jsonToCsvConvertor(exportData, 'New-Report');
I have a question related the chart export.
Please see Jsfiddle here
I added a text label using chart.renderer.text on the Yaxis for the latest value of series.
If I directly click button "Export Image". There is no problem, the label can be displayed. I'm using the following way to export image. draw_labels() is a function to draw yaxis label.
$("#b").click(function () {
chart.exportChart(null, {
chart: {
backgroundColor: '#FFFFFF',
width: 972,
height: 480,
events: {
load: function () {
draw_labels(this);
}
}
}
});
});
The problem is after I clicked range selector or change Xaxis range. When I try to export the
chart to image, there is no labels are drawn. The following is the complete code.
The following is the complete code:
$(function () {
var chart;
$.getJSON('http://www.highcharts.com/samples/data/jsonp.php?filename=aapl-c.json&callback=?', function (data) {
chart = new Highcharts.StockChart({
chart: {
renderTo: 'container',
events: {
load: function () {
draw_labels(this);
$("#b").click(function () {
chart.exportChart(null, {
chart: {
backgroundColor: '#FFFFFF',
width: 972,
height: 480,
events: {
load: function () {
draw_labels(this);
}
}
}
});
});
}
}
},
series: [{
name: 'AAPL',
id: 'test',
data: data,
tooltip: {
valueDecimals: 2
}
}],
navigator: {
enabled: false
},
yAxis: {
tickWidth: 0,
id: 'value_axis',
type: 'linear',
gridLineColor: '#EEE',
lineColor: '#D0CDC9',
lineWidth: 0,
minorTickInterval: null,
opposite: true,
offset: 0
},
xAxis: {
events: {
afterSetExtremes: function (e) {
console.log('test');
$('[id="test_text"]').remove();
draw_labels(chart);
}
}
}
});
});
function draw_labels(chart) {
$(chart.series).each(function (i, serie) {
var s_id = serie.options.id;
var temp_id = s_id;
var point = serie.points[serie.points.length - 1];
if (point) {
var pre, post;
if (point.y) {
var last_value_dis = (point.y).toFixed(1);
yaxis_name = 'value_axis';
//Get Yaxis position
var y_axis = chart.get(yaxis_name);
offsite_yaxis = 0;
element_text = chart.renderer.text(
//the text to render
'<span style="font-size:10px;font-weight:bold;color:' + serie.color + ';">' + last_value_dis + '</span>',
//the 'x' position
y_axis.width + y_axis.offset,
//the 'y' position
chart.plotTop + point.plotY + 3).attr({
id: temp_id + '_text',
zIndex: 999
}).add();
}
}
});
}
});
Here, I have fixed it for you. Here is a saved image:
Following changes have been done:
Added a redraw event to your exportchart
redraw: function () {
$("#test_text").remove() ;
draw_labels(this);
}
Changed this line in afterSetExtremes
$('[id="test_text"]').remove();
to
$("#test_text").remove() ;
Earlier one was not working as expected, so I had to change it.
Problem with disappearing text is related with id, when I removed it, label appears. But then I came across second issue, wrong y position. So i declare global variable, then when you call your function, set position of label, and use in chart exporting this variable. As a result label is exported correct.
http://jsfiddle.net/UGbpJ/11/