Tweening Paths With Svelte, D3, and Svelte Motion - store

I have a question regarding svelte animation and tweening paths. Here is a repl with the code https://svelte.dev/repl/b2509f1cfff54ee5856db44997f6fe53?version=3.53.1
I have
<script>
import { draw } from "svelte/transition";
import { tweened } from 'svelte/motion'
import { interpolateString, interpolateLab, interpolateTransformCss } from 'd3-interpolate';
import * as easings from 'svelte/easing';
import * as d3 from "d3";
import {path1} from './paths.js';
import {path2} from './paths.js';
import {path3} from './paths.js';
import {path4} from './paths.js';
let paths_1 = [
{id: "google", path: path1},
{id: "(direct)", path: path2}
]
let paths_2 = [
{id: "google", path: path3},
{id: "(direct)", path: path4}
]
let width = 800;
let height = 400;
let selected_path = paths_1;
function select(path) {
selected_path = path;
}
// animated copy of points
let animated_path = tweened(null, {
interpolate: interpolateString,
duration: 1000,
easing: easings.cubicOut
})
$: animated_path.set(selected_path);
</script>
<button on:click|preventDefault={() => select(paths_1)}>
Path1
</button>
<button on:click|preventDefault={() => select(paths_2)}>
Path2
</button>
<svg {width} {height}>
{#each $animated_path as {id, path}}
<path
stroke="black"
d={path}
/>
{/each}
</svg>
<style>
path {
fill: none;
}
</style>
But the paths are just disappearing and then the other ones are popping in again instead of making a nice transition.
I made it work with one path transitioning in this repl: https://svelte.dev/repl/560b12831bea443a84b861ce756b3a98?version=3.53.1
and looked up how to do the tweening from this one: https://svelte.dev/repl/b4c485ee69484fd8a63b8dc07c3b20a2?version=3.4.1

Related

Empty Div Preventing Interaction with amCharts5 MapChart on Vue3

I decided to dip my toes in Vue and have had an idea for a website for a while which I'd like to use amCharts5 for.
I had some issues initially as all the info I could find was related to Vue2, but I think I've somewhat wrapped my head around Vue3 and its composition API.
The MapChart is created, however there is always a div slapped on top of it which prevent any interaction. If I delete this element via DevTools, the MapChart becomes interactive.
I've tried debugging this and commenting sections of the code out, regardless this div is always created. And I simply can't figure out if it's injected by Vue or if amCharts 5 is the culprit.
The highlighted element is the one I must delete for it to become interactive.
Here's how the component is setup;
<template>
<div class="testClass" ref="chartdiv">
</div>
</template>
<script setup lang="ts">
import * as am5 from "#amcharts/amcharts5";
import * as am5map from "#amcharts/amcharts5/map";
import am5geodata_worldLow from "#amcharts/amcharts5-geodata/worldLow";
import am5themes_Animated from '#amcharts/amcharts5/themes/Animated';
import { ref, onMounted, onUnmounted } from "vue";
const chartdiv = ref<HTMLElement | null>()
var root!: am5.Root;
onMounted(() => {
if (chartdiv.value) {
// Create the Root
var root = am5.Root.new(chartdiv.value);
// Setup the MapChart
var chart = root.container.children.push(
am5map.MapChart.new(root, {
panX: "rotateX",
panY: "rotateY",
projection: am5map.geoOrthographic(),
centerMapOnZoomOut: false
})
);
// Setup Animations
root.setThemes([
am5themes_Animated.new(root)
]);
// Create MapPolygons
var polygonSeries = chart.series.push(
am5map.MapPolygonSeries.new(root, {
geoJSON: am5geodata_worldLow
})
);
// Setup MapPolygon Styling
polygonSeries.mapPolygons.template.setAll({
tooltipText: "{name}",
fill: am5.color("#909090")
});
// Setup MapPolygon Hover Styling
polygonSeries.mapPolygons.template.states.create("hover", {
fill: am5.color("#FF0000"),
stroke: am5.color("#00FF00"),
strokeWidth: 2
});
polygonSeries.mapPolygons.template.events.on("click", function(event) {
//console.log("Clicked: {0}", event.target);
});
// Setup Background
var backgroundSeries = chart.series.unshift(
am5map.MapPolygonSeries.new(root, {})
);
backgroundSeries.mapPolygons.template.setAll({
fill: am5.color(0x2c84d0),
stroke: am5.color(0x2c84d0)
});
backgroundSeries.data.push({
geometry: am5map.getGeoRectangle(90, 180, -90, -180)
});
}
});
onUnmounted(() => {
if (root) {
root.dispose();
}
});
</script>
<style scoped>
.testClass {
width: 50vw;
height: 50vh;
}
</style>
When you create a Vite-powered Vue project, it automatically creates a bunch of CSS files for you. One of those is base.css.
Inside this file, you'll find these lines which causes all the headache;
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
position: relative;
font-weight: normal;
}
Removing those lines will fix the issue.

