@@ -13,7 +13,7 @@ import {
1313 Legend ,
1414} from 'chart.js' ;
1515import zoomPlugin from 'chartjs-plugin-zoom' ;
16- import { ImageDown , Copy , FileDown , Minimize2 } from 'lucide-react' ;
16+ import { ImageDown , Copy , FileDown , ImageMinus } from 'lucide-react' ;
1717import { getMinSteps } from "../utils/getMinSteps.js" ;
1818import { useTranslation } from 'react-i18next' ;
1919
@@ -194,6 +194,65 @@ export default function ChartContainer({
194194 }
195195 } , [ t ] ) ;
196196
197+ const copyChartSmallImage = useCallback ( async ( id ) => {
198+ const chart = chartRefs . current . get ( id ) ;
199+ if ( ! chart || ! navigator ?. clipboard ) return ;
200+
201+ const maxSize = 50 * 1024 ; // 50KB
202+ const canvas = chart . canvas ;
203+
204+ const tempCanvas = document . createElement ( 'canvas' ) ;
205+ const ctx = tempCanvas . getContext ( '2d' ) ;
206+
207+ let scale = 1 ;
208+ let quality = 0.8 ;
209+ let dataUrl ;
210+ let attempts = 0 ;
211+ const maxAttempts = 20 ;
212+
213+ while ( attempts < maxAttempts ) {
214+ const width = Math . floor ( canvas . width * scale ) ;
215+ const height = Math . floor ( canvas . height * scale ) ;
216+
217+ tempCanvas . width = width ;
218+ tempCanvas . height = height ;
219+
220+ ctx . fillStyle = '#ffffff' ;
221+ ctx . fillRect ( 0 , 0 , width , height ) ;
222+ ctx . drawImage ( canvas , 0 , 0 , width , height ) ;
223+
224+ dataUrl = tempCanvas . toDataURL ( 'image/jpeg' , quality ) ;
225+
226+ const base64Length = dataUrl . length - 'data:image/jpeg;base64,' . length ;
227+ const fileSize = Math . ceil ( base64Length * 0.75 ) ;
228+
229+ if ( fileSize <= maxSize ) {
230+ break ;
231+ }
232+
233+ if ( quality > 0.3 ) {
234+ quality -= 0.1 ;
235+ } else if ( scale > 0.3 ) {
236+ scale -= 0.1 ;
237+ quality = 0.7 ;
238+ } else {
239+ break ;
240+ }
241+
242+ attempts ++ ;
243+ }
244+
245+ try {
246+ const res = await fetch ( dataUrl ) ;
247+ const blob = await res . blob ( ) ;
248+ await navigator . clipboard . write ( [
249+ new ClipboardItem ( { 'image/jpeg' : blob } )
250+ ] ) ;
251+ } catch ( e ) {
252+ console . error ( t ( 'copyImageError' ) , e ) ;
253+ }
254+ } , [ t ] ) ;
255+
197256 const exportChartCSV = useCallback ( ( id ) => {
198257 const chart = chartRefs . current . get ( id ) ;
199258 if ( ! chart ) return ;
@@ -832,7 +891,7 @@ export default function ChartContainer({
832891 aria-label = { t ( 'exportSmallImage' ) }
833892 title = { t ( 'exportSmallImage' ) }
834893 >
835- < Minimize2 size = { 16 } />
894+ < ImageMinus size = { 16 } />
836895 </ button >
837896 < button
838897 type = "button"
@@ -843,6 +902,15 @@ export default function ChartContainer({
843902 >
844903 < Copy size = { 16 } />
845904 </ button >
905+ < button
906+ type = "button"
907+ className = "p-1 rounded-md text-gray-600 hover:text-green-600 hover:bg-gray-100"
908+ onClick = { ( ) => copyChartSmallImage ( `metric-comp-${ idx } ` ) }
909+ aria-label = { t ( 'copySmallImage' ) }
910+ title = { t ( 'copySmallImage' ) }
911+ >
912+ < ImageMinus size = { 16 } />
913+ </ button >
846914 < button
847915 type = "button"
848916 className = "p-1 rounded-md text-gray-600 hover:text-blue-600 hover:bg-gray-100"
@@ -891,7 +959,7 @@ export default function ChartContainer({
891959 aria-label = { t ( 'exportSmallImage' ) }
892960 title = { t ( 'exportSmallImage' ) }
893961 >
894- < Minimize2 size = { 16 } />
962+ < ImageMinus size = { 16 } />
895963 </ button >
896964 < button
897965 type = "button"
@@ -902,6 +970,15 @@ export default function ChartContainer({
902970 >
903971 < Copy size = { 16 } />
904972 </ button >
973+ < button
974+ type = "button"
975+ className = "p-1 rounded-md text-gray-600 hover:text-green-600 hover:bg-gray-100"
976+ onClick = { ( ) => copyChartSmallImage ( `metric-${ idx } ` ) }
977+ aria-label = { t ( 'copySmallImage' ) }
978+ title = { t ( 'copySmallImage' ) }
979+ >
980+ < ImageMinus size = { 16 } />
981+ </ button >
905982 < button
906983 type = "button"
907984 className = "p-1 rounded-md text-gray-600 hover:text-blue-600 hover:bg-gray-100"
0 commit comments