Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 6 years ago.
Improve this question
I'm looking to build a moble app where the user can take an SVG image, manipulate it under a "recording", and then send the recording to a friend for them to "replay".
I have some experience with D3.js and have also looked at Snap.svg library for SVG manipulation but I'm not fully wrapping my head around how to implement this.
In particular, what's a good way to be able to save the manipulations the user is making and then "replay" them? For example, I can use D3.js to manipulate the SVG, but since this is code-based, I can't exactly "serialize" the animation in order to send to someone else and for them to be able to "replay" it. I don't want to go down the route of code generation..
Any ideas how to do this?
I am not sure how complex your application targets are. However, it is easy to serialise updates in an SVG as JSON array and replay them using d3. Here is a small example for the same.
Steps:
Update SVG by using update buttons.
Revert the chart by clicking on clear button.
Now click play button to replay the last updates.
var defaults = [{
key: "r",
val: 15,
type: "attr"
}, {
key: "fill",
val: "blue",
type: "style"
}];
var updates = [];
var c20 = d3.scale.category20();
var circles = d3.selectAll("circle");
d3.select("#increase_rad")
.on("click", function() {
var val = parseInt(d3.select("circle").attr("r")) + 1;
circles.attr("r", val);
updates.push({
"key": "r",
"val": val,
"type": "attr"
});
enableButton();
});
d3.select("#decrease_rad")
.on("click", function() {
var val = parseInt(d3.select("circle").attr("r")) - 1;
circles.attr("r", val);
updates.push({
"key": "r",
"val": val,
"type": "attr"
});
enableButton();
});
d3.select("#change_color")
.on("click", function() {
var val = c20(Math.floor(Math.random() * (18) + 1));
circles.style("fill", val);
updates.push({
"key": "fill",
"val": val,
"type": "style"
});
enableButton();
});
d3.select("#clear")
.on("click", function() {
applyEffects(defaults);
});
d3.select("#play")
.on("click", function() {
applyEffects(updates);
});
function applyEffects(effects, delay) {
var trans = circles.transition()
effects.forEach(function(update) {
if (update.type == "attr") {
trans = trans.attr(update.key, update.val).transition();
} else {
trans = trans.style(update.key, update.val).transition();
}
});
}
function enableButton() {
d3.select("#clear").attr("disabled", null);
d3.select("#play").attr("disabled", null);
}
svg {
background: white;
}
.link {
stroke: black;
}
.node {
fill: blue;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div>
<input type="button" value="Clear" id="clear" disabled />
<input type="button" value="Play" id="play" disabled />
<br/>
<br/>
<input type="button" value="Increase Radius" id="increase_rad" />
<input type="button" value="Decrease Radius" id="decrease_rad" />
<input type="button" value="Change Color" id="change_color" />
</div>
<svg width="250" height="250">
<g id="links">
<line class="link" x1="138.0538594815113" y1="55.927846328346924" x2="58.77306466322782" y2="110.43892621419347"></line>
<line class="link" x1="138.0538594815113" y1="55.927846328346924" x2="195.04044384802015" y2="133.44259356292176"></line>
</g>
<g id="nodes">
<g class="node" transform="translate(138.0538594815113,55.927846328346924)">
<circle r=15></circle>
</g>
<g class="node" transform="translate(58.77306466322782,110.43892621419347)">
<circle r=15></circle>
</g>
<g class="node" transform="translate(195.04044384802015,133.44259356292176)">
<circle r=15></circle>
</g>
</g>
</svg>
Related
im using the Vue.js 3 and D3.js v7 to making the flowchart.
My problem here is I can't dynamically append the component inside the D3.js.
My component is imported like shown below.
components: {
StoryPanel: defineAsyncComponent(() => import("#/Pages/Story/Partials/StoryPanel"))
},
let nodes = container.selectAll("g")
.data(root.descendants())
.join("g")
.append("foreignObject")
.attr("width", entityWidth - 10)
.attr("height", 150)
nodes.append("xhtml:div")
.attr("class", "border border-black")
.html(function (d) {
// return '<StoryPanel />' tried this but not working
})
.style("border", '1px solid black')
This is the generated html
<foreignObject width="190" height="150" transform="translate(-95,10)">
<div class="border border-black" style="border: 1px solid black;">
<storypanel></storypanel>
</div>
</foreignObject>
[Edit 1]
Tried this Rendering Vue 3 component into HTML string as #MichalLevĂ˝ suggested, but still not working
.html(function (d) {
const tempApp = createApp({
render() {
return h(StoryPanel,{step: d})
}
})
const el = document.createElement('div');
const mountedApp = tempApp.mount(el)
return mountedApp.$el.outerHTML
})
[Edit 2]
I found it works only when using const instead of a Component.vue
const CircleWithTextSvg = {
name: 'CircleWithTextSvg',
props: {
text: {
type: String,
default: "1"
},
fill: {
type: String,
default: "white"
}
},
template: `<div>template</div>`
}
Any help is appreciated. Thank you.
Use createApp
import {createApp} from 'vue';
and use this instead and return the outerHTML
const app = createApp(Component, {prop1: this.prop1})
.mount(document.createElement('div'))
return app.$el.outerHTML
I am attempting to create a tooltip that will appear when the user hover overs the icon, the tootltip will give information regarding the event. The tag is within an SVG image.
This is the code for the image:
<image class="tooltip-trigger" data-tooltip-text="Battles" id="Battles" x="100" y="200" width="30" height="30" xlink:href="battle.png" />
<g id="tooltip" visibility="hidden" >
<rect x="2" y="2" width="80" height="24" fill="black" opacity="0.4" rx="2" ry="2"/>
<rect width="80" height="24" fill="white" rx="2" ry="2"/>
<text x="3" y="6">Tooltip</text>
</g>
<script type="text/ecmascript"><![CDATA[
(function() {
var svg = document.getElementById('tooltip-svg-5');
var tooltip = svg.getElementById('tooltip');
var tooltipText = tooltip.getElementsByTagName('text')[0].firstChild;
var triggers = svg.getElementsByClassName('tooltip-trigger');
for (var i = 0; i < triggers.length; i++) {
triggers[i].addEventListener('mousemove', showTooltip);
triggers[i].addEventListener('mouseout', hideTooltip);
}
function showTooltip(evt) {
var CTM = svg.getScreenCTM();
var x = (evt.clientX - CTM.e + 6) / CTM.a;
var y = (evt.clientY - CTM.f + 20) / CTM.d;
tooltip.setAttributeNS(null, "transform", "translate(" + x + " " + y + ")");
tooltip.setAttributeNS(null, "visibility", "visible");
tooltipText.data = evt.target.getAttributeNS(null, "data-tooltip-text");
}
function hideTooltip(evt) {
tooltip.setAttributeNS(null, "visibility", "hidden");
}
})()
]]></script>
When loaded, the tooltip does not appear
Your code is working. The problem you may have is that the tooltip appears mostly outside the svg element. I've added overflow:visible; to the svg element and now you can see the tooltip.
(function() {
var svg = document.getElementById('tooltip-svg-5');
var tooltip = svg.getElementById('tooltip');
var tooltipText = tooltip.getElementsByTagName('text')[0].firstChild;
var triggers = svg.getElementsByClassName('tooltip-trigger');
for (var i = 0; i < triggers.length; i++) {
triggers[i].addEventListener('mousemove', showTooltip);
triggers[i].addEventListener('mouseout', hideTooltip);
}
function showTooltip(evt) {
var CTM = svg.getScreenCTM();
var x = (evt.clientX - CTM.e + 6) / CTM.a;
var y = (evt.clientY - CTM.f + 20) / CTM.d;
tooltip.setAttributeNS(null, "transform", "translate(" + x + " " + y + ")");
tooltip.setAttributeNS(null, "visibility", "visible");
tooltipText.data = evt.target.getAttributeNS(null, "data-tooltip-text");
}
function hideTooltip(evt) {
tooltip.setAttributeNS(null, "visibility", "hidden");
}
})()
svg{border:1px solid; overflow:visible;display:block;margin:auto}
<svg id="tooltip-svg-5" viewBox="0 -8 130 238" width="200">
<image class="tooltip-trigger" data-tooltip-text="Battles" id="Battles" x="100" y="200" width="30" height="30" xlink:href="https://s3-us-west-2.amazonaws.com/s.cdpn.io/222579/pin.png" />
<g id="tooltip" visibility="hidden">
<rect x="2" y="2" width="80" height="24" fill="black" opacity="0.4" rx="2" ry="2"/>
<rect width="80" height="24" fill="white" rx="2" ry="2"/>
<text x="3" y="6">Tooltip</text>
</g>
</svg>
Another option would have been adding text-anchor="end" to the text and rewriting the rects accordingly.
Yet another potion would have been using an HTML element for the tooltip.
I'm having some trouble updating (enter/update/exit) the fill color of several inline SVG Paths.
<svg>
<g id="section11">
<path id="section11p" d="m 431.78572,404.50506 0,36.875 20,-0.0893 22.14285,3.66072 8.92858,-38.48215 -7.5,-1.60714 z"/>
</g>
<g id="section10">
<path id="section10p" d="m 476.792,445.13425 8.83884,-38.38579 19.31917,6.43972 21.2132,11.23795 14.77348,9.21764 -21.97082,33.71384 -28.158,-16.66752 z"/>
</g>
</svg>
I used the code from this fiddle (http://jsfiddle.net/ybAj5/6/) to successfully (initially) color all of my inline SVG paths:
var colour = d3.scale.linear()
.domain([0, 20])
.range(["lightgreen", "darkgreen"]);
dataset1.forEach(function(d){ //d is of form [id,value]
d3.select("g#"+d[0]) //select the group matching the id
.datum(d) //attach this data for future reference
.selectAll("path, polygon")
.datum(d) //attach the data directly to *each* shape
.attr("fill", d?colour(d[1]):"lightgray");
});
And here is the dataset I've used:
var dataset1 = [["section10", 3],
["section11", 11],
["section13", 19]];
But I would like to incorporate these two datasets:
var dataset2 = [["section10", 0],
["section11", 11],
["section13", 15]];
var dataset3 = [["section10", 1],
["section11", 3],
["section13", 18]];
Using this dropdown menu:
<div class ="fixed"><select id = "opts">
<option value="dataset1" selected="selected">2012 Cohort</option>
<option value="dataset2">2013 Cohort</option>
<option value="dataset3">2014 Cohort</option>
</select></div>
I understand I need to encapsulate the process about within an enter/update/exit process with something like this:
function updateLegend(newData) {
// bind data
var appending = d3.selectAll('g')
.data(newData);
// add new elements
appending.enter().append('g');
// update existing elements
appending.transition()
.duration(0)
.style("fill", function(d,i){return colour(i);});
// remove old elements
appending.exit().remove();
}
// generate initial legend
updateLegend(ds2);
// handle on click event
d3.select('#opts')
.on('change', function() {
var newData = eval(d3.select(this).property('value'));
updateLegend(newData);
});
But its tough because I feel that all of the examples I've seen perform the update on SVGs which were created with d3, not custom SVGs created and pasted into the html page from Inkscape (an opensource SVG software).
I feel I'm very close to the solution and any help would be appreciated!
You don't need an "enter", "update" and "exit" selection for this. Just pass the value of dataset as an argument.
Also, you don't need to bind data. You just need this:
data.forEach(function(d){ //d is of form [id,value]
d3.select("#"+d[0]) //select the group matching the id
.attr("fill", colour(d[1]));
});
Check the demo:
var colour = d3.scale.linear()
.domain([0, 20])
.range(["lightgreen", "darkgreen"]);
var datasets = {dataset1: [["section10", 0],
["section11", 11]],
dataset2:[["section10", 1],
["section11", 2]],
dataset3:[["section10", 17],
["section11", 19]],
dataset4:[["section10", 2],
["section11", 19]]
};
d3.select('#opts').on('change', function() {
var value = d3.select(this).property('value');
var newData = datasets[value];
reColor(newData);
});
function reColor(data){
data.forEach(function(d){ //d is of form [id,value]
d3.select("#"+d[0]) //select the group matching the id
.attr("fill", colour(d[1]));
});
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div class ="fixed"><select id = "opts">
<option value="" selected="selected">Select</option>
<option value="dataset1">2013 Cohort</option>
<option value="dataset2">2014 Cohort</option>
<option value="dataset3">2015 Cohort</option>
<option value="dataset4">2016 Cohort</option>
</select></div>
<svg width="300" height="300">
<g id="section11">
<path id="section11p" d="m 31.78572,104.50506 0,36.875 20,-0.0893 22.14285,3.66072 8.92858,-38.48215 -7.5,-1.60714 z"/>
</g>
<g id="section10">
<path id="section10p" d="m 76.792,145.13425 8.83884,-38.38579 19.31917,6.43972 21.2132,11.23795 14.77348,9.21764 -21.97082,33.71384 -28.158,-16.66752 z"/>
</g>
</svg>
I have a choropleth map of counties, and a bar chart showing population of each county on the map. When I click on the bar chart the map is filtered and redrawn to show the selected bar, but when I click on the a county in the choropleth map the bar chart is not filtered to show the population data. I don't understand why it would be filtering one way but not the other. Any help is appreciated!
<div id="iowa-map">
<strong>Population by counties (color: total population)</strong>
<a class="reset" href="javascript:iowaMap.filterAll();dc.redrawAll();" style="display: none; ">reset</a>
<span class="reset" style="display: none;"> | Current filter: <span class="filter"></span></span>
<div class="clearfix"></div>
</div>
<div id="population-chart">
<strong>Population by county</strong>
<a class="reset" href="javascript:populationChart.filterAll();dc.redrawAll();" style="display: none; ">reset</a>
<span class="reset" style="display: none;"> | Current filter: <span class="filter"></span></span>
<div class="clearfix"></div>
</div>
<div class="clearfix"></div>
<div>
Reset All
</div>
var iowaMap = dc.geoChoroplethChart("#iowa-map");
var populationChart = dc.barChart("#population-chart");
d3.csv("iowaCountiesPop.csv", function (data) {
data.forEach(function (d) {
d.county = d.county;
d.popByCounty = +d.e2015;
});
var data = crossfilter(data);
var counties = data.dimension(function (d) {
return d.county;
});
var counties2 = data.dimension(function (d) {
return d.county;
});
var popByCounty = counties.group().reduceSum(function (d) {
return d.popByCounty;
});
d3.json("IowaCounties.json", function (countiesJson) {
iowaMap.width(990)
.height(500)
.dimension(counties)
.group(popByCounty)
.projection(d3.geo.mercator()
.translate([495, 250])
.rotate([93 + 20 / 60, -41 - 60 / 60])
.scale(7900))
.colors(d3.scale.quantile().range(colorScheme[quantiles]))
.colorDomain([0, 430640])
.overlayGeoJson(countiesJson.features, "NAME", function (d) {
return d.properties.NAME;
})
.title(function (d) {
return d.key + " County \nTotal Population: " + numberFormat(d.value);
})
.on('renderlet', function(map) {
map.selectAll("path").on("click", function(d) {
//console.log("click!", d)
map.filter(d.properties.NAME)
.redrawGroup();
})
});
populationChart.width(width)
.height(height)
.dimension(counties2)
.group(popByCounty)
.x(d3.scale.ordinal().domain(counties))
.xUnits(dc.units.ordinal)
.margins({top: 0, right: 0, bottom: 70, left: 70})
.yAxisLabel(["Population Values"])//,[12])
.xAxisLabel("County Names")
.barPadding(0.1)
.outerPadding(0.05)
.elasticY(false)
//.turnOnControls(true)
.on('renderlet', function(chart) {
chart.selectAll('rect').on("click", function(d) {
//console.log("click!", d)
chart.filter(d.data.key)
.redrawGroup();
})
chart.selectAll("g.x text")
.style("text-anchor", "end")
.attr("dx","-8")
.attr("dy", "5")
.attr("transform", "rotate(-50)");
});
dc.renderAll();
});
});
So what is the the Most Frequently Asked Question Ever for dc.js?
Why is my chart not filtering?
And what is the most frequent answer?
Your charts are on the same dimension, or more specifically, your second chart's group is observing the same dimension that the first chart is filtering. Which means that it will not see the changes, because groups do not observe their own dimensions.
It looks like you started to go in this direction, but only duplicated the dimension. Both charts are observing the same group, and since that group is produced from the dimension for the map, it will not observe filtering on the map.
iowaMap
.dimension(counties)
.group(popByCounty)
populationChart
.dimension(counties2)
.group(popByCounty)
Instead:
function pop_by_county(dim) {
return dim.group().reduceSum(function(d) {
return d.popByCounty;
});
}
var popByCounty = pop_by_county(counties),
popByCounty2 = pop_by_county(counties2);
populationChart
.dimension(counties2)
.group(popByCounty2)
https://jsfiddle.net/gordonwoodhull/28qsa0jr/7/
I am using D3 for the exercise. However, I am having trouble passing an object to the .style() method:
var myStyles = [
'#268BD2',
'#BD3613',
'#D11C24',
'#C61C6F',
'#595AB7',
'#2176C7'
];
This below piece of code is working
d3.selectAll('.item')
.data(myStyles)
.style('background',function(d){return d});
But none of the below two code pieces are working
d3.selectAll('.item')
.data(myStyles)
.style({'background':function(d){return d}});
d3.selectAll('.item')
.data(myStyles)
.style({'color':'white','background':function(d){return d}});
Please explain what is wrong here.
You can apply objects in styles and attributes by using d3-selection-multi.
First, you have to reference the mini library:
<script src="https://d3js.org/d3-selection-multi.v0.4.min.js"></script>
Then, you have to use styles, not style:
.styles({'color':'white','background':function(d){return d}});
You can see the code working in this fiddle, in which I'm using an object to set the styles: https://jsfiddle.net/gerardofurtado/o54rtrqc/1/
For attributes, use attrs instead of attr.
Here is the API.
I'm also working through the Lynda.com course. Here's an example of the "Binding data to the DOM" exercise using D3 version 4:
JSFiddle
HTML:
<!--
Added these two scripts:
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/d3-selection-multi.v0.4.min.js"></script>
-->
<div class="container">
<h2>D3 Graphic</h2>
<section id="chart">
<div class="item">Darth Vader</div>
<div class="item">Luke Skywalker</div>
<div class="item">Han Solo</div>
<div class="item">Obi-Wan Kenobi</div>
<div class="item">Chewbacca</div>
<div class="item">Boba Fett</div>
</section>
</div>
JS:
var myStyles = [{
width: 200,
color: '#A57706'
}, {
width: 300,
color: '#BD3613'
}, {
width: 150,
color: '#D11C24'
}, {
width: 350,
color: '#C61C6F'
}, {
width: 400,
color: '#595AB7'
}, {
width: 250,
color: '#2176C7'
}];
d3.selectAll('.item')
.data(myStyles)
.styles({
'color': 'white',
'background': function(d) {
return d.color;
},
width: function(d) {
return d.width + 'px';
}
})