Material UI - Stepper top and bottom labels with line

I try to make a stepper with Material UI (V5).
Here is my code:
import * as React from 'react';
import Box from '#mui/material/Box';
import Stepper from '#mui/material/Stepper';
import Step from '#mui/material/Step';
import Typography from '#mui/material/Typography';
import StepLabel from '#mui/material/StepLabel';
const steps = [
'Step 1 - Top',
'Step 2 - Top',
'Ste^3 - Top',
];
export default function HorizontalLabelPositionBelowStepper() {
return (
<Box sx={{ width: '100%' }}>
<Stepper activeStep={1} alternativeLabel
>
{steps.map((label) => (
<Step key={label}>
<Typography>Top text</Typography>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
</Box>
);
}
That gives this result:
enter image description here
How to properly position the line and the 'top text' labels?
What about aligning the icons with the line such as: image with icon and line aligned
Do I need to create a separated Grid or Box before the Stepper to create the top labels? Thank you.
thank you for your suggestions.
Marc
Bellow I have the code you need including two solutions.
Solution 1 is faster and it will work better when the label-text of the steps have similar width between them. If not you have to specify the .MuiStepConnector-root left and right properties for each child if you want to have a better looking result.
Solution 2 includes a little more code and but will work no matter the width of each label-text. As it used a background color make sure to change it to match your background.
Here is the codesandbox to play with it and see both working solutions.
Please make sure to comment or uncomment the code depending the solution you want to try.
Code:
import * as React from "react";
import Box from "#mui/material/Box";
import Stepper from "#mui/material/Stepper";
import Step from "#mui/material/Step";
import StepLabel from "#mui/material/StepLabel";
import { Typography } from "#mui/material";
const steps = ["Step 1 - Top", "Step 2 - Top", "Ste^3 - Top"];
// Solution 1
const StepperSx = {
"& .MuiStepConnector-root": {
left: "calc(-50% + 40px)",
right: "calc(50% + 40px)"
},
"& .MuiStepConnector-line": {
marginTop: "22px"
}
};
// Solution 2
// const StepperSx2 = {
// textAlign: "center",
// "& .MuiStepConnector-root": {
// zIndex: "1",
// position: "relative"
// }
// };
// Solution 2
// const TypographySx = {
// zIndex: "2",
// background: "#FFF",
// display: "inline",
// position: "relative",
// padding: "0 15px"
// };
export default function HorizontalLabelPositionBelowStepper() {
return (
<Box sx={{ width: "100%" }}>
<Stepper
activeStep={1}
alternativeLabel
sx={StepperSx} // For solution 1
// sx={StepperSx2} // For solution 2
>
{steps.map((label) => (
<Step key={label}>
<Typography
align="center"
// sx={TypographySx} // For solution 2
>
Top text
</Typography>
<StepLabel>{label}</StepLabel>
</Step>
))}
</Stepper>
</Box>
);
}

How to fetch coordinates of Polygon in React.JS using react-google-maps

I wants to fetch all the coordinates of a polygon drawn on Google's Map. And here is my code
import React from "react";
import { compose, withProps } from "recompose";
import {
withScriptjs,
withGoogleMap,
GoogleMap,
Marker
} from "react-google-maps";
//import withScriptjs from "react-google-maps/lib/async/withScriptjs";
import { DrawingManager } from "react-google-maps/lib/components/drawing/DrawingManager";
const MyMapComponent = compose(
withProps({
/**
* Note: create and replace your own key in the Google console.
* https://console.developers.google.com/apis/dashboard
* The key "AIzaSyBkNaAGLEVq0YLQMi-PYEMabFeREadYe1Q" can be ONLY used in this sandbox (no forked).
*/
googleMapURL:
"https://maps.googleapis.com/maps/api/js?key=AIzaSyALpmb4KhFoR2Kcvty21gzzegprl4ilIgs&v=3.exp&libraries=geometry,drawing,places",
loadingElement: <div style={{ height: `100%` }} />,
containerElement: <div style={{ height: `400px` }} />,
mapElement: <div style={{ height: `100%` }} />
}),
withScriptjs,
withGoogleMap
)(props => (
<GoogleMap
defaultZoom={8}
defaultCenter={new window.google.maps.LatLng(-34.397, 150.644)}
>
<DrawingManager
defaultDrawingMode={
window.google.maps.drawing.OverlayType.ControlPosition
}
defaultOptions={{
drawingControl: true,
drawingControlOptions: {
position: window.google.maps.ControlPosition.TOP_CENTER,
drawingModes: [
window.google.maps.drawing.OverlayType.CIRCLE,
window.google.maps.drawing.OverlayType.POLYGON,
window.google.maps.drawing.OverlayType.POLYLINE,
window.google.maps.drawing.OverlayType.RECTANGLE
]
},
circleOptions: {
fillColor: `#ffff00`,
fillOpacity: 1,
strokeWeight: 5,
clickable: false,
editable: true,
zIndex: 1
}
}}
/>
{props.isMarkerShown && (
<Marker position={{ lat: -34.397, lng: 150.644 }} />
)}
</GoogleMap>
));
My focus of work is to fetch all the coordinates of that polygon that should be drawn on Google Maps.I also wants to store these coordinates in MongoDB using mongoose and NodeJs as backend.
We can use this function to get all the coordinates of a polygon or any other reactangle.
function getPaths(polygon) {
var polygonBounds = polygon.getPath();
var bounds = [];
for (var i = 0; i < polygonBounds.length; i++) {
var point = {
lat: polygonBounds.getAt(i).lat(),
lng: polygonBounds.getAt(i).lng()
};
bounds.push(point);
}
console.log(bounds);
}
And in GoogleMap component, i simplified the above code by given way.
<DrawingManager
drawingMode={"polygon"}
onPolygonComplete={value => console.log(getPaths(value))} />

Displaying a list of Markers with React

im trying to display a list of Markers with lat and lng stored on my mongodb database.
Im using the google-maps-react and this is my child map component
import {
Map,
InfoWindow,
Marker,
GoogleApiWrapper,
Polygon
} from 'google-maps-react';
import React, { Component } from 'react';
export class MapContainer extends Component {
render() {
const coords = this.props.initialCenter;
const position = this.props.position;
const paths = this.props.paths;
const style = this.props.style;
const center = this.props.center;
console.log(this.props);
return (
<Map
google={this.props.google}
zoom={18}
initialCenter={coords}
style={style}
center={center}
>
<Marker
onClick={this.onMarkerClick}
name={'Current location'}
position={position}
/>
<InfoWindow onClose={this.onInfoWindowClose}>
<div>
<h1>Test</h1>
</div>
</InfoWindow>
<Polygon
paths={paths}
strokeColor='#0000FF'
strokeOpacity={0.8}
strokeWeight={2}
fillColor='#0000FF'
fillOpacity={0.35}
/>
</Map>
);
}
}
export default GoogleApiWrapper({
apiKey: process.env.REACT_APP_GOOGLE_MAPS_API_TEST
})(MapContainer);
I get the coordinates from my endpoint here
const centers = this.props.places.places.map(place => {
return {
lat: place.center[0],
lng: place.center[1]
};
});
And im trying to display the list like this:
const position = { ...centers} ->does nothing
//const position = { lat: 41.53113384600326, lng: -8.619018495082855 }; -> prints one Marker
//const position = centers[0] -> prints one Marker
Im using that child component like this:
<MapContainer
initialCenter={initialCenter}
position={position}
paths={paths}
style={style}
center={center}
/>
Any help on this ?
EDIT:
Changed child component to
<Marker
onClick={this.onMarkerClick}
name={'Current location'}
position={{ position }}
/>
And on parent:
Where centers is:
<MapContainer
initialCenter={initialCenter}
paths={paths}
style={style}
center={center}
position={centers.map(p => (
<Marker
onClick={() => this.onMarkerClick(p)}
name={'Current location'} // You should probably have a "name" field for each positions
position={p}
/>
))}
></MapContainer>
const centers = this.props.places.places.map(place => {
return {
lat: place.center[0],
lng: place.center[1]
};
});
Still not showing
You need to send the array of position as props to your component, and then render them all like this:
this.props.positions.map(p => <Marker
onClick={() => this.onMarkerClick(p)}
name={'Current location'} // You should probably have a "name" field for each positions
position={p}
/>)

Exporting dc.js chart from SVG to PNG

I have a dc.js chart and I want to export it as a PNG image, using exupero's saveSvgAsPng:
function save() {
var options = {};
options.backgroundColor = '#ffffff';
options.selectorRemap = function(s) { return s.replace(/\.dc-chart/g, ''); };
var chart = document.getElementById('chart').getElementsByTagName('svg')[0];
saveSvgAsPng(chart, 'chart.png', options)
}
var data = [
{day: 1, service: 'ABC', count: 100},
{day: 2, service: 'ABC', count: 80},
{day: 4, service: 'ABC', count: 10},
{day: 7, service: 'XYZ', count: 380},
{day: 8, service: 'XYZ', count: 400}
];
var ndx = crossfilter(data);
var dim = ndx.dimension(function(d){return [d.service, d.day];});
var grp = dim.group().reduceSum(function(d) { return d.count; });
grp = fillGroup(grp, d3.cross(['ABC', 'XYZ'], d3.range(1, 9)));
var chart= dc.seriesChart("#chart")
.width(500)
.height(180)
.chart(function(c) { return dc.lineChart(c).renderArea(true).curve(d3.curveCardinal); })
.dimension(dim)
.group(grp)
.brushOn(false)
.seriesAccessor(function(d) { return d.key[0]; })
.keyAccessor(function(d) { return d.key[1]; })
.valueAccessor(function(d) { return +d.value; })
.x(d3.scaleLinear())
.elasticX(true)
.y(d3.scaleLinear().domain([0, 450]))
.legend(dc.legend().horizontal(false).x(60).y(10))
.yAxisLabel("Count")
.render();
function fillGroup(grupo, rango) {
return {
all:function () {
var resultados = grupo.all().slice(0);
var encontrado = {};
resultados.forEach(function(d) {
encontrado[d.key] = true;
});
rango.forEach(function(d) {
if (!encontrado[d]) { resultados.push({key: d, value: 0}); }
});
return resultados;
}
};
}
/* Please ignore what follows - it's the minified SaveSvgAsPng library,
I haven't found any CDN for it... */
(function(){const out$=typeof exports!='undefined'&&exports||typeof define!='undefined'&&{}||this||window;if(typeof define!=='undefined')define(()=>out$);const xmlns='http://www.w3.org/2000/xmlns/';const doctype='<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [<!ENTITY nbsp " ">]>';const urlRegex=/url\(["']?(.+?)["']?\)/;const fontFormats={woff2:'font/woff2',woff:'font/woff',otf:'application/x-font-opentype',ttf:'application/x-font-ttf',eot:'application/vnd.ms-fontobject',sfnt:'application/font-sfnt',svg:'image/svg+xml'};const isElement=obj=>obj instanceof HTMLElement||obj instanceof SVGElement;const requireDomNode=el=>{if(!isElement(el))throw new Error(`an HTMLElement or SVGElement is required; got ${el}`)};const isExternal=url=>url&&url.lastIndexOf('http',0)===0&&url.lastIndexOf(window.location.host)===-1;const getFontMimeTypeFromUrl=fontUrl=>{const formats=Object.keys(fontFormats).filter(extension=>fontUrl.indexOf(`.${extension}`)>0).map(extension=>fontFormats[extension]);if(formats)return formats[0];console.error(`Unknown font format for ${fontUrl}. Fonts may not be working correctly.`);return'application/octet-stream'};const arrayBufferToBase64=buffer=>{let binary='';const bytes=new Uint8Array(buffer);for(let i=0;i<bytes.byteLength;i++)binary+=String.fromCharCode(bytes[i]);return window.btoa(binary)}
const getDimension=(el,clone,dim)=>{const v=(el.viewBox&&el.viewBox.baseVal&&el.viewBox.baseVal[dim])||(clone.getAttribute(dim)!==null&&!clone.getAttribute(dim).match(/%$/)&&parseInt(clone.getAttribute(dim)))||el.getBoundingClientRect()[dim]||parseInt(clone.style[dim])||parseInt(window.getComputedStyle(el).getPropertyValue(dim));return typeof v==='undefined'||v===null||isNaN(parseFloat(v))?0:v};const getDimensions=(el,clone,width,height)=>{if(el.tagName==='svg')return{width:width||getDimension(el,clone,'width'),height:height||getDimension(el,clone,'height')};else if(el.getBBox){const{x,y,width,height}=el.getBBox();return{width:x+width,height:y+height}}};const reEncode=data=>decodeURIComponent(encodeURIComponent(data).replace(/%([0-9A-F]{2})/g,(match,p1)=>{const c=String.fromCharCode(`0x${p1}`);return c==='%'?'%25':c}));const uriToBlob=uri=>{const byteString=window.atob(uri.split(',')[1]);const mimeString=uri.split(',')[0].split(':')[1].split(';')[0]
const buffer=new ArrayBuffer(byteString.length);const intArray=new Uint8Array(buffer);for(let i=0;i<byteString.length;i++){intArray[i]=byteString.charCodeAt(i)}
return new Blob([buffer],{type:mimeString})};const query=(el,selector)=>{if(!selector)return;try{return el.querySelector(selector)||el.parentNode&&el.parentNode.querySelector(selector)}catch(err){console.warn(`Invalid CSS selector "${selector}"`,err)}};const detectCssFont=rule=>{const match=rule.cssText.match(urlRegex);const url=(match&&match[1])||'';if(!url||url.match(/^data:/)||url==='about:blank')return;const fullUrl=url.startsWith('../')?`${rule.href}/../${url}`:url.startsWith('./')?`${rule.href}/.${url}`:url;return{text:rule.cssText,format:getFontMimeTypeFromUrl(fullUrl),url:fullUrl}};const inlineImages=el=>Promise.all(Array.from(el.querySelectorAll('image')).map(image=>{let href=image.getAttributeNS('http://www.w3.org/1999/xlink','href')||image.getAttribute('href');if(!href)return Promise.resolve(null);if(isExternal(href)){href+=(href.indexOf('?')===-1?'?':'&')+'t='+new Date().valueOf()}
return new Promise((resolve,reject)=>{const canvas=document.createElement('canvas');const img=new Image();img.crossOrigin='anonymous';img.src=href;img.onerror=()=>reject(new Error(`Could not load ${href}`));img.onload=()=>{canvas.width=img.width;canvas.height=img.height;canvas.getContext('2d').drawImage(img,0,0);image.setAttributeNS('http://www.w3.org/1999/xlink','href',canvas.toDataURL('image/png'));resolve(!0)}})}));const cachedFonts={};const inlineFonts=fonts=>Promise.all(fonts.map(font=>new Promise((resolve,reject)=>{if(cachedFonts[font.url])return resolve(cachedFonts[font.url]);const req=new XMLHttpRequest();req.addEventListener('load',()=>{const fontInBase64=arrayBufferToBase64(req.response);const fontUri=font.text.replace(urlRegex,`url("data:${font.format};base64,${fontInBase64}")`)+'\n';cachedFonts[font.url]=fontUri;resolve(fontUri)});req.addEventListener('error',e=>{console.warn(`Failed to load font from: ${font.url}`,e);cachedFonts[font.url]=null;resolve(null)});req.addEventListener('abort',e=>{console.warn(`Aborted loading font from: ${font.url}`,e);resolve(null)});req.open('GET',font.url);req.responseType='arraybuffer';req.send()}))).then(fontCss=>fontCss.filter(x=>x).join(''));let cachedRules=null;const styleSheetRules=()=>{if(cachedRules)return cachedRules;return cachedRules=Array.from(document.styleSheets).map(sheet=>{try{return sheet.cssRules}catch(e){console.warn(`Stylesheet could not be loaded: ${sheet.href}`)}})};const inlineCss=(el,options)=>{const{selectorRemap,modifyStyle,modifyCss,fonts}=options||{};const generateCss=modifyCss||((selector,properties)=>{const sel=selectorRemap?selectorRemap(selector):selector;const props=modifyStyle?modifyStyle(properties):properties;return `${sel}{${props}}\n`});const css=[];const detectFonts=typeof fonts==='undefined';const fontList=fonts||[];styleSheetRules().forEach(rules=>{if(!rules)return;Array.from(rules).forEach(rule=>{if(typeof rule.style!='undefined'){if(query(el,rule.selectorText))css.push(generateCss(rule.selectorText,rule.style.cssText));else if(detectFonts&&rule.cssText.match(/^#font-face/)){const font=detectCssFont(rule);if(font)fontList.push(font)}else css.push(rule.cssText)}})});return inlineFonts(fontList).then(fontCss=>css.join('\n')+fontCss)};out$.prepareSvg=(el,options,done)=>{requireDomNode(el);const{left=0,top=0,width:w,height:h,scale=1,responsive=!1,}=options||{};return inlineImages(el).then(()=>{let clone=el.cloneNode(!0);const{width,height}=getDimensions(el,clone,w,h);if(el.tagName!=='svg'){if(el.getBBox){clone.setAttribute('transform',clone.getAttribute('transform').replace(/translate\(.*?\)/,''));const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');svg.appendChild(clone);clone=svg}else{console.error('Attempted to render non-SVG element',el);return}}
clone.setAttribute('version','1.1');clone.setAttribute('viewBox',[left,top,width,height].join(' '));if(!clone.getAttribute('xmlns'))clone.setAttributeNS(xmlns,'xmlns','http://www.w3.org/2000/svg');if(!clone.getAttribute('xmlns:xlink'))clone.setAttributeNS(xmlns,'xmlns:xlink','http://www.w3.org/1999/xlink');if(responsive){clone.removeAttribute('width');clone.removeAttribute('height');clone.setAttribute('preserveAspectRatio','xMinYMin meet')}else{clone.setAttribute('width',width*scale);clone.setAttribute('height',height*scale)}
Array.from(clone.querySelectorAll('foreignObject > *')).forEach(foreignObject=>{if(!foreignObject.getAttribute('xmlns'))
foreignObject.setAttributeNS(xmlns,'xmlns','http://www.w3.org/1999/xhtml')});return inlineCss(el,options).then(css=>{const style=document.createElement('style');style.setAttribute('type','text/css');style.innerHTML=`<![CDATA[\n${css}\n]]>`;const defs=document.createElement('defs');defs.appendChild(style);clone.insertBefore(defs,clone.firstChild);const outer=document.createElement('div');outer.appendChild(clone);const src=outer.innerHTML.replace(/NS\d+:href/gi,'xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href');if(typeof done==='function')done(src,width,height);else return{src,width,height}})})};out$.svgAsDataUri=(el,options,done)=>{requireDomNode(el);const result=out$.prepareSvg(el,options).then(({src})=>`data:image/svg+xml;base64,${window.btoa(reEncode(doctype+src))}`);if(typeof done==='function')return result.then(done);return result};out$.svgAsPngUri=(el,options,done)=>{requireDomNode(el);const{encoderType='image/png',encoderOptions=0.8,backgroundColor,canvg}=options||{};const convertToPng=({src,width,height})=>{const canvas=document.createElement('canvas');const context=canvas.getContext('2d');const pixelRatio=window.devicePixelRatio||1;canvas.width=width*pixelRatio;canvas.height=height*pixelRatio;canvas.style.width=`${canvas.width}px`;canvas.style.height=`${canvas.height}px`;context.setTransform(pixelRatio,0,0,pixelRatio,0,0);if(canvg)canvg(canvas,src);else context.drawImage(src,0,0);if(backgroundColor){context.globalCompositeOperation='destination-over';context.fillStyle=backgroundColor;context.fillRect(0,0,canvas.width,canvas.height)}
let png;try{png=canvas.toDataURL(encoderType,encoderOptions)}catch(e){if((typeof SecurityError!=='undefined'&&e instanceof SecurityError)||e.name==='SecurityError'){console.error('Rendered SVG images cannot be downloaded in this browser.');return}else throw e}
if(typeof done==='function')done(png);return Promise.resolve(png)}
if(canvg)return out$.prepareSvg(el,options).then(convertToPng);else return out$.svgAsDataUri(el,options).then(uri=>{return new Promise((resolve,reject)=>{const image=new Image();image.onload=()=>resolve(convertToPng({src:image,width:image.width,height:image.height}));image.onerror=()=>{reject(`There was an error loading the data URI as an image on the following SVG\n${window.atob(uri.slice(26))}Open the following link to see browser's diagnosis\n${uri}`)}
image.src=uri})})};out$.download=(name,uri)=>{if(navigator.msSaveOrOpenBlob)navigator.msSaveOrOpenBlob(uriToBlob(uri),name);else{const saveLink=document.createElement('a');if('download' in saveLink){saveLink.download=name;saveLink.style.display='none';document.body.appendChild(saveLink);try{const blob=uriToBlob(uri);const url=URL.createObjectURL(blob);saveLink.href=url;saveLink.onclick=()=>requestAnimationFrame(()=>URL.revokeObjectURL(url))}catch(e){console.warn('This browser does not support object URLs. Falling back to string URL.');saveLink.href=uri}
saveLink.click();document.body.removeChild(saveLink)}
else{window.open(uri,'_temp','menubar=no,toolbar=no,status=no')}}};out$.saveSvg=(el,name,options)=>{requireDomNode(el);out$.svgAsDataUri(el,options||{},uri=>out$.download(name,uri))};out$.saveSvgAsPng=(el,name,options)=>{requireDomNode(el);out$.svgAsPngUri(el,options||{},uri=>out$.download(name,uri))}})()
circle.dot { fill-opacity:0.5 !important; }
/* Please ignore what follows - it's the minified version of
https://cdnjs.cloudflare.com/ajax/libs/dc/3.0.4/dc.css, I had to include it here
because if it's stored in a different domain, SaveSvgAsPng can't load it */
.dc-chart path.dc-symbol,.dc-legend g.dc-legend-item.fadeout{fill-opacity:.5;stroke-opacity:.5}div.dc-chart{float:left}.dc-chart rect.bar{stroke:none;cursor:pointer}.dc-chart rect.bar:hover{fill-opacity:.5}.dc-chart rect.deselected{stroke:none;fill:#ccc}.dc-chart .pie-slice{fill:#fff;font-size:12px;cursor:pointer}.dc-chart .pie-slice.external{fill:#000}.dc-chart .pie-slice :hover,.dc-chart .pie-slice.highlight{fill-opacity:.8}.dc-chart .pie-path{fill:none;stroke-width:2px;stroke:#000;opacity:.4}.dc-chart .selected path,.dc-chart .selected circle{stroke-width:3;stroke:#ccc;fill-opacity:1}.dc-chart .deselected path,.dc-chart .deselected circle{stroke:none;fill-opacity:.5;fill:#ccc}.dc-chart .axis path,.dc-chart .axis line{fill:none;stroke:#000;shape-rendering:crispEdges}.dc-chart .axis text{font:10px sans-serif}.dc-chart .grid-line,.dc-chart .axis .grid-line,.dc-chart .grid-line line,.dc-chart .axis .grid-line line{fill:none;stroke:#ccc;opacity:.5;shape-rendering:crispEdges}.dc-chart .brush rect.selection{fill:#4682b4;fill-opacity:.125}.dc-chart .brush .custom-brush-handle{fill:#eee;stroke:#666;cursor:ew-resize}.dc-chart path.line{fill:none;stroke-width:1.5px}.dc-chart path.area{fill-opacity:.3;stroke:none}.dc-chart path.highlight{stroke-width:3;fill-opacity:1;stroke-opacity:1}.dc-chart g.state{cursor:pointer}.dc-chart g.state :hover{fill-opacity:.8}.dc-chart g.state path{stroke:#fff}.dc-chart g.deselected path{fill:gray}.dc-chart g.deselected text{display:none}.dc-chart g.row rect{fill-opacity:.8;cursor:pointer}.dc-chart g.row rect:hover{fill-opacity:.6}.dc-chart g.row text{fill:#fff;font-size:12px;cursor:pointer}.dc-chart g.dc-tooltip path{fill:none;stroke:gray;stroke-opacity:.8}.dc-chart g.county path{stroke:#fff;fill:none}.dc-chart g.debug rect{fill:#00f;fill-opacity:.2}.dc-chart g.axis text{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}.dc-chart .node{font-size:.7em;cursor:pointer}.dc-chart .node :hover{fill-opacity:.8}.dc-chart .bubble{stroke:none;fill-opacity:.6}.dc-chart .highlight{fill-opacity:1;stroke-opacity:1}.dc-chart .fadeout{fill-opacity:.2;stroke-opacity:.2}.dc-chart .box text{font:10px sans-serif;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}.dc-chart .box line{fill:#fff}.dc-chart .box rect,.dc-chart .box line,.dc-chart .box circle{stroke:#000;stroke-width:1.5px}.dc-chart .box .center{stroke-dasharray:3,3}.dc-chart .box .data{stroke:none;stroke-width:0}.dc-chart .box .outlier{fill:none;stroke:#ccc}.dc-chart .box .outlierBold{fill:red;stroke:none}.dc-chart .box.deselected{opacity:.5}.dc-chart .box.deselected .box{fill:#ccc}.dc-chart .symbol{stroke:none}.dc-chart .heatmap .box-group.deselected rect{stroke:none;fill-opacity:.5;fill:#ccc}.dc-chart .heatmap g.axis text{pointer-events:all;cursor:pointer}.dc-chart .empty-chart .pie-slice{cursor:default}.dc-chart .empty-chart .pie-slice path{fill:#fee;cursor:default}.dc-chart circle.dot{stroke:none}.dc-data-count{float:right;margin-top:15px;margin-right:15px}.dc-data-count .filter-count,.dc-data-count .total-count{color:#3182bd;font-weight:700}.dc-legend{font-size:11px}.dc-legend .dc-legend-item{cursor:pointer}.dc-hard .number-display{float:none}div.dc-html-legend{overflow-y:auto;overflow-x:hidden;height:inherit;float:right;padding-right:2px}div.dc-html-legend .dc-legend-item-horizontal{display:inline-block;margin-left:5px;margin-right:5px;cursor:pointer}div.dc-html-legend .dc-legend-item-horizontal.selected{background-color:#3182bd;color:white}div.dc-html-legend .dc-legend-item-vertical{display:block;margin-top:5px;padding-top:1px;padding-bottom:1px;cursor:pointer}div.dc-html-legend .dc-legend-item-vertical.selected{background-color:#3182bd;color:white}div.dc-html-legend .dc-legend-item-color{display:table-cell;width:12px;height:12px}div.dc-html-legend .dc-legend-item-label{line-height:12px;display:table-cell;vertical-align:middle;padding-left:3px;padding-right:3px;font-size:.75em}.dc-html-legend-container{height:inherit}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crossfilter/1.3.12/crossfilter.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dc/3.0.4/dc.min.js"></script>
<div id="chart"></div>
<button id="export" onclick="save()">Export as PNG</button>
Basically, I just get the SVG DOM element, and pass it to the saveSvgAsPng function:
var chart = document.getElementById('chart').getElementsByTagName('svg')[0];
saveSvgAsPng(chart, 'chart.png', options);
This is how the dc.js chart looks like:
And this is how the exported PNG looks:
Why does it show lines/areas/circles under the X axis (and beyond the horizontal limits too)? How can I fix it?
The <defs><clipPath /></defs> section is present within the SVG element, and I guess it's properly defined (right?).
I haven't tried saveSvgAsPng, so this is just a guess, but you could try
chart.select('g.chart-body').attr('clip-path',
chart.select('g.chart-body').attr('clip-path').replace(/.*#/, 'url(#'))
Reasoning: dc.js uses an obscure form of the clip-path attribute with an absolute URL. It's looking for the URL of the current page using window.location.href and that could go wrong, or saveSvgAsPng might not expect an absolute URL.
It does this for Angular compatibility but I can see why this would confuse a library.
The code above will remove the base URL, leaving only the relative hash part.
If this helps, we can add an option for this behavior.
I'm not self-answering, I just want to add a side note, which might be helpful for other SaveSvgAsPng users:
For the exported PNG to have the same look as the SVG, SaveSvgAsPng needs to properly apply the CSS styles. Otherwise, it would look like this:
If you run into this problem, please note that:
The stylesheets need to be stored in the same domain as the javascript code, otherwise the library won't be able to load them (for security reasons).
Most dc.js' styles are applied to the .dc-chart class or its children. This CSS class is applied to the parent DIV, not to the SVG element, which is what SaveSvgAsPng exports. Therefore, you will have to remove the selector from the CSS rules. The easiest way to do so is using the selectorRemap option, like this:
var options = {
selectorRemap: function(s) { return s.replace(/\.dc-chart/g, ''); }
};
var chart = document.getElementById('chart').getElementsByTagName('svg')[0];
saveSvgAsPng(chart, 'chart.png', options);
I'm not familiar with saveSvgAsPng, it might be that it's already using canvas. If It's the case, please downvote my question, probably not going to be useful ;)
Did you try using the svg->canvas->png path? I did use it with other d3 projects and worked fine.
This is a snippet lifted from another answer on that question:
var btn = document.querySelector('button');
var svg = document.querySelector('svg');
var canvas = document.querySelector('canvas');
function triggerDownload (imgURI) {
var evt = new MouseEvent('click', {
view: window,
bubbles: false,
cancelable: true
});
var a = document.createElement('a');
a.setAttribute('download', 'MY_COOL_IMAGE.png');
a.setAttribute('href', imgURI);
a.setAttribute('target', '_blank');
a.dispatchEvent(evt);
}
btn.addEventListener('click', function () {
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var data = (new XMLSerializer()).serializeToString(svg);
var DOMURL = window.URL || window.webkitURL || window;
var img = new Image();
var svgBlob = new Blob([data], {type: 'image/svg+xml;charset=utf-8'});
var url = DOMURL.createObjectURL(svgBlob);
img.onload = function () {
ctx.drawImage(img, 0, 0);
DOMURL.revokeObjectURL(url);
var imgURI = canvas
.toDataURL('image/png')
.replace('image/png', 'image/octet-stream');
triggerDownload(imgURI);
};
img.src = url;
});
<button>svg to png</button>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="200" height="200">
<rect x="10" y="10" width="50" height="50" />
<text x="0" y="100">Look, i'm cool</text>
</svg>
<canvas id="canvas"></canvas>

Resources