Skip to content

Commit

Permalink
Charts: download button and decimal places (#3172)
Browse files Browse the repository at this point in the history
* Download buttin and decimal places

* code cleanup; solved Sonarcloud issues

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Stefano Ricci <[email protected]>
  • Loading branch information
3 people authored Dec 1, 2023
1 parent d637c11 commit 3e608ec
Show file tree
Hide file tree
Showing 7 changed files with 96 additions and 53 deletions.
12 changes: 10 additions & 2 deletions webapp/views/App/views/Data/Charts/Charts.js
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -61,7 +62,14 @@ const Charts = () => {
dimensions={dimensions}
/>

<Chart specs={spec} draft={draft} renderChart={renderChart} data={chartData} fullScreen={fullScreen} />
<Chart
specs={spec}
draft={draft}
renderChart={renderChart}
data={chartData}
fullScreen={fullScreen}
chartRef={chartRef}
/>
</Split>
</div>
)
Expand Down
50 changes: 47 additions & 3 deletions webapp/views/App/views/Data/Charts/components/Chart/Chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 (
<div className="charts_chart__container">
<button onClick={downloadPng}>Download PNG</button>
{(chartType || hasSvg) && (
<Split sizes={[70, 30]} expandToMin={true} className="wrap wrap_vertical" direction="vertical">
<div className="charts_chart__image_container">
{ChartComponent ? (
<ChartComponent specs={specs} originalData={data} />
<ChartComponent specs={specs} originalData={data} chartRef={chartRef} />
) : (
hasSvg && (
<img
src={`data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(data.svg)))}`}
id="chartImg"
alt=""
alt="chart"
width="100%"
height="100%"
/>
Expand All @@ -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
Original file line number Diff line number Diff line change
@@ -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 <div className="chart-container" ref={chartRef}></div>
}
Expand All @@ -65,6 +55,7 @@ BarChart.propTypes = {
})
),
}),
chartRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({ current: PropTypes.instanceOf(Element) })]),
}

export default React.memo(BarChart)
Original file line number Diff line number Diff line change
Expand Up @@ -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} <br/> Metric: ${metricName} <br/> Value: ${metricValue}`)
.html(`Group: ${d.data.groupBy} <br/> Metric: ${metricName} <br/> Value: ${metricValue.toFixed(2)}`)
.style('left', pos[0] + 10 + 'px')
.style('top', pos[1] - 10 + 'px')
tooltip
.html(`Group: ${d.data.groupBy} <br/> Metric: ${metricName} <br/> Value: ${metricValue}`)
.html(`Group: ${d.data.groupBy} <br/> Metric: ${metricName} <br/> Value: ${metricValue.toFixed(2)}`)
.style('left', event.pageX + 10 + 'px')
.style('top', event.pageY - 10 + 'px')
})
Expand All @@ -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} <br/> Value: ${d[metricAggregationNames[0]]}`)
.html(`Group: ${d.groupBy} <br/> Value: ${d[metricAggregationNames[0]].toFixed(2)}`)
.style('left', event.pageX + 10 + 'px')
.style('top', event.pageY - 10 + 'px')
})
Expand Down
Original file line number Diff line number Diff line change
@@ -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])
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
@@ -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(() => {
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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)
Expand Down

0 comments on commit 3e608ec

Please sign in to comment.