diff --git a/webapp/views/App/views/Data/Charts/Charts.js b/webapp/views/App/views/Data/Charts/Charts.js index 12f58c2041..8b476d9975 100644 --- a/webapp/views/App/views/Data/Charts/Charts.js +++ b/webapp/views/App/views/Data/Charts/Charts.js @@ -1,5 +1,5 @@ import './Charts.scss' -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef } from 'react' import { Query } from '@common/model/query' import { Button } from '@webapp/components/buttons' import Chart from './components/Chart' @@ -17,6 +17,7 @@ const Charts = () => { const [fullScreen, setFullScreen] = useState(false) const { nodeDefLabelType, toggleLabelFunction } = useNodeDefLabelSwitch() const { dimensions, entityDefUuid, setEntityDefUuid } = useGetDimensionsFromArena(nodeDefLabelType) + const chartRef = useRef(null) const { config, configItemsByPath, configActions, spec, updateSpec, draft, chartData, renderChart } = useChart( entityDefUuid ? Query.create({ entityDefUuid }) : null, @@ -61,7 +62,14 @@ const Charts = () => { dimensions={dimensions} /> - + ) diff --git a/webapp/views/App/views/Data/Charts/components/Chart/Chart.js b/webapp/views/App/views/Data/Charts/components/Chart/Chart.js index 03c62eecfe..0738d7704f 100644 --- a/webapp/views/App/views/Data/Charts/components/Chart/Chart.js +++ b/webapp/views/App/views/Data/Charts/components/Chart/Chart.js @@ -7,7 +7,7 @@ import BarChart from './components/ChartTypes/BarChart/' import PieChart from './components/ChartTypes/PieChart/' import './Chart.scss' -const Chart = ({ data, specs, fullScreen }) => { +const Chart = ({ data, specs, fullScreen, chartRef }) => { const chartType = specs?.chartType const hasData = Boolean(data) const hasSvg = Boolean(data?.svg) @@ -26,23 +26,66 @@ const Chart = ({ data, specs, fullScreen }) => { const ChartComponent = chartComponentByType[chartType] + const downloadPng = () => { + const chart = chartRef?.current + if (chart) { + const svgElement = chart.querySelector('svg') + if (svgElement) { + // Serialize SVG + const serializer = new XMLSerializer() + const svgString = serializer.serializeToString(svgElement) + const svgBlob = new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' }) + const url = URL.createObjectURL(svgBlob) + + // Create an Image element + const img = new Image() + img.src = url + img.onload = () => { + // Create a canvas element + const canvas = document.createElement('canvas') + canvas.width = svgElement.clientWidth + canvas.height = svgElement.clientHeight + const ctx = canvas.getContext('2d') + + // Draw the image onto the canvas + ctx.drawImage(img, 0, 0) + + // Create a download link for the canvas image + const pngUrl = canvas.toDataURL('image/png') + const downloadLink = document.createElement('a') + downloadLink.href = pngUrl + downloadLink.download = 'chart.png' + document.body.appendChild(downloadLink) + downloadLink.click() + document.body.removeChild(downloadLink) + URL.revokeObjectURL(url) + } + } else { + console.error('No SVG element found inside the chart container.') + } + } else { + console.error('Chart container ref is not available.') + } + } + if (!hasData) { return null } return (
+ {(chartType || hasSvg) && (
{ChartComponent ? ( - + ) : ( hasSvg && ( @@ -60,6 +103,7 @@ Chart.propTypes = { data: PropTypes.object, specs: PropTypes.object, fullScreen: PropTypes.bool, + chartRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) })]), } export default Chart diff --git a/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/BarChart/BarChart.js b/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/BarChart/BarChart.js index d34c5d0114..82adb0547a 100644 --- a/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/BarChart/BarChart.js +++ b/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/BarChart/BarChart.js @@ -1,45 +1,35 @@ -import React, { useEffect, useRef } from 'react' -import PropTypes from 'prop-types' -import { renderBarChart } from './utils/render' import './BarChart.css' -const BarChart = ({ specs, originalData }) => { - const chartRef = useRef() +import React, { useEffect } from 'react' +import PropTypes from 'prop-types' + +import { Objects } from '@openforis/arena-core' +import { renderBarChart } from './utils/render' + +const BarChart = ({ specs, originalData, chartRef }) => { useEffect(() => { - if ( - !specs?.query?.metric?.field || - specs?.query?.metric?.field === '' || - !specs?.query?.groupBy?.field || - specs?.query?.groupBy?.field === '' || - !specs?.query?.aggregation?.type || - specs?.query?.aggregation?.type === '' - ) - return - const groupByField = specs.query.groupBy.field + const { query = {} } = specs ?? {} + const metricField = query.metric?.field + const groupByField = query.groupBy?.field + const aggregationType = query.aggregation?.type + + if (Objects.isEmpty(metricField) || Objects.isEmpty(groupByField) || Objects.isEmpty(aggregationType)) return if (groupByField) { let data if (originalData?.chartResult) { data = originalData.chartResult.map((item) => ({ groupBy: item[groupByField], - [`${specs.query.metric.field}_${specs.query.aggregation.type || 'sum'}`]: parseFloat( - item[`${specs.query.metric.field}_${specs.query.aggregation.type || 'sum'}`] - ), + [`${metricField}_${aggregationType}`]: parseFloat(item[`${metricField}_${aggregationType}`]), })) } else { data = [] } - renderBarChart( - data, - specs, - [`${specs.query.metric.field}_${specs.query.aggregation.type || 'sum'}`], - groupByField, - chartRef - ) + renderBarChart(data, specs, [`${metricField}_${aggregationType}`], groupByField, chartRef) } - }, [specs, originalData]) + }, [specs, originalData, chartRef]) return
} @@ -65,6 +55,7 @@ BarChart.propTypes = { }) ), }), + chartRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) })]), } export default React.memo(BarChart) diff --git a/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/BarChart/utils/renderHelpers.js b/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/BarChart/utils/renderHelpers.js index d2a5f6571f..ed99d265c0 100644 --- a/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/BarChart/utils/renderHelpers.js +++ b/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/BarChart/utils/renderHelpers.js @@ -139,11 +139,11 @@ export const renderStackedBars = (svg, data, metricAggregationNames, scales, col const pos = isHorizontal ? [event.pageY, event.pageX] : [event.pageX, event.pageY] tooltip - .html(`Group: ${d.data.groupBy}
Metric: ${metricName}
Value: ${metricValue}`) + .html(`Group: ${d.data.groupBy}
Metric: ${metricName}
Value: ${metricValue.toFixed(2)}`) .style('left', pos[0] + 10 + 'px') .style('top', pos[1] - 10 + 'px') tooltip - .html(`Group: ${d.data.groupBy}
Metric: ${metricName}
Value: ${metricValue}`) + .html(`Group: ${d.data.groupBy}
Metric: ${metricName}
Value: ${metricValue.toFixed(2)}`) .style('left', event.pageX + 10 + 'px') .style('top', event.pageY - 10 + 'px') }) @@ -167,7 +167,7 @@ export const renderSingleMetricBars = (svg, data, metricAggregationNames, chartP .on('mouseover', function (event, d) { tooltip.transition().duration(200).style('opacity', 0.9) tooltip - .html(`Group: ${d.groupBy}
Value: ${d[metricAggregationNames[0]]}`) + .html(`Group: ${d.groupBy}
Value: ${d[metricAggregationNames[0]].toFixed(2)}`) .style('left', event.pageX + 10 + 'px') .style('top', event.pageY - 10 + 'px') }) diff --git a/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/PieChart/PieChart.js b/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/PieChart/PieChart.js index 92a9478762..b09acc079a 100644 --- a/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/PieChart/PieChart.js +++ b/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/PieChart/PieChart.js @@ -1,14 +1,12 @@ import * as d3 from 'd3' import PropTypes from 'prop-types' -import React, { useEffect, useRef } from 'react' +import React, { useEffect } from 'react' import './PieChart.css' import { processData } from './utils/processData' -const PieChart = ({ specs, originalData }) => { +const PieChart = ({ specs, originalData, chartRef }) => { const { data, categoryField, valueField } = processData(originalData, specs) - const chartRef = useRef() - useEffect(() => { renderPieChart() }, [data, specs]) @@ -135,6 +133,7 @@ PieChart.propTypes = { }).isRequired, }).isRequired, originalData: PropTypes.object.isRequired, + chartRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) })]), } export default React.memo(PieChart) diff --git a/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/ScatterPlot/ScatterPlot.js b/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/ScatterPlot/ScatterPlot.js index 14e6ccc29f..081b2e2511 100644 --- a/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/ScatterPlot/ScatterPlot.js +++ b/webapp/views/App/views/Data/Charts/components/Chart/components/ChartTypes/ScatterPlot/ScatterPlot.js @@ -1,23 +1,23 @@ -import * as d3 from 'd3' -import PropTypes from 'prop-types' -import React, { useEffect, useRef } from 'react' import './ScatterPlot.css' + +import React, { useEffect } from 'react' +import PropTypes from 'prop-types' +import * as d3 from 'd3' + import { createLegend } from './utils/legend' import { processData } from './utils/processData' -const ScatterPlot = ({ specs, originalData }) => { +const ScatterPlot = ({ specs, originalData, chartRef }) => { const { data, xField, yField } = processData(originalData, specs) - const chartRef = useRef() - // Shape symbols const shapeSymbols = [ - d3.symbolCircle, // Circle - d3.symbolCross, // Cross - d3.symbolDiamond, // Diamond - d3.symbolSquare, // Square - d3.symbolStar, // Star - d3.symbolTriangle, // Triangle - d3.symbolWye, // Wye + d3.symbolCircle, + d3.symbolCross, + d3.symbolDiamond, + d3.symbolSquare, + d3.symbolStar, + d3.symbolTriangle, + d3.symbolWye, ] useEffect(() => { @@ -236,6 +236,7 @@ ScatterPlot.propTypes = { }).isRequired, }).isRequired, originalData: PropTypes.array.isRequired, + chartRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) })]), } export default React.memo(ScatterPlot) diff --git a/webapp/views/App/views/Data/Charts/state/chartTypes/bar/index.js b/webapp/views/App/views/Data/Charts/state/chartTypes/bar/index.js index c90513b2f6..aad91a8b24 100644 --- a/webapp/views/App/views/Data/Charts/state/chartTypes/bar/index.js +++ b/webapp/views/App/views/Data/Charts/state/chartTypes/bar/index.js @@ -20,7 +20,7 @@ const bar = { type: 'container', blocks: { groupBy: GroupByBlock({ - subtitle: 'Select the metric to group the data ( X axis )', + subtitle: 'Select the metric to group the data (X axis)', valuesToSpec: ({ value = [], spec = {} }) => { const transform = valuesToCalculations(value) const groupBy = { @@ -42,7 +42,7 @@ const bar = { metric: GroupByBlock({ id: 'metric', title: 'Metric', - subtitle: 'Select the metric to measure the data ( Y axis )', + subtitle: 'Select the metric to measure the data (Y axis)', optionsParams: { filter: ['quantitative'] }, valuesToSpec: ({ spec = {}, value = [] }) => { const transform = valuesToCalculations(value)