First, please excuse my poor english.
I'm working on a project with a grid-map and an external csv.
The grid-map is not composed of rectangles or hexagons but only with the centroid of any kind of symbol that will be use at the end.
So I have a Topojson file with centroids "ID" and centroids "Coordinates".
The external CSV is composed of several columns, the first one with the same centroids "ID" and the other one with value for different year.
"ID","C2001","C2002","C2003","C2004","C2005","C2006","C2007","C2008","C2009","C2010","C2000"
6050,"-5.55753","-5.55914","-5.75444","-5.76307","-5.81660","-5.99361","-6.02150","-6.15979","-5.73530","-6.30509","-5.52990"
6051,"-5.55753","-5.55914","-5.75444","-5.76307","-5.81660","-5.99361","-6.02150","-6.15979","-5.73530","-6.30509","-5.52990"
Here is my code
var width = 960,
height = 600;
var options = [
{date: "2000", selected: "+d.C2000"},
{date: "2001", selected: "+d.C2001"},
{date: "2002", selected: "+d.C2002"},
{date: "2003", selected: "+d.C2003"},
{date: "2004", selected: "+d.C2004"},
{date: "2005", selected: "+d.C2005"},
{date: "2006", selected: "+d.C2006"},
{date: "2007", selected: "+d.C2007"},
{date: "2008", selected: "+d.C2008"},
{date: "2009", selected: "+d.C2009"},
{date: "2010", selected: "+d.C2010"},
];
var color = d3.scale.threshold()
.domain([-1985, -1400, -1000, -700, -300, -100, -25, -0])
.range(["#7f0000", "#b30000", "#d7301f", "#ef6548", "#fc8d59", "#fdbb84", "#fdd49e", "#fee8c8", "#fff7ec"]);
var path = d3.geo.path()
.projection(null)
.pointRadius(1.5);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
queue()
.defer(d3.json, "CO2_light.json")
.defer(d3.csv, "tdd_CO2_emissions.csv")
.await(ready);
function ready(error, centroid, CO2) {
var rateById = {};
console.log(rateById); //To
//CO2.forEach(function(d) { rateById[+d.ID] = +d.C2000; }); WORKING FINE = value in rateById
CO2.forEach(function(d) { rateById[+d.ID] = options[0].selected; }); //NOT WORKING = inside rateById "+d.C2000" instead the value
svg.selectAll("path")
.data(topojson.feature(centroid, centroid.objects.CENTROID).features)
.enter().append("path")
.attr("class", "centerGrid")
.attr("d", path)
.style("fill", function(d) { return color(rateById[+d.properties.ID]); });
d3.select(".loading").remove();
For the moment I leave the button for my next problem and I'm focus on this two line below
//CO2.forEach(function(d) { rateById[+d.ID] = +d.C2000; }); WORKING FINE = value in rateById
CO2.forEach(function(d) { rateById[+d.ID] = options[0].selected; }); //NOT WORKING = inside rateById "+d.C2000" instead the value
If I use the first line I get a nice grid-map (see image) but if I'm trying to access at value of a specific year from the options array with the second line and do console.log(rateById); I get this
Object
6050: "+d.C2000"
6051: "+d.C2000"
6712: "+d.C2000"
Instead of this
Object
6050: -6.30509
6051: -6.30509
6712: -7.0441
Fixed here: http://jsfiddle.net/z7sLdyu2/2/
2 changes to do in your code:
First, your options select should not contain javascript code to be executed (+d.), but only the year values:
var options = [
{date: "2000", selected: "C2000"},
{date: "2001", selected: "C2001"},
{date: "2002", selected: "C2002"},
{date: "2003", selected: "C2003"},
{date: "2004", selected: "C2004"},
{date: "2005", selected: "C2005"},
{date: "2006", selected: "C2006"},
{date: "2007", selected: "C2007"},
{date: "2008", selected: "C2008"},
{date: "2009", selected: "C2009"},
{date: "2010", selected: "C2010"}
];
Then in the loop, assign the rateById value by accessing the data d property for the selected year like this:
rateById[+d.ID] = +d[options[0].selected];
P.S.: I had to remove part of your code in your jsfiddle, as the centroids json was not found in your version of the jsfiddle, leading to errors not related to the problem of this question.
Related
I have an object (not an array):
var people = {};
people['Zanny'] = {date: 447, last: 'Smith'};
people['Nancy'] = {date: 947, last: 'William'};
people['Jen'] = {date: 147, last: 'Roberts'};
Unlike this generic sorting question I need to specify which sub-key's value to sort by, in this case: date. For the sake of simplicity I've removed the first seven digits of the dates as that is not important.
After the sorting the object should be sorted by the values of the date key as follows:
people['Jen'] = {date: 147, last: 'Roberts'};//147 is the lowest/first value.
people['Zanny'] = {date: 447, last: 'Smith'};
people['Nancy'] = {date: 947, last: 'William'};//947 is the highest/last value.
I need to create a reusable non-anonymous named function with two obvious parameters: the object and the key whose value the sorting is based on. I've been spending some time trying experimenting with the following without any success yet:
function array_sort_by_values(o, k)
{
return Object.entries(o).sort().reduce(function (result, key)
{
//console.log(result,key);
result[key][k] = o[key][k]; return result;
}, {});
}
I can not use o.sort as that is for array types (even though JavaScript claims that everything is an object and that "arrays" do not exist).
I can not overemphasize that I must be able to specify the name of the key as I need to use this form of sorting for a few different things. What I am missing here?
Absolutely no frameworks or libraries.
How about convert object to array to sort and convert back?
var people = {};
people['Zanny'] = {date: 447, last: 'Smith'};
people['Nancy'] = {date: 947, last: 'William'};
people['Jen'] = {date: 147, last: 'Roberts'};
console.log(Object.fromEntries(Object.entries(people).sort((a,b) => {
return a[1].date - b[1].date
})))
var people = {};
people['Zanny'] = {date: 447, last: 'Smith'};
people['Nancy'] = {date: 947, last: 'William'};
people['Jen'] = {date: 147, last: 'Roberts'};
console.log(Object.fromEntries(Object.entries(people).sort(([,a],[,b]) => {
return a.date - b.date
})))
As a named function:
function array_sort_by_values(o, k)
{
return Object.fromEntries(Object.entries(o).sort((a,b) => {return a[1][k] - b[1][k]}));
}
To keep the changes in effect the object has to be reassigned:
people = array_sort_by_values(people, 'date');
console.log(people);
array_sort_by_values(people, 'last')
console.log(people);
I have implemented AMCharts JS library in one of my web apps and am having an issue. Right now i need to add multiple y-axis values to one single x-axis point (which is actually a date). I have 5 values 5,1,5,4,1,3 which contain same x-axis point (same date) but when i observe the graph sometimes 4 is missing while sometimes 5 as shown in the image. Am i really doing something wrong? This is the form of data
{date: "2016-03-29", value: 5}
{date: "2016-03-29", value: 1}
{date: "2016-03-29", value: 5}
{date: "2016-03-29", value: 4}
{date: "2016-03-29", value: 1}
{date: "2016-03-29", value: 3}
{date: "2016-10-20", value: 0}
{date: "2016-10-20", value: 0}
{date: "2016-10-20", value: 0}
There are a couple of issues with your setup.
First, your data is structured incorrectly for a chart with parseDates enabled. You can't have multiple elements of the same date. AmCharts requires that you group your data points if they share the same date/category. This is by design and every single example on the website shows this, so all of your 2016-03-29 and 2016-10-20 points can be reduced into two objects. I'm presuming that those multiple values correspond to different graph objects, so you need unique value fields for each, i.e.
[{
date: "2016-03-29",
value1: 5,
value2: 1,
value3: 5,
value4: 4,
value5: 1,
value6: 3
}, {
date: "2016-10-20"
value1: 0,
value2: 0,
value3: 0
}]
And then you have 6 graph objects with valueFields set to value1 through value6 to accommodate for those points. Again, I'm assuming that you have multiple graphs given your dataset.
Edit
Your comment indicates that this is not the case and they all belong to one graph. You still need to restructure your data and give unique timestamps since you can't have multiple entries with the same timestamp for a parseDates-enabled chart. If those values came in at different times, then provide that information. For example, assuming your data is hourly:
{date: "2016-03-29 01:00", value: 5}
{date: "2016-03-29 02:00", value: 1}
{date: "2016-03-29 03:00", value: 5}
{date: "2016-03-29 04:00", value: 4}
{date: "2016-03-29 05:00", value: 1}
{date: "2016-03-29 06:00", value: 3}
{date: "2016-10-20 01:00", value: 0}
{date: "2016-10-20 02:00", value: 0}
{date: "2016-10-20 03:00", value: 0}
This will work but you have to set your dataDateFormat to match the timestamp (in this case "YYYY-MM-DD JJ:NN") and adjust your minPeriod to accommodate for the smallest interval in between your points (for this example: "hh").
If you don't parse dates, then you can keep your setup, but the chart will look weird with multiple 2016-03-29 entries. Those are your only options.
As for displaying multiple value axes, a value axis needs to be associated with a graph. If you need multiple value axes, then you need multiple graphs. Each graph's valueAxis property needs to be assigned to a valueAxis object or valueAxis id. You can't assign multiple value axes to one graph object. You can see how this works in this example.
If you only have one graph and need to display a second value axis, create a duplicate graph instance and disable visual aspects of it, like so:
valueAxes: [{
"position": "left",
"id": "v1"
}, {
"position": "right",
"id": "v2"
}],
graphs: [{
//initial graph
type: "line",
bullet: "round",
valueField: "value"
}, {
//invisible duplicate graph
//for second axis
lineAlpha: 0,
showBalloons: false,
visibleInLegend: false,
valueAxis: "v2",
valueField: "value"
}],
Demo
I am building a stacked bar chart using d3 v4.4.4 in react.js v15.3.0. Below is the data I am using to build my stacked bar chart:
[
{ timestamp: "2006", source1: "20", source2: "20", source3: "20", source4: '20'},
{ timestamp: "2007", source1: "70", source2: "50", source3: "10", source4: '70'},
{ timestamp: "2008", source1: "80", source2: "50", source3: "60", source4: '40'},
{ timestamp: "2009", source1: "30", source2: "20", source3: "40", source4: '50'},
{ timestamp: "2010", source1: "70", source2: "20", source3: "90", source4: '20'}
]
I have gotten the stacked bar chart to show up seen here, but it is not showing the first object in the array of data. Instead the stacked bar chart duplicates the last object in the array of data and uses for both the first and last stacked bar in the chart.
const stack = d3.stack().keys(keys)
const layers = stack(chartDataWorkingCopy)
this was the result of logging layers to the console
As you can see from the screenshot above the first and last arrays are the same, and the first object (i.e. { timestamp: "2006", source1: "20", source2: "20", source3: "20", source4: '20'}) from the original data I used to build the chart is not there.
Can someone explain to me why d3 is skipping over the first object in the array and duplicating the last when building the stacked bar chart?
Below is the source code for the stacked bar chart component:
import React, { PropTypes } from 'react'
import * as d3 from 'd3'
import _ from 'lodash'
class StackedBarChart extends React.Component {
static propTypes = {
chartData: PropTypes.array,
barWidth: PropTypes.number,
barOffset: PropTypes.number,
chartHeight: PropTypes.number,
chartWidth: PropTypes.number
}
static defaultProps = {
chartData: [
{ timestamp: "2006", source1: "20", source2: "20", source3: "20", source4: '20'},
{ timestamp: "2007", source1: "70", source2: "50", source3: "10", source4: '70'},
{ timestamp: "2008", source1: "80", source2: "50", source3: "60", source4: '40'},
{ timestamp: "2009", source1: "30", source2: "20", source3: "40", source4: '50'},
{ timestamp: "2010", source1: "70", source2: "20", source3: "90", source4: '20'}
],
barWidth: 10,
barOffset: 5,
chartHeight: 200,
chartWidth: 300
}
componentDidMount() {
this.renderChart()
}
componentDidUpdate(prevProps){
if(!_.isEqual(this.props, prevProps)){
d3.select('svg').remove()
this.renderChart()
}
}
splitTheDifference(data) {
let str = String(data)
const points = str.split(',')
return (points[1] - points[0])
}
renderChart() {
const { chartData, barWidth, barOffset, chartHeight, chartWidth} = this.props
let chartDataWorkingCopy = [...chartData]
// keep the bars from going off the page
const maxBars = (chartWidth / (barWidth + 2))
if (chartDataWorkingCopy.length > maxBars) {
chartDataWorkingCopy = chartDataWorkingCopy.slice(-Math.floor(maxBars))
}
const keys = _.remove(_.keys(_.extend.apply({}, chartDataWorkingCopy)), (d) => {
return d !== 'timestamp'
})
const xScale = d3.scaleBand().range([0, chartWidth]).padding(0.1)
const yScale = d3.scaleLinear().range([chartHeight, 0])
const color = ['hsl(8, 82%, 50%)', 'hsl(76, 96%, 50%)', 'hsl(178, 99%, 50%)', 'hsl(302, 100%, 50%)', 'hsl(58, 98%, 50%)', 'hsl(144, 100%, 50%)']
const xAxis = d3.axisBottom(xScale).tickFormat(d3.timeFormat("%b"))
const yAxis = d3.axisLeft(yScale)
const width = chartWidth
const height = chartHeight
let svg = d3.select('.barChartContainer').append("svg")
.attr('width', width)
.attr('height', height)
.append('g')
const stack = d3.stack().keys(keys)
const layers = stack(chartDataWorkingCopy)
xScale.domain(chartDataWorkingCopy.map(function(d) { return d.timestamp }))
yScale.domain([0, d3.max(layers[layers.length - 1], function(d) {
return d[0] + d[1]
}) ]).nice()
const layer = svg.selectAll('.layer')
.data(layers)
.enter().append('g')
.attr('class', 'layer')
.style('fill', function(d, i) { return color[i] })
layer.selectAll("rect")
.data(function(d) {
return d
})
.enter().append("rect")
.attr("x", function(d, i) {
return i * 12
})
.attr("y", function(d) { return yScale(d[1]) })
.attr("height", function(d) { return yScale(d[0]) - yScale(d[1]) })
.attr("width", 10)
.on('mouseover', (d) => {
d3.select('#tooltip')
.classed('hidden', false)
.style('position', 'absolute')
.style('background', '#333333')
.style('color', '#fff')
.style('padding', 10)
.style('left', `${d3.event.pageX}px`)
.style('top', `${d3.event.pageY - 80}px`)
.select('#value')
.text(this.splitTheDifference(d))
})
.on('mouseout', (d) => {
d3.select('#tooltip')
.classed('hidden', true)
})
}
render(){
return(
<div className={ 'barChartContainer' }>
<div id='tooltip' className='hidden'>
<p id='value'></p>
</div>
</div>
)
}
}
export default StackedBarChart
Well, d3.stack() is not skipping the first object in your data array.
The problem is that this code...
const keys = _.remove(_.keys(_.extend.apply({}, chartDataWorkingCopy)), (d) => {
return d !== 'timestamp'
})
... is not only getting the keys, but also modifying your chartDataWorkingCopy array, which still points to your chartData array.
Right now, you may think that you're cloning your original array here:
let chartDataWorkingCopy = [...chartData];
But, unfortunately, you're not. You cannot clone an array of objects with the spread operator. The documentation says:
Typically the spread operators in ES2015 goes one level deep while copying an array. Therefore, they are unsuitable for copying multidimensional arrays. It's the same case with Object.assign() and Object spread operators. Look at the example below for a better understanding.
So, in your code, any change to chartDataWorkingCopy also changes chartData.
Solution 01
You can get the keys without modifying the array, using vanilla JS or D3, you don't need lodash for this. For instance, this is a very simple way to get the keys you want using only D3 and without modifying the source array:
const keys = d3.keys(chartDataWorkingCopy[0]).filter(d => d != "timestamp");
Solution 02
If you want to keep your lodash function, use a method that really copies a deep array, like this:
let chartDataWorkingCopy = JSON.parse(JSON.stringify(chartData));
In this demo, I'm keeping your code (with lodash), just changing the code that makes a copy of the original array. Now the values are correct:
var chartData = [
{ timestamp: "2006", source1: "20", source2: "20", source3: "20", source4: '20'},
{ timestamp: "2007", source1: "70", source2: "50", source3: "10", source4: '70'},
{ timestamp: "2008", source1: "80", source2: "50", source3: "60", source4: '40'},
{ timestamp: "2009", source1: "30", source2: "20", source3: "40", source4: '50'},
{ timestamp: "2010", source1: "70", source2: "20", source3: "90", source4: '20'}
];
let chartDataWorkingCopy = JSON.parse(JSON.stringify(chartData));
const keys = _.remove(_.keys(_.extend.apply({}, chartDataWorkingCopy)), (d) => {
return d !== 'timestamp'
})
const stack = d3.stack().keys(keys)
const layers = stack(chartData);
console.log(layers);
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
PS: Copying an array with JSON.parse(JSON.stringify(array)) only works if you don't have actual date objects in that array. Right now, "2006", "2007", "2008" and so on are only strings, so this method works.
Sorry, maybe it's stupid question but i'm still learning.
I'm trying to make one simple line chart using D3.js in angularJS. everything working fine and in that i am using a custom directive to plot the chart using D3. I am adding the chart data in the controller itself by using $scope.initially it will plot but i will change the data in one button click but the chart is not updating automatically,
after reading angularjs documentation i understand by using $scope we can able to change the UI content dynamically.whether it is possible here or any other scenario is present in angularJs
I am a beginner in AngularJS
<div data-ng-app="chartApp" data-ng-controller="SalesController">
<h1>Today's Sales Chart</h1>
<div linear-chart chart-data="salesData"></div>
<button type="button" data-ng-click="Change()">Click Me!</button>
</div>
My JS code
var app = angular.module('chartApp', []);
app.controller('SalesController', ['$scope', function($scope){
$scope.salesData=[
{hour: 1,sales: 54},
{hour: 2,sales: 66},
{hour: 3,sales: 77},
{hour: 4,sales: 70},
{hour: 5,sales: 60},
{hour: 6,sales: 63},
{hour: 7,sales: 55},
{hour: 8,sales: 47},
{hour: 9,sales: 55},
{hour: 10,sales: 30}
];
$scope.Change = function () {
//here i am changing the data so i need to replot the chart
$scope.salesData=[
{hour: 1,sales: 14},
{hour: 2,sales: 16},
{hour: 3,sales: 77},
{hour: 4,sales: 10},
{hour: 5,sales: 60},
{hour: 6,sales: 63},
{hour: 7,sales: 55},
{hour: 8,sales: 47},
{hour: 9,sales: 55},
{hour: 10,sales: 30}
];
}
}]);
//creating one custom directive to plot the chart
app.directive('linearChart', function($window){
return{
restrict:'EA',
template:"<svg width='850' height='200'></svg>",
link: function(scope, elem, attrs){
var salesDataToPlot=scope[attrs.chartData];
//if u gave like this u can remove attributr chart-data
// var salesDataToPlot=scope.salesData
var padding = 20;
var pathClass="path";
var xScale, yScale, xAxisGen, yAxisGen, lineFun;
var d3 = $window.d3;
var rawSvg=elem.find('svg');
var svg = d3.select(rawSvg[0]);
function setChartParameters(){
xScale = d3.scale.linear()
.domain([salesDataToPlot[0].hour, salesDataToPlot[salesDataToPlot.length-1].hour])
.range([padding + 5, rawSvg.attr("width") - padding]);
yScale = d3.scale.linear()
.domain([0, d3.max(salesDataToPlot, function (d) {
return d.sales;
})])
.range([rawSvg.attr("height") - padding, 0]);
xAxisGen = d3.svg.axis()
.scale(xScale)
.orient("bottom")
.ticks(salesDataToPlot.length - 1);
yAxisGen = d3.svg.axis()
.scale(yScale)
.orient("left")
.ticks(5);
lineFun = d3.svg.line()
.x(function (d) {
return xScale(d.hour);
})
.y(function (d) {
return yScale(d.sales);
})
.interpolate("basis");
}
function drawLineChart() {
setChartParameters();
svg.append("svg:g")
.attr("class", "x axis")
.attr("transform", "translate(0,180)")
.call(xAxisGen);
svg.append("svg:g")
.attr("class", "y axis")
.attr("transform", "translate(20,0)")
.call(yAxisGen);
svg.append("svg:path")
.attr({
d: lineFun(salesDataToPlot),
"stroke": "blue",
"stroke-width": 2,
"fill": "none",
"class": pathClass
});
}
drawLineChart();
}
};
});
Fiddle
Here you go a live example:
http://jsfiddle.net/7dc3efrc/1/
scope.$watch(attrs.chartData, function(newValue) {
if (newValue) {
svg.selectAll("*").remove();
drawLineChart(newValue);
}
}, true);
Use $watch method of scope to trigger update on model change. Also, don't forget to clear previous graph if you don't need it (svg.selectAll("*").remove()).
Please, see full example as I've made some minor modifications to your salesDataToPlot variable (it is now a function parameter).
You can use $watch on scope to listen to salesData data changes and rebuild your chart when it's done.
//...
scope.$watch('salesData', function (newSalesData){
// drawLineChart();
// redraw logic
});
//...
Suppose I have an array of values with corresponding dates, [{date: d1, value: v1}, ..., {date: dn, value: vn}], that I'd like to visualize using d3.js. As long as subsequent measurements are within a certain time range, for example not more than a week apart, I am happy with d3 interpolating between the measurements.
However, when subsequent records are farther apart, I don't want d3 to connect them. What would be the easiest way to achieve this?
Your question is not exactly clear: by "interpolate", I believe you mean "connecting the dots".
If you use a time scale for your x axis, D3 will automatically connect the dots for you and create a line (an SVG path element), regardless the time separation in the data points. But there is a way for making "gaps" in that line: using line.defined(). According to the API:
If defined is specified, sets the defined accessor to the specified function or boolean and returns this line generator.
The problem is, for this approach to work, you'll have to set a given value (let's say, null) in your dataset, between the dates in which you don't want to draw the line (that is, the dates that are not close enough, as you say in your question). You can do this manually or using a function.
This is a working demo: in my dataset, my data jumps from 7-Oct to 17-Oct (more than 1 week). So, I just created a null value in any date between these two (in my demo, 16-Oct). Then, in the line generator, the line jumps this null value, using defined:
d3.line().defined(function(d){return d.value != null;})
The result is that the line jumps from 7-Oct to 17-Oct:
var data = [{date: "1-Oct-16",value: 14},
{date: "2-Oct-16",value: 33},
{date: "3-Oct-16",value: 12},
{date: "4-Oct-16",value: 43},
{date: "5-Oct-16",value: 54},
{date: "6-Oct-16",value: 71},
{date: "7-Oct-16",value: 32},
{date: "16-Oct-16",value: null},
{date: "17-Oct-16",value: 54},
{date: "18-Oct-16",value: 14},
{date: "19-Oct-16",value: 34},
{date: "20-Oct-16",value: 32},
{date: "21-Oct-16",value: 56},
{date: "22-Oct-16",value: 24},
{date: "23-Oct-16",value: 42},
{date: "24-Oct-16",value: 52},
{date: "25-Oct-16",value: 66},
{date: "26-Oct-16",value: 34},
{date: "27-Oct-16",value: 62},
{date: "28-Oct-16",value: 48},
{date: "29-Oct-16",value: 51},
{date: "30-Oct-16",value: 42}];
var parseDate = d3.timeParse("%d-%b-%y");
data.forEach(function (d) {
d.date = parseDate(d.date);
});
var svg = d3.select("body")
.append("svg")
.attr("width", 500)
.attr("height", 200);
var xScale = d3.scaleTime().range([20, 480]);
var yScale = d3.scaleLinear().range([180, 10]);
xScale.domain(d3.extent(data, function (d) {
return d.date;
}));
yScale.domain([0, d3.max(data, function (d) {
return d.value;
})]);
var xAxis = d3.axisBottom(xScale).tickFormat(d3.timeFormat("%d"));
var yAxis = d3.axisLeft(yScale);
var baseline = d3.line()
.defined(function(d){return d.value != null;})
.x(function (d) {
return xScale(d.date);
})
.y(function (d) {
return yScale(d.value);
});
svg.append("path") // Add the valueline path.
.attr("d", baseline(data))
.attr("fill", "none")
.attr("stroke", "teal");
svg.append("g")
.attr("transform", "translate(0,180)")
.call(xAxis);
svg.append("g")
.attr("transform", "translate(20,0)")
.attr("class", "y axis")
.call(yAxis);
<script src="https://d3js.org/d3.v4.min.js"></script>
If you're trying to draw a broken line chart, then I don't think interpolation is what you're looking for. Building a custom SVG path is probably the way to go. See an example here:
https://bl.ocks.org/joeldouglass/1a0b2e855d2bedc24c63e396b04c8e36