Skip to content

Instantly share code, notes, and snippets.

@LordJohn42
Last active September 26, 2019 12:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LordJohn42/6d420b37a519462b31277c71fbc6103c to your computer and use it in GitHub Desktop.
Save LordJohn42/6d420b37a519462b31277c71fbc6103c to your computer and use it in GitHub Desktop.
Vue component for d3 hexagonal view. (Выглядит примерно так https://www.d3-graph-gallery.com/img/graph/hexbinmap_geo_basic.png)
<template>
<div>
<div class="row">
<div class="col-sm-12">
<div class="content-header">Цветок</div>
</div>
</div>
<section id="grid-option">
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="card-block">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<div class="card-block">
<div class="card-text text-center">
<div id="flowerElem" class="flower">
<svg v-if="states.flowerEl"></svg>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!--Modal-->
<b-modal id="selectRefModal" @ok="addRefToFlower" ref="showSelectRefModal" title="Вставить в цветок"
cancel-title="Отмена" ok-title="Вставить" :ok-disabled="!refSelected">
<b-form-select v-model="refSelected" :options="refOptions" class="mb-3">
<template slot="first">
<option :value="null" disabled>{{ refOptions.length > 0 ? '-- Выберите человека для вставки --' : '-- Нет людей для вставки --' }}</option>
</template>
</b-form-select>
</b-modal>
</div>
</template>
<script>
import * as d3 from 'd3'
import _ from 'lodash'
import {Flower} from '../classes/API'
import Auth from "../classes/API/Auth"
export default {
name: 'Flower',
data() {
return {
refOptions: [],
refSelected: '',
currentXY: {},
states: {flowerEl: true},
flowerData: []
}
},
mounted() {
this.init()
},
methods: {
init(reload) {
let self = this
if (reload) {
self.states.flowerEl = false
setTimeout(function () {
self.states.flowerEl = true
return self.init()
}, 500)
}
Flower.getFlowerView(this, {x: 0, y: 0}).then(response => {
self.flowerData = response.data
self.initFlower(self.flowerData)
})
},
addRefToFlower() {
Flower.flowerSetReferral(this, {
userId: this.refSelected.id,
x: this.currentXY.x,
y: this.currentXY.y
}).then(response => {
if (response.status === 200) {
this.init(true)
} else {
alert('Прозошла ошибка, попробуйте немного позже')
}
})
},
showSelectRefModal(elem) {
this.currentXY = elem
Flower.getFlowerToPlacement(this).then(response => {
let refOption = []
_.forEach(response.data, function (value) {
refOption.push({value: value, text: value.name + ' (' + value.nickname + ')'})
})
this.refOptions = refOption
this.refSelected = null
this.$root.$emit('bv::show::modal', 'selectRefModal')
})
},
initFlower(pages) {
// index
// Pan & Zooming
let svg = d3.select("svg")
.call(d3.zoom().on("zoom", function () {
svg.attr("transform", d3.event.transform + "translate(100, 750), rotate(-90)")
}).scaleExtent([1, 2]))
svg = d3.select("svg")
.append('g')
.attr('id', 'svgMain')
.attr("transform", "translate(100, 750) rotate(-90)")
let tooltip = d3.select("body")
.append("div")
.attr("class", "tooltipFlower")
.style("opacity", 0)
let margin = {
top: 10,
right: 10,
bottom: 10,
left: 10
}
let height = 1000 - margin.left - margin.right
let width = 1000 - margin.top - margin.bottom
let self = this
const
w = 60,
h = 45,
radius = 55,
radiusCos = radius * Math.cos(Math.PI / 6);
let
userCoordinates = {x: 0, y: 0},
center = userCoordinates,
parityCenter = Math.abs(center.x % 2),
r = radius * 0.45,
cos60 = Math.cos(Math.PI / 6),
sin60 = Math.sin(Math.PI / 6),
hexData = [
{y: r, x: 0},
{y: r * sin60, x: r * cos60},
{y: -r * sin60, x: r * cos60},
{y: -r, x: 0},
{y: -r * sin60, x: -r * cos60},
{y: r * sin60, x: -r * cos60},
{y: r, x: 0}
];
let hexGroup = svg.selectAll("g")
.data(laps(calcCoordinates(fillMesh(mergePages(pages), center))))
.enter()
.append("g")
.attr("transform", function (d) {
return "translate(" + d._x + "," + d._y + "), rotate(90)";
});
let drawHexagon = d3.line()
.x(function (d) {
return d.x;
})
.y(function (d) {
return d.y;
});
hexGroup
.append('path')
.attr("d", drawHexagon(hexData))
.each(function (d) {
switch (d.type) {
case 'empty':
d3.select(this)
.style("stroke-dasharray", "4,2")
.style("opacity", .3)
.style("stroke", "lightblue")
.style("stroke-width", "2")
.style("stroke-linejoin", "round")
.style("fill", "white");
break;
case 'append':
d3.select(this)
.style("stroke-dasharray", "4,2")
.style("stroke", "#8fc5b7")
.style("stroke-width", "2")
.style("stroke-linejoin", "round")
.style("fill", "white")
.on('mouseover', function () {
d3.select(this)
.attr("cursor", "pointer")
.transition()
.ease(d3.easeElastic)
.duration(1000)
.attr("transform", function () {
return "scale(1.2)";
});
})
.on('mousemove', function () {
})
.on('mouseout', function () {
tooltip.style('visibility', 'hidden');
d3.select(this)
.transition()
.ease(d3.easeElastic)
.duration(1000)
.attr("transform", function () {
return "scale(1)";
});
})
.on('click', function (d) {
self.showSelectRefModal(d)
});
break;
case 'exist':
d3.select(this)
.style("stroke", function (d) {
if (d.id === Auth.getUserRow('uid', true)) {
return '#1f6e6a'
}
// color lap by current user qualification
if (d.qLap === 1) {
return '#c3786e'
} else if (d.qLap === 2 || d.qLap === 3) {
return '#e1a76c'
} else if (d.qLap >= 4 && d.qLap <= 6) {
return '#c38588'
} else if (d.qLap >= 7 && d.qLap <= 10) {
return '#7aaf77'
} else if (d.qLap >= 11 && d.qLap <= 15) {
return '#a773a0'
} else if (d.qLap >= 16 && d.qLap <= 20) {
return '#7172a1'
} else if (d.qLap >= 21 && d.qLap <= 25) {
return '#6a5692'
} else if (d.qLap >= 26 && d.qLap <= 30) {
return '#006961'
} else {
return '#84b9b5'
}
})
.style("stroke-width", "2")
.style("stroke-linejoin", "round")
.attr("class", function (d) {
return 'filled' + ' ' + d.qualification
})
.style("fill", function (d) {
if (d.id === Auth.getUserRow('uid', true)) {
return '#4ca398'
}
// fill lap by current user qualification
if (d.qLap === 1) {
return '#f5aeb1'
} else if (d.qLap === 2 || d.qLap === 3) {
return '#ffe4a2'
} else if (d.qLap >= 4 && d.qLap <= 6) {
return '#dcb495'
} else if (d.qLap >= 7 && d.qLap <= 10) {
return '#81af7e'
} else if (d.qLap >= 11 && d.qLap <= 15) {
return '#a7819d'
} else if (d.qLap >= 16 && d.qLap <= 20) {
return '#7c78a1'
} else if (d.qLap >= 21 && d.qLap <= 25) {
return '#746592'
} else if (d.qLap >= 26 && d.qLap <= 30) {
return '#1b6966'
} else {
return '#81d8cd'
}
})
.style("opacity", function (d) {
if (_.isUndefined(d.qLap) && d.id !== Auth.getUserRow('uid', true)) {
return .7
}
})
.on('mouseover', function () {
tooltip.style('visibility', 'visible');
d3.select(this)
.attr("cursor", "pointer")
.transition()
.duration(200)
.style("opacity", .8);
})
.on('mousemove', showTooltip)
.on('mouseout', function () {
tooltip.style('visibility', 'hidden');
// opacity for elems
d3.select(this)
.transition()
.duration(200)
.style("opacity", function (d) {
if (_.isUndefined(d.qLap) && d.id !== Auth.getUserRow('uid', true)) {
return .7
}
return 1
});
});
break;
}
});
// Text on Hex
hexGroup.append("text")
.each(function (d) {
switch (d.type) {
case 'empty':
break;
case 'append':
d3.select(this)
.attr("text-anchor", "middle")
.attr("y", 7)
.attr("id", function (d, i) {
return "append" + i;
})
.attr("font-size", "20px")
.attr("font-family", "Arial")
.attr("fill", "#8fc5b7")
.attr("pointer-events", "none")
.text("+");
break;
case 'exist':
d3.select(this)
.attr("text-anchor", "middle")
.attr("y", 6)
.attr("id", function (d, i) {
return "exist" + i;
})
.attr("font-size", "18px")
.attr("letter-spacing", "2px")
.attr("font-family", "Arial")
.attr("fill", "#FFF")
.attr("pointer-events", "none")
.text(function (d) {
let initials = d.name.split(' ');
let fName = initials[0][0] || '';
if (initials[1]) {
let lName = initials[1][0] || '';
return (fName + lName).toUpperCase()
}
return fName.toUpperCase();
});
break;
}
});
// Show tooltip
function showTooltip(d) {
let textBox = 'Имя: <b>' + d.name + '</b></b><br>'
+ 'Групповой объем: <b>' + self.$options.filters.round(d.teamVolume, 2) + ' (Б)</b><br>'
+ 'Личный объем: <b>' + self.$options.filters.round(d.personalVolume, 2) + ' (Б)</b><br>'
+ 'Квалификация: <b>' + d.qualification + '</b><br>';
tooltip.style('visibility', 'visible');
tooltip.transition()
.style("opacity", .85);
tooltip
.html(textBox)
.style("left", (d3.event.pageX + 5) + "px")
.style("top", (d3.event.pageY - 36) + "px");
}
// draw
function createEmptyMesh(centerPos, halfCountByWidth, halfCountByHeight) {
let emptyMesh = [];
for (let i = 0; i <= 2 * halfCountByHeight; i++) {
for (let j = 0; j <= 2 * halfCountByWidth; j++) {
emptyMesh.push({
x: centerPos.x - halfCountByHeight + i,
y: centerPos.y - halfCountByWidth + j,
type: 'empty'
});
}
}
return emptyMesh;
}
function fillMesh(elems, centerPos) {
function findAndReplace(_mesh, _elem) {
let index = _.findIndex(_mesh, {x: _elem.x, y: _elem.y});
if (index !== -1 && (_elem.type === 'exist' || (_elem.type === 'append' && _mesh[index].type === 'empty'))) {
_mesh[index] = _elem;
}
}
const halfCountByWidth = Math.round(width / 1.5 / radius * 3 / 2);
const halfCountByHeight = Math.round(height / 1.5 / radiusCos * 3 / 2);
let mesh = createEmptyMesh(centerPos, halfCountByWidth, halfCountByHeight);
_.forEach(_.filter(
elems,
function (d) {
return d.x <= centerPos.x + halfCountByWidth &&
d.x >= centerPos.x - halfCountByWidth &&
d.y <= centerPos.y + halfCountByHeight &&
d.y >= centerPos.y - halfCountByHeight;
}
), function (elem) {
elem.type = 'exist';
findAndReplace(mesh, elem);
_.forEach(['left', 'right', 'left-up', 'right-up', 'left-down', 'right-down'], function (direction) {
findAndReplace(mesh, stepPos({x: elem.x, y: elem.y, type: 'append'}, direction));
});
});
return mesh;
}
function findLap(center, radius) {
let lap = [{x: center.x, y: center.y - radius}]
_.forEach(['right-up', 'right', 'right-down', 'left-down', 'left', 'left-up'], function (direction) {
for (let i = 1; i <= radius; i++) {
lap.push(stepPos(_.last(lap), direction))
}
})
lap.pop()
return lap
}
function laps(mesh) {
let elemOnLaps = [],
elemObj, centerUser = _.find(mesh, function (d) {
return d.id === Auth.getUserRow('uid', true)
})
_.forEach(calcLapsByQualification(_.find(mesh, centerUser).qualification), function (lap) {
_.map(findLap(centerUser, lap), function (obj) {
elemObj = _.find(mesh, obj)
elemObj.qLap = lap
elemOnLaps.push(elemObj)
})
})
return mesh
}
function calcLapsByQualification(q) {
let lastLap, res = []
switch (q) {
case 'Q0':
lastLap = 0
break;
case 'Q1':
lastLap = 1
break;
case 'Q2':
lastLap = 3
break;
case 'Q3':
lastLap = 6
break;
case 'Q4':
lastLap = 10
break;
case 'Q5':
lastLap = 15
break;
case 'Q6':
lastLap = 20
break;
case 'Q7':
lastLap = 25
break;
case 'Q8':
lastLap = 30
break;
}
for (let i = 0; i <= lastLap; i++) {
res.push(i)
}
return res
}
function calcCoordinates(mesh) {
return _.flatten(_.values(_.mapValues(
_.groupBy(
mesh,
'x'
),
function (rowElems, rowXCoordinate) {
let x = (width / 2) - (center.x - rowXCoordinate) * radiusCos,
res;
if (Math.abs(rowXCoordinate % 2) === parityCenter) {
res = _.map(rowElems, function (d) {
d._x = x;
d._y = (height / 2) - (center.y - d.y) * radius;
return d;
});
} else if (parityCenter === 1) {
res = _.map(rowElems, function (d) {
d._x = x;
d._y = (height / 2) - (center.y - d.y) * radius + radius / 2
return d;
});
} else {
res = _.map(rowElems, function (d) {
d._x = x;
d._y = (height / 2) - (center.y - d.y) * radius - radius / 2
return d;
});
}
return res;
})));
}
// pos-support
function pageWithPos(x, y) {
return {x: w * (x / w), y: h * (y / h)};
}
function pagesForPos(x, y) {
let xyPagePos = pageWithPos(x, y),
res;
if (x % w < w / 2. && y % h < h / 2.) {
res = [{x: xyPagePos.x, y: xyPagePos.y},
{x: xyPagePos.x - w, y: xyPagePos.y},
{x: xyPagePos.x, y: xyPagePos.y - h},
{x: xyPagePos.x - w, y: xyPagePos.y - h}];
} else if (x % w > w / 2. && y % h < h / 2.) {
res = [{x: xyPagePos.x, y: xyPagePos.y},
{x: xyPagePos.x + w, y: xyPagePos.y},
{x: xyPagePos.x, y: xyPagePos.y - h},
{x: xyPagePos.x + w, y: xyPagePos.y - h}];
} else {
res = [{x: 0, y: 0}];
}
return res;
}
function pagesForPosWithout(x, y, needlessPages) {
return _.filter(pagesForPos(x, y), function (page) {
return _.isUndefined(_.find(needlessPages, function (needlessPage) {
return needlessPage.x === page.x && needlessPage.y === page.y;
}));
});
}
function mergePages(pages) {
return _.reduce(pages, function (acc, page) {
return acc.concat(page.elems);
}, []);
}
function updatePages(pages, newPages) {
let notAffectedPages = _.filter(pages, function (page) {
return _.isUndefined(_.find(newPages, function (newPage) {
return newPage.x === page.x && newPage.y === page.y;
}));
});
return notAffectedPages.concat(newPages);
}
function stepPos(elem, direction) {
const parity = Math.abs(elem.x % 2);
let res = {};
switch (direction) {
case 'left' :
res = {
x: elem.x,
y: elem.y - 1
};
break;
case 'right' :
res = {
x: elem.x,
y: elem.y + 1
};
break;
case 'left-up' :
res = {
x: elem.x + 1,
y: (parity) ? elem.y - 1 : elem.y
};
break;
case 'left-down' :
res = {
x: elem.x - 1,
y: (parity) ? elem.y - 1 : elem.y
};
break;
case 'right-up' :
res = {
x: elem.x + 1,
y: (parity) ? elem.y : elem.y + 1
};
break;
case 'right-down' :
res = {
x: elem.x - 1,
y: (parity) ? elem.y : elem.y + 1
};
break;
}
if (elem.type) {
res.type = elem.type;
}
return res;
}
}
}
}
</script>
<style scoped>
.card .card-block {
padding: 0;
}
.flower svg {
width: 100%;
height: 650px;
}
</style>
<style>
.tooltipFlower {
text-align: center;
position: absolute;
font: 14px "Helvetica Neue", Helvetica, sans-serif;
z-index: 99999999;
border-radius: 8px;
pointer-events: none;
background-color: #ffffff;
padding: 3px 12px;
border: 1px solid #bbbbbb;
box-shadow: 1px 1px 4px #bbbbbb;
}
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment