|
d3.json('https://gist.githubusercontent.com/susielu/3d194b8660ec6ab214a3/raw/farmers-markets.json', (error, data) => { |
|
const variables = [ |
|
{ "key": "vegetables", "label": "Vegetables 96%", "percent": .96}, |
|
{ "key": "bakedgoods", "label": "Baked Goods 88%", "percent": .88}, |
|
{ "key": "honey", "label": "Honey 81%", "percent": .81}, |
|
{ "key": "jams", "label": "Jams 80%", "percent": .80}, |
|
{ "key": "fruits", "label": "Fruits 80%", "percent": .80}, |
|
{ "key": "herbs", "label": "Herbs 79%", "percent": .79}, |
|
{ "key": "eggs", "label": "Eggs 74%", "percent": .74}, |
|
{ "key": "flower", "label": "Flowers 69%", "percent": .69}, |
|
{ "key": "soap", "label": "Soap 67%", "percent": .67 }, |
|
{ "key": "plants", "label": "Plants 66%", "percent": .66}, |
|
{ "key": "crafts", "label": "Crafts 61%", "percent": .61}, |
|
{ "key": "prepared", "label": "Prepared Food 61%", "percent": .61}, |
|
{ "key": "meat", "label": "Meat 55%", "percent": .55}, |
|
{ "key": "cheese", "label": "Cheese 50%", "percent": .50}, |
|
{ "key": "poultry", "label": "Poultry 45%", "percent": .45}, |
|
{ "key": "coffee", "label": "Coffee 33%", "percent": .33}, |
|
{ "key": "maple", "label": "Maple 32%", "percent": .32}, |
|
{ "key": "nuts", "label": "Nuts 29%", "percent": .29}, |
|
{ "key": "trees", "label": "Trees 29%", "percent": .29}, |
|
{ "key": "seafood", "label": "Seafood 24%", "percent": .24}, |
|
{ "key": "juices", "label": "Juices 22%", "percent": .22}, |
|
{ "key": "mushrooms", "label": "Mushrooms 22%", "percent": .22}, |
|
{ "key": "petfood", "label": "Pet Food 18%", "percent": .18}, |
|
{ "key": "wine", "label": "Wine 17%", "percent": .17}, |
|
{ "key": "beans", "label": "Beans 14%", "percent": .14}, |
|
{ "key": "grains", "label": "Grains 14%", "percent": .14}, |
|
{ "key": "wildharvest", "label": "Wild Harvest 13%", "percent": .13}, |
|
{ "key": "nursery", "label": "Nursery 6%", "percent": .06}, |
|
{ "key": "tofu", "label": "Tofu 4%", "percent": .04} |
|
] |
|
|
|
const features = [] |
|
const clusters = 5 |
|
|
|
data.forEach(d => { |
|
const f = []; |
|
variables.forEach(k => { |
|
if (d[k.key] === "Y") { |
|
f.push(1) |
|
} else { |
|
f.push(0) |
|
} |
|
}) |
|
|
|
features.push(f) |
|
}) |
|
|
|
|
|
const km = new kMeans({ |
|
K: clusters |
|
}) |
|
|
|
const cluster = () => { |
|
km.cluster(features); |
|
|
|
while (km.step()) { |
|
km.findClosestCentroids(); |
|
km.moveCentroids(); |
|
|
|
let hasConverged; |
|
try{ |
|
hasConverged = km.hasConverged() |
|
if(hasConverged) break; |
|
} |
|
catch (e) { |
|
console.log('error', e) |
|
} |
|
} |
|
|
|
console.log(km.centroids, km.clusters); |
|
|
|
variables.forEach((v, i) => { |
|
v.clusterValues = [] |
|
|
|
km.centroids.forEach((c, j) => { |
|
v.clusterValues.push({ |
|
value: c[i], |
|
cluster: j |
|
}) |
|
}) |
|
|
|
v.clusterValues.forEach(c => { |
|
c.minDiff = Math.min(...v.clusterValues.map( m => c.value !== m.value ? Math.abs(m.value - c.value) : 1 )) |
|
}) |
|
|
|
}) |
|
|
|
} |
|
|
|
cluster() |
|
|
|
const svg = d3.select('svg') |
|
|
|
const w = 29 |
|
const wPadding = 65 |
|
const h = 130 |
|
const yOffset = 220 |
|
const y = d3.scaleLinear().range([0 + yOffset, h + yOffset]).domain([1, 0]) |
|
|
|
const colors = d3.scaleOrdinal() |
|
.domain(d3.range(0, clusters, 1)) |
|
.range(['rgb(30, 0, 255)', '#0960ff', '#00ffc4','#2bff1d', '#a4e800', 'rgb(0, 92, 255)', 'rgb(255, 0, 92)', 'rgb(255, 92, 0)', 'orange']) |
|
|
|
const getOffset = (d, i, j) => { |
|
const minDiff = variables[j].clusterValues[i].minDiff |
|
return y(d + minDiff) - y(d) |
|
} |
|
|
|
const drawArea = (d, i) => { |
|
const line = d3.area() |
|
.x((d, i) => i*w + wPadding ) |
|
.y1((d, j) => y(d) + getOffset(d, i, j)) |
|
.y0((d, j) => y(d) - getOffset(d, i, j)) |
|
.curve(d3.curveCardinal) |
|
|
|
return line(d) |
|
} |
|
|
|
const drawLine = d3.line() |
|
.x((d, i) => i*w + wPadding ) |
|
.y(d => y(d)) |
|
.curve(d3.curveCardinal) |
|
|
|
variables.forEach((v, i) => { |
|
const variable = svg.append('g') |
|
.attr('class', 'variable') |
|
|
|
const xOffset = i*w + wPadding |
|
|
|
variable.append('line') |
|
.attr('x1', xOffset) |
|
.attr('x2', xOffset) |
|
.attr('y1', y(0)) |
|
.attr('y2', y(1)) |
|
|
|
variable.append('text') |
|
.attr('x', xOffset) |
|
.attr('y', h + yOffset + 50) |
|
.attr('transform', `rotate(45, ${xOffset - 15}, ${h + yOffset + 50})`) |
|
.text(v.label) |
|
|
|
}) |
|
|
|
svg.append('g') |
|
.attr('class', 'connectors') |
|
|
|
const colorLegend = d3.legendColor() |
|
.orient('horizontal') |
|
.shapeWidth(30) |
|
.scale(colors) |
|
|
|
svg.append('g') |
|
.attr('class', 'legend') |
|
.attr('transform', 'translate(300, 120)') |
|
.call(colorLegend) |
|
|
|
const legendScale = d3.scaleLinear() |
|
.domain([0, d3.max(km.clusters, d => d.length)]) |
|
.range([0, 60]) |
|
|
|
|
|
const t = d3.transition() |
|
.ease(d3.easeLinear) |
|
|
|
const recluster = () => { |
|
cluster() |
|
|
|
const connectorsArea = svg.select('g.connectors').selectAll('path.area') |
|
.data(km.centroids) |
|
|
|
const connectorsLine = svg.select('g.connectors').selectAll('path.line') |
|
.data(km.centroids) |
|
|
|
const hoverPath = () => { |
|
|
|
} |
|
|
|
connectorsArea.enter() |
|
.append('path') |
|
.attr('class','area') |
|
.on('hover', hoverPath) |
|
.merge(connectorsArea) |
|
.attr('stroke', (d,i) => colors(i)) |
|
.attr('fill', (d,i) => colors(i)) |
|
.transition() |
|
.attr('d', (d, i) => drawArea(d, i)) |
|
|
|
connectorsArea.exit() |
|
.remove() |
|
|
|
connectorsLine.enter() |
|
.append('path') |
|
.attr('class', 'line') |
|
.on('hover', hoverPath) |
|
.merge(connectorsLine) |
|
.attr('stroke', (d,i) => d3.color(colors(i)).darker()) |
|
.transition() |
|
.attr('d', (d, i) => drawLine(d, i)) |
|
|
|
connectorsLine.exit() |
|
.remove() |
|
|
|
svg.selectAll('g.variable') |
|
.each( () => { |
|
|
|
legendScale |
|
.domain([0, d3.max(km.clusters, d => d.length)]) |
|
|
|
svg.selectAll('.cell .swatch') |
|
.data(km.clusters) |
|
.transition(t) |
|
.attr('height', d => legendScale(d.length)) |
|
.attr('y', d => -legendScale(d.length) + 15) |
|
|
|
svg.select('.legendCells') |
|
.selectAll('text.count') |
|
.data(km.clusters) |
|
.enter() |
|
.append('text') |
|
.attr('class', 'count') |
|
|
|
svg.selectAll('text.count') |
|
.text(d => d.length) |
|
.transition(t) |
|
.attr('x', (d, i) => i*32 + 15) |
|
.attr('y', d => -legendScale(d.length) + 10) |
|
|
|
}) |
|
} |
|
|
|
|
|
recluster() |
|
// interactions |
|
const buttons = svg.append('g') |
|
.attr('class', 'buttons') |
|
.attr('transform', 'translate(30, 130)') |
|
|
|
buttons.append('rect') |
|
.attr('width', 100) |
|
.attr('height', 30) |
|
.on('click', recluster) |
|
|
|
buttons.append('text') |
|
.text('Recluster') |
|
.attr('x', 50) |
|
.attr('y', 20) |
|
|
|
}) |