Този скрипт не може да бъде инсталиран директно. Това е библиотека за други скриптове и може да бъде използвана с мета-директива // @require https://update.greasyfork.org/scripts/552715/1678839/shikiTestLIB.js
// ==UserScript==
// @name ShikiTreeLibwewqsdaxedwscxz
// @namespace https://shikimori.one/
// @version 1.0.0.0.0.0
// @description Lib для графиков
// @license MIT
// @require https://cdn.jsdelivr.net/npm/[email protected]/lodash.min.js
// ==/UserScript==
; (function () {
'use strict';
$.tools = $.tools || {version: '1.2.5'};
$.tools.tooltip = {
conf: {
// default effect variables
effect: 'toggle',
fadeOutSpeed: "fast",
predelay: 0,
delay: 30,
opacity: 1,
tip: 0,
// 'top', 'bottom', 'right', 'left', 'center'
position: ['top', 'center'],
offset: [0, 0, 0],
relative: false,
cancelDefault: true,
// type to event mapping
events: {
def: "mouseenter,mouseleave",
input: "focus,blur",
widget: "focus mouseenter,blur mouseleave",
tooltip: "mouseenter,mouseleave"
},
// 1.2
layout: '<div/>',
tipClass: 'tooltip',
ignoreSelector: ''
},
addEffect: function(name, loadFn, hideFn) {
effects[name] = [loadFn, hideFn];
}
};
var effects = {
toggle: [
function(done) {
var conf = this.getConf(), tip = this.getTip(), o = conf.opacity;
if (o < 1) { tip.css({opacity: o}); }
tip.show();
done.call();
},
function(done) {
this.getTip().hide();
done.call();
}
],
fade: [
function(done) {
var conf = this.getConf();
this.getTip().fadeTo(conf.fadeInSpeed, conf.opacity, done);
},
function(done) {
this.getTip().fadeOut(this.getConf().fadeOutSpeed, done);
}
]
};
function Tooltip(trigger, conf) {
var self = this,
fire = trigger.add(self),
tip,
timer = 0,
pretimer = 0,
title = trigger.data("do-not-use-title") ? null : trigger.attr("title"),
tipAttr = trigger.attr("data-tooltip"),
effect = effects[conf.effect],
shown,
// get show/hide configuration
isInput = trigger.is(":input"),
isWidget = isInput && trigger.is(":checkbox, :radio, select, :button, :submit"),
type = trigger.attr("type"),
evt = conf.events[type] || conf.events[isInput ? (isWidget ? 'widget' : 'input') : 'def'];
// check that configuration is sane
if (!effect) { throw "Nonexistent effect \"" + conf.effect + "\""; }
evt = evt.split(/,\s*/);
if (evt.length != 2) { throw "Tooltip: bad events configuration for " + type; }
// trigger --> show
trigger.on(evt[0], function(e) {
clearTimeout(pretimer);
var predelay = trigger.data('predelay') || conf.predelay;
if (predelay) {
pretimer = setTimeout(function() { self.show(e); }, predelay);
} else {
self.show(e);
}
// trigger --> hide
}).on(evt[1], function(e) {
clearTimeout(pretimer);
var delay = trigger.data('delay') || conf.delay;
if (delay) {
timer = setTimeout(function() { self.hide(e); }, delay);
} else {
self.hide(e);
}
});
if (conf.ignoreSelector) {
trigger
.on('mouseover', conf.ignoreSelector, function(e) {
// event with delay because this event is triggered before same event for 'trigger'
delay().then(function() {
clearTimeout(pretimer);
self.hide(e);
});
})
.on('mouseout', conf.ignoreSelector, function(e) {
// event without delay because this event must be triggered before same event for 'trigger'
if (e.target == trigger[0] || $(e.target).closest(trigger)) {
trigger.trigger('mouseover', [{target: trigger[0]}]);
}
}
)
}
// remove default title
if (title && conf.cancelDefault) {
trigger.removeAttr("title");
trigger.data("title", title);
}
$.extend(self, {
show: function(e) {
// для устройств с тачскрином и узких экранов тултипы отключаем
if (!e.target.classList.contains('mobile') && (
('ontouchstart' in window) ||
(navigator.MaxTouchPoints > 0) ||
(navigator.msMaxTouchPoints > 0) ||
isMobile())) {
return;
}
// tip not initialized yet
if (!tip) {
// data-tooltip
if (tipAttr) {
tip = $(tipAttr);
// single tip element for all
} else if (conf.tip) {
tip = $(conf.tip).eq(0);
// remote tooltip
} else if ((trigger.data('tooltip_url') || trigger.data('href') || trigger.attr('href')) && !trigger.data('local-tooltip')) {
tip = $(conf.defaultTemplate)
.addClass(conf.tipClass)
.css('z-index', parseInt(trigger.parents('.tooltip').css('z-index')) || 1)
.hide();
if (trigger.data('insert-tooltip-after')) {
tip.insertAfter(trigger);
} else {
tip.appendTo(document.body);
}
// why delay?
// delay(100).then(function() {
e = e || $.Event();
e.type = "onBeforeFetch";
fire.trigger(e, []);
const tooltip_url = trigger.data('tooltip_url') ||
trigger.data('href') ||
trigger.attr('href').replace(/(\?|$)/, '/tooltip$1');
tip.find('.tooltip-details').load(tooltip_url, function() {
// если есть только картинка, то ставим класс tooltip-image
var $this = $(this);
var $desc = $this.find('.tooltip-desc');
if ($desc.length && $desc.html() == '' && $this.find('img').length) {
$this.parents('.tooltip').addClass('tooltip-image');
}
$this.process();
// после подгрузки надо тултип перересовать, если он видимый
delay(50).then(function() {
if (tip.css('display') == 'none') {
return;
}
var top = parseInt(tip.css('top'));
var left = parseInt(tip.css('left'));
var pos = getPosition(trigger, tip, conf, true);
if (Math.abs(top - pos.top) > 20 || Math.abs(left - pos.left) > 20) {
tip.stop(true, true).css({top: pos.top, left: pos.left});
}
})
});
// })
trigger.attr('data-remote', null);
trigger.attr('tooltip', null);
// autogenerated tooltip
//} else if (title) {
//tip = $(conf.layout).addClass(conf.tipClass).appendTo(document.body)
//.hide().append(title);
// manual tooltip
} else {
tip = trigger.next();
if (!tip.length) { tip = trigger.parent().next(); }
}
if (!tip.length) { throw "Cannot find tooltip for " + trigger; }
}
if (self.isShown()) { return self; }
// stop previous animation
tip.stop(true, true);
// get position
var pos = getPosition(trigger, tip, conf);
// restore title for single tooltip element
if (conf.tip) {
tip.html(trigger.data("title"));
}
// onBeforeShow
e = e || $.Event();
e.type = "onBeforeShow";
fire.trigger(e, [pos]);
//if (e.isDefaultPrevented()) { return self; }
// onBeforeShow may have altered the configuration
pos = getPosition(trigger, tip, conf);
// set position
tip.css({position:'absolute', top: pos.top, left: pos.left});
shown = true;
// invoke effect
effect[0].call(self, function() {
e.type = "onShow";
shown = 'full';
fire.trigger(e);
});
// tooltip events
var event = conf.events.tooltip.split(/,\s*/);
if (!tip.data("__set")) {
tip.on(event[0], function() {
clearTimeout(timer);
clearTimeout(pretimer);
});
if (event[1] && !trigger.is("input:not(:checkbox, :radio), textarea")) {
tip.on(event[1], function(e) {
// being moved to the trigger element
if (e.relatedTarget != trigger[0]) {
trigger.trigger(evt[1].split(" ")[0]);
}
});
}
tip.data("__set", true);
}
return self;
},
hide: function(e) {
if (!tip || !self.isShown()) { return self; }
// onBeforeHide
e = e || $.Event();
e.type = "onBeforeHide";
fire.trigger(e);
//if (e.isDefaultPrevented()) { return; }
shown = false;
effects[conf.effect][1].call(self, function() {
e.type = "onHide";
fire.trigger(e);
});
return self;
},
isShown: function(fully) {
return fully ? shown == 'full' : shown;
},
getConf: function() {
return conf;
},
getTip: function() {
return tip;
},
getTrigger: function() {
return trigger;
}
});
// callbacks
$.each("onBeforeFetch,onHide,onBeforeShow,onShow,onBeforeHide".split(","), function(i, name) {
// configuration
if ($.isFunction(conf[name])) {
$(self).on(name, conf[name]);
}
// API
self[name] = function(fn) {
if (fn) { $(self).on(name, fn); }
return self;
};
});
}
// --- ShikiMath class ---
/* eslint camelcase:0 */
class ShikiMath {
// detecting whether point is above or below a line
// x,y - point
// x1,y1 - point 1 of line
// x2,y2 - point 2 of line
static is_above(x, y, x1, y1, x2, y2) {
const dx = x2 - x1;
const dy = y2 - y1;
return ((((dy * x) - (dx * y)) + (dx * y1)) - (dy * x1)) <= 0;
}
// detecting in which "sector" point x2,y2 is located accordingly to
// rectangular node with center in x1,y1 and width=rx*2 and height=ry*2
static sector(x1, y1, x2, y2, rx, ry) {
// left_bottom to right_top
const lb_to_rt = this.is_above(x2, y2, x1 - rx, y1 - ry, x1, y1);
// left_top to right_bottom
const lt_to_rb = this.is_above(x2, y2, x1 - rx, y1 + ry, x1, y1);
if (lb_to_rt && lt_to_rb) {
return 'top';
} if (!lb_to_rt && lt_to_rb) {
return 'right';
} if (!lb_to_rt && !lt_to_rb) {
return 'bottom';
}
return 'left';
}
// math for obtaining coords for link between two rectangular nodes
// with center in xN,yN and width=rxN*2 and height=ryN*2
static square_cutted_line(x1, y1, x2, y2, rx1, ry1, rx2, ry2) {
let f_x1;
let f_x2;
let f_y1;
let f_y2;
const dx = x2 - x1;
const dy = y2 - y1;
const y = x => (((dy * x) + (dx * y1)) - (dy * x1)) / dx;
const x = y => (((dx * y) - (dx * y1)) + (dy * x1)) / dy;
const target_sector = this.sector(x1, y1, x2, y2, rx1, ry1);
if (target_sector === 'right') {
f_x1 = x1 + rx1;
f_y1 = y(f_x1);
f_x2 = x2 - rx2;
f_y2 = y(f_x2);
} else if (target_sector === 'left') {
f_x1 = x1 - rx1;
f_y1 = y(f_x1);
f_x2 = x2 + rx2;
f_y2 = y(f_x2);
}
if (target_sector === 'top') {
f_y1 = y1 + ry1;
f_x1 = x(f_y1);
f_y2 = y2 - ry2;
f_x2 = x(f_y2);
}
if (target_sector === 'bottom') {
f_y1 = y1 - ry1;
f_x1 = x(f_y1);
f_y2 = y2 + ry2;
f_x2 = x(f_y2);
}
return {
x1: f_x1,
y1: f_y1,
x2: f_x2,
y2: f_y2,
sector: target_sector
};
}
// tests for math
static rspec() {
// is_above
this._assert(true, this.is_above(-1, 2, -1, -1, 1, 1));
this._assert(true, this.is_above(0, 2, -1, -1, 1, 1));
this._assert(true, this.is_above(0, 0, -1, -1, 1, 1));
this._assert(true, this.is_above(1, 2, -1, -1, 1, 1));
this._assert(false, this.is_above(2, 1, -1, -1, 1, 1));
this._assert(false, this.is_above(-1, -2, -1, -1, 1, 1));
// sector test
this._assert('top', this.sector(0, 0, 0, 10, 1, 1));
this._assert('top', this.sector(0, 0, 10, 10, 1, 1));
this._assert('right', this.sector(0, 0, 10, 0, 1, 1));
this._assert('right', this.sector(0, 0, 10, -10, 1, 1));
this._assert('bottom', this.sector(0, 0, 0, -10, 1, 1));
this._assert('left', this.sector(0, 0, -10, 0, 1, 1));
// square_cutted_line
this._assert(
{ x1: -9, y1: 0, x2: 9, y2: 0, sector: 'right' },
this.square_cutted_line(-10, 0, 10, 0, 1, 1, 1, 1)
);
this._assert(
{ x1: 5, y1: 0, x2: -5, y2: 0, sector: 'left' },
this.square_cutted_line(10, 0, -10, 0, 5, 1, 5, 1)
);
this._assert(
{ x1: 0, y1: 5, x2: 0, y2: -5, sector: 'bottom' },
this.square_cutted_line(0, 10, 0, -10, 1, 5, 1, 5)
);
this._assert(
{ x1: 0, y1: -5, x2: 0, y2: 5, sector: 'top' },
this.square_cutted_line(0, -10, 0, 10, 1, 5, 1, 5)
);
this._assert(
{ x1: 5, y1: 5, x2: -5, y2: -5, sector: 'left' },
this.square_cutted_line(10, 10, -10, -10, 5, 5, 5, 5)
);
this._assert(
{ x1: 0.5, y1: 1, x2: 1.5, y2: 3, sector: 'top' },
this.square_cutted_line(0, 0, 2, 4, 1, 1, 1, 1)
);
}
static _assert(left, right) {
if (JSON.stringify(left) !== JSON.stringify(right)) {
throw `math error: expected ${JSON.stringify(left)}, got ${JSON.stringify(right)}`;
}
}
}
// --- FranchiseGraph class ---
const START_MARKERS = ['prequel'];
const END_MARKERS = ['sequel'];
class FranchiseGraph {
constructor(data) {
// image sizes
this._bound_x = this._bound_x.bind(this);
this._bound_y = this._bound_y.bind(this);
this._y_by_date = this._y_by_date.bind(this);
this._node_selected = this._node_selected.bind(this);
this._tick = this._tick.bind(this);
this._link_truncated = this._link_truncated.bind(this);
this._collide = this._collide.bind(this);
this.image_w = 64;
this.image_h = 64;
this.links_data = data.links;
this.nodes_data = data.nodes.map(node => (
new FranchiseNode(node, this.image_w, this.image_h, node.id === data.current_id)
));
this._prepare_data();
this._position_nodes();
this._prepare_force();
this._check_non_symmetrical_links();
}
_prepare_data() {
let original_size;
this.links_data.forEach(link => {
link.source = this.nodes_data.find(n => n.id === link.source_id);
link.target = this.nodes_data.find(n => n.id === link.target_id);
if (!link.source || !link.target) {
console.warn(`Link skipped: ${link.source_id} -> ${link.target_id}`);
}
});
this.max_weight = _.max(this.links_data.map(v => v.weight)) * 1.0;
this.size = (original_size = this.nodes_data.length);
// console.log "nodes: #{@size}, max_weight: #{@max_weight}"
// screen sizes
this.screen_width =
this.size < 30 ?
this._scale(this.size, {
from_min: 0,
from_max: 30,
to_min: 480,
to_max: 1300
}
) :
this._scale(this.size, {
from_min: 0,
from_max: 100,
to_min: 1600,
to_max: 2461
}
);
this.screen_height = this.screen_width;
// dates for positioning on Y axis
const min_date = _.max(this.nodes_data.map(v => v.date));
const max_date = _.max(this.nodes_data.map(v => v.date));
// do not use min/max dates if they belong to multiple entries
if (this.nodes_data.filter(v => v.date === min_date).length === 1) {
this.min_date = min_date * 1.0;
}
if (this.nodes_data.filter(v => v.date === max_date).length === 1) {
return this.max_date = max_date * 1.0;
}
}
// initial nodes positioning
_position_nodes() {
// return unless @min_date && @max_date
return this.nodes_data.forEach(d => {
d.y = this._y_by_date(d.date);
d.x = (this.screen_width / 2.0) - d.rx;
if (d.date === this.min_date) {
d.fixed = true;
// move it proportionally to its relations count
d.y += this._scale(d.weight, { from_min: 4, from_max: 20, to_min: 0, to_max: 700 });
}
if (d.date === this.max_date) {
d.fixed = true;
d.y -= 20;
// move it proportionally to its relations count
return d.y -= this._scale(d.weight, { from_min: 4, from_max: 9, to_min: 0, to_max: 150 });
}
});
}
// configure d3 force object
_prepare_force() {
return window.d3_force = (this.d3_force = d3.layout.force()
.charge(function (d) {
if (d.selected) {
if (d.weight > 100) {
return -9000;
} else {
return -5000;
}
} else if (d.weight > 100) {
return -7000;
} else if (d.weight > 20) {
return -4000;
} else if (d.weight > 7) {
return -3000;
} else {
return -2000;
}
}).friction(0.9)
.linkDistance(d => {
const max_width =
this.max_weight < 3 ?
this._scale(this.size, { from_min: 2, from_max: 6, to_min: 100, to_max: 300 }) :
this.max_weight > 100 ?
this._scale(this.max_weight, { from_min: 30, from_max: 80, to_min: 300, to_max: 1000 }) :
this._scale(this.max_weight, { from_min: 30, from_max: 80, to_min: 300, to_max: 1500 });
return this._scale(300 * (d.weight / this.max_weight), {
from_min: 0,
from_max: 300,
to_min: 150,
to_max: max_width
}
);
}).size([this.screen_width, this.screen_height])
.nodes(this.nodes_data)
.links(this.links_data));
}
_check_non_symmetrical_links() {
return this.links_data.forEach(entry_1 => {
const symmetrical_link = this.links_data
.find(entry_2 => (entry_2.source_id === entry_1.target_id) && (entry_2.target_id === entry_1.source_id));
if (!symmetrical_link) {
console.warn(`non symmetical link [${entry_1.source_id}, ${entry_1.target_id}]`, entry_1);
}
});
}
// scale X which expected to be in [from_min..from_max] to new value in [to_min...to_max]
_scale(x, opt) {
let percent = (x - opt.from_min) / (opt.from_max - opt.from_min);
percent = Math.min(1, Math.max(percent, 0));
return opt.to_min + ((opt.to_max - opt.to_min) * percent);
}
// bound X coord to be within screen area
_bound_x(d, x = d.x) {
const min = d.rx + 5;
const max = this.screen_width - d.rx - 5;
return Math.max(min, Math.min(max, x));
}
// bound Y coord to be within screen area
_bound_y(d, y = d.y) {
const min = d.ry + 5;
const max = this.screen_width - d.ry - 5;
return Math.max(min, Math.min(max, y));
}
// determine Y coord by date (oldest to top, newest to bottom)
_y_by_date(date) {
return this._scale(date, {
from_min: this.min_date,
from_max: this.max_date,
to_min: this.image_h / 2.0,
to_max: this.screen_height - (this.image_h / 2.0)
}
);
}
render_to(target) {
this._append_svg(target);
this._append_markers();
this._append_links();
this._append_nodes();
this.d3_force.start().on('tick', this._tick);
for (let i = 0, end = this.size * this.size, asc = end >= 0; asc ? i <= end : i >= end; asc ? i++ : i--) { this.d3_force.tick(); }
return this.d3_force.stop();
}
// handler for node selection
_node_selected(d) {
if (this.selected_node) {
this.selected_node.deselect(this._bound_x, this._bound_y, this._tick);
if (this.selected_node === d) {
this.selected_node = null;
return;
}
}
this.selected_node = d;
return this.selected_node.select(this._bound_x, this._bound_y, this._tick);
}
// svg tag
_append_svg(target) {
return this.d3_svg = d3.select(target)
.append('svg')
.attr({ width: this.screen_width, height: this.screen_height });
}
// lines between nodes
_append_links() {
return this.d3_link = this.d3_svg.append('svg:g').selectAll('.link')
.data(this.links_data)
.enter().append('svg:path')
.attr({
class(d) { return `${d.source_id}-${d.target_id} link ${d.relation}`; },
'marker-start'(d) {
if (START_MARKERS.find(v => v === d.relation)) { return `url(#${d.relation})`; }
},
'marker-end'(d) {
if (END_MARKERS.find(v => v === d.relation)) { return `url(#${d.relation})`; }
},
'marker-mid'(d) { return `url(#${d.relation}_label)`; }
});
}
// nodes (images + borders + year)
_append_nodes() {
this.d3_node = this.d3_svg.append('.svg:g').selectAll('.node')
.data(this.nodes_data)
.enter().append('svg:g')
.attr({
class: 'node',
id(d) { return d.id; }
}).call(this.d3_force.drag())
.on('click', d => {
if (d3.event != null ? d3.event.defaultPrevented : undefined) { return; }
return this._node_selected(d);
});
// .call(@d3_force.drag().on('dragstart', -> $(@).children('text').hide()).on('dragend', -> $(@).children('text').show()))
// .on 'mouseover', (d) ->
// $(@).children('text').show()
// .on 'mouseleave', (d) ->
// $(@).children('text').hide()
this.d3_node.append('svg:path').attr({ class: 'border_outer', d: '' });
this.d3_image_container = this.d3_node.append('svg:g').attr({ class: 'image-container' });
this.d3_image_container.append('svg:image')
.attr({
width(d) { return d.width; },
height(d) { return d.height; },
'xlink:href'(d) { return d.image_url; }
});
this.d3_image_container.append('svg:path')
.attr({
class: 'border_inner',
d(d) { return `M 0,0 ${d.width},0 ${d.width},${d.height} 0,${d.height} 0,0`; }
});
// year
this.d3_image_container.append('svg:text')
.attr({
x(d) { return d.yearX(); },
y(d) { return d.yearY(); },
class: 'year shadow'
}).text(d => d.year);
return this.d3_image_container.append('svg:text')
.attr({
x(d) { return d.yearX(); },
y(d) { return d.yearY(); },
class: 'year'
}).text(d => d.year);
}
// kind
// @d3_image_container.append('svg:text')
// .attr x: @image_w - 2, y: 0 , class: 'kind shadow'
// .text (d) -> d.kind
// @d3_image_container.append('svg:text')
// .attr x: @image_w - 2, y: 0, class: 'kind'
// .text (d) -> d.kind
// markers for links between nodes
_append_markers() {
this.d3_defs = this.d3_svg.append('svg:defs');
// arrow size
const aw = 8;
this.d3_defs.append('svg:marker')
.attr({
id: 'sequel',
orient: 'auto',
refX: aw,
refY: aw / 2,
markerWidth: aw,
markerHeight: aw,
stroke: '#123',
fill: '#333'
}).append('svg:polyline')
.attr({ points: `0,0 ${aw},${aw / 2} 0,${aw} ${aw / 4},${aw / 2} 0,0` });
return this.d3_defs.append('svg:marker')
.attr({
id: 'prequel',
orient: 'auto',
refX: 0,
refY: aw / 2,
markerWidth: aw,
markerHeight: aw,
stroke: '#123',
fill: '#333'
}).append('svg:polyline')
.attr({ points: `${aw},${aw} 0,${aw / 2} ${aw},0 ${(aw * 3) / 4},${aw / 2} ${aw},${aw}` });
}
// @d3_svg.append('svg:defs').selectAll('marker')
// .data(['sequel', 'prequel'])
// .enter().append('svg:marker')
// .attr
// refX: 10, refY: 0
// id: String,
// markerWidth: 6, markerHeight: 6, orient: 'auto'
// stroke: '#123', fill: '#123'
// viewBox: '0 -5 10 10'
// .append('svg:path')
// .attr
// d: (d) ->
// if START_MARKERS.find(d)
// "M10,-5L0,0L10,5"
// else
// "M0,-5L10,0L0,5"
// move nodes and links accordingly to coords calculated by d3.force
_tick() {
this.d3_node.attr({
transform: d => `translate(${this._bound_x(d) - d.rx}, ${this._bound_y(d) - d.ry})`
});
this.d3_link.attr({
d: this._link_truncated
});
// collistion detection between nodes
return this.d3_node.forEach(this._collide(0.5));
}
// math for obtaining coords for links between rectangular nodes
_link_truncated(d) {
if (!d.source || !d.target) return ''; // защита от undefined
// if (!location.href.endsWith('?test')) {
// if (d.source.id >= d.target.id) { return ''; }
// }
const rx1 = d.source.rx;
const ry1 = d.source.ry;
const rx2 = d.target.rx;
const ry2 = d.target.ry;
const x1 = this._bound_x(d.source);
const y1 = this._bound_y(d.source);
const x2 = this._bound_x(d.target);
const y2 = this._bound_y(d.target);
const coords = ShikiMath.square_cutted_line(x1, y1, x2, y2, rx1, ry1, rx2, ry2);
if (!Number.isNaN(coords.x1) && !Number.isNaN(coords.y1) &&
!Number.isNaN(coords.x2) && !Number.isNaN(coords.y2)) {
return `M${coords.x1},${coords.y1} L${coords.x2},${coords.y2}`;
} else {
return `M${x1},${y1} L${x2},${y2}`;
}
}
// math for collision detection. originally it was designed for circle
// nodes so it is not absolutely accurate for rectangular nodes
_collide(alpha) {
const quadtree = d3.geom.quadtree(this.nodes_data);
return d => {
const nx1 = d.x - d.width;
const nx2 = d.x + d.width;
const ny1 = d.y - d.height;
const ny2 = d.y + d.height;
return quadtree.visit((quad, x1, y1, x2, y2) => {
if (quad.point && (quad.point !== d)) {
const rb = Math.max(d.rx + quad.point.rx, d.ry + quad.point.ry) * 1.15;
let x = d.x - quad.point.x;
let y = d.y - quad.point.y;
let l = Math.sqrt((x * x) + (y * y));
if ((l < rb) && (l !== 0)) {
l = ((l - rb) / l) * alpha;
x *= l;
y *= l;
d.x = this._bound_x(d, d.x - x);
d.y = this._bound_y(d, d.y - y);
quad.point.x = this._bound_x(quad.point, quad.point.x + x);
quad.point.y = this._bound_y(quad.point, quad.point.y + y);
}
}
return (x1 > nx2) || (x2 < nx1) || (y1 > ny2) || (y2 < ny1);
});
};
}
}
const SELECT_SCALE = 2;
const BORDER_OFFSET = 3;
class FranchiseNode {
constructor(data, width, height, isCurrent) {
this.width = width;
this.height = height;
this.isCurrent = isCurrent;
Object.assign(this, data);
this.selected = false;
this.fixed = false;
if (this.isCurrent) {
this.width = Math.ceil(this.width * 1.3);
this.height = Math.ceil(this.height * 1.3);
}
this.initialWidth = this.width;
this.initialHeight = this.height;
this._calcRs();
this._tooltip = null;
}
get d3Node() {
if (!this._d3Node) this._d3Node = d3.select($(`.node#${this.id}`)[0]);
return this._d3Node;
}
get d3ImageContainer() {
if (!this._d3ImageContainer) this._d3ImageContainer = this.d3Node.selectAll('.image-container');
return this._d3ImageContainer;
}
get d3Image() {
if (!this._d3Image) this._d3Image = this.d3Node.selectAll('image');
return this._d3Image;
}
get d3Year() {
if (!this._d3Year) this._d3Year = this.d3Node.selectAll('.year');
return this._d3Year;
}
get d3OuterBorder() {
if (!this._d3OuterBorder) this._d3OuterBorder = this.d3Node.selectAll('path.border_outer');
return this._d3OuterBorder;
}
get d3InnerBorder() {
if (!this._d3InnerBorder) this._d3InnerBorder = this.d3Node.selectAll('path.border_inner');
return this._d3InnerBorder;
}
deselect(boundX, boundY, tick) {
this.selected = false;
this.fixed = this.pfixed;
this._hideTooltip();
this._animate(this.initialWidth, this.initialHeight, boundX, boundY, tick);
}
select(boundX, boundY, tick) {
this.selected = true;
this.pfixed = this.fixed;
this.fixed = true;
this._loadTooltip();
this._animate(
this.initialWidth * SELECT_SCALE,
this.initialHeight * SELECT_SCALE,
boundX,
boundY,
tick
);
}
_initTooltip() {
if (!this._tooltip) {
const trigger = $(this.d3Node.node());
const conf = {
effect: 'fade',
tipClass: 'sticky-tooltip',
defaultTemplate: '<div class="sticky-tooltip"><div class="tooltip-details"></div></div>'
};
this._tooltip = new Tooltip(trigger, conf);
}
}
yearX(w = this.width) {
return w - 2;
}
yearY(h = this.height) {
return h - 2;
}
_calcRs() {
this.rx = this.width / 2.0;
return this.ry = this.height / 2.0;
}
_animate(newWidth, newHeight, boundX, boundY, tick) {
let ih;
let io;
let iw;
if (this.selected) {
io = d3.interpolate(0, BORDER_OFFSET);
iw = d3.interpolate(this.width, newWidth);
ih = d3.interpolate(this.height, newHeight);
this.d3Node.attr({ class: 'node selected' });
} else {
io = d3.interpolate(BORDER_OFFSET, 0);
iw = d3.interpolate(this.width - (BORDER_OFFSET * 2), newWidth);
ih = d3.interpolate(this.height - (BORDER_OFFSET * 2), newHeight);
this.d3Node.attr({ class: 'node' });
}
return this.d3Node
.transition()
.duration(500)
.tween('animation', () => t => {
// t = 1
const o = io(t);
const o2 = o * 2;
const w = iw(t);
const h = ih(t);
const widthIncrement = (w + o2) - this.width;
const heightIncrement = (h + o2) - this.height;
this.width += widthIncrement;
this.height += heightIncrement;
this._calcRs();
const outerBorderPath = `M 0,0 ${w + o2},0 ${w + o2},${h + o2} 0,${h + o2} 0,0`;
this.d3Node.attr({
transform: `translate(${boundX(this) - this.rx}, ${boundY(this) - this.ry})`
});
this.d3OuterBorder.attr({ d: outerBorderPath });
this.d3ImageContainer.attr({ transform: `translate(${o}, ${o})` });
this.d3InnerBorder.attr({ d: `M 0,0 ${w},0 ${w},${h} 0,${h} 0,0` });
this.d3Image.attr({ width: w, height: h });
this.d3Year.attr({ x: this.yearX(w), y: this.yearY(h) });
return tick();
});
}
_hideTooltip() {
if (this._tooltip) {
this._tooltip.hide();
}
}
async _loadTooltip() {
this._initTooltip();
const tip = this._tooltip.getTip();
if (!tip) return;
tip.addClass('b-ajax');
this._tooltip.show();
try {
const { data } = await axios.get(`https://shikimori.one/api/comments/${this.id}/tooltip`);
tip.removeClass('b-ajax');
tip.find('.tooltip-details').html(data.html_body);
} catch (err) {
console.error('Tooltip:', err);
}
}
}
window.ShikiMath = ShikiMath;
window.FranchiseGraph = FranchiseGraph;
window.FranchiseNode = FranchiseNode;
})();