Last active
September 26, 2019 12:32
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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