viewof graphic = {
const total_mins = 24 * 60;
const width = 630, height = 800;
const circleRadius = 280;
const parseTime = d3.timeParse("%H:%M:%S");
const parseDate = d3.timeParse("%Y-%m-%d");
const data = sun_time_data.map((d, i) => {
const monthIndex = parseDate(d.date).getMonth();
const dayIndex = parseDate(d.date).getDate();
const weekdayIndex = parseDate(d.date).getDay();
const s = parseTime(d.sunrise), e = parseTime(d.sunset);
const sM = s.getHours() * 60 + s.getMinutes();
const eM = e.getHours() * 60 + e.getMinutes();
const s_sec = sM * 60 + s.getSeconds();
const e_sec = eM * 60 + e.getSeconds();
const sec_len = e_sec - s_sec;
const hours_len = Math.floor(sec_len / 3600);
const minutes_len = Math.floor((sec_len % 3600) / 60);
const seconds_len = sec_len % 60;
return {
i, monthIndex, dayIndex, weekdayIndex, dayLength: eM - sM,
hours_len, minutes_len, seconds_len
};
});
console.log(data)
const rScale = d3.scaleLinear()
.domain([0, total_mins])
.range([0, circleRadius]);
const angleScale = d3.scaleLinear()
.domain([0, data.length + 12 + 2])
.range([0, 2 * Math.PI]);
// create a location where the circle would center around
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height])
.style("background", "black");
const g = svg.append("g")
.attr("transform", `translate(${width/2},${height/2})`);
svg.append("defs")
// add a 'marker' inside 'defs', giving it an id 'arrowhead',
// to reference later
.append("marker")
.attr("id", "arrowhead")
// the 0-2, 0-2 coords in which the arrow will be drawn
.attr("viewBox", "0 0 2 2")
// the coords will be tied to the center of our
// 0-2 coordinate system
.attr("refX", 1)
.attr("refY", 1)
// when rendering, scale to 6x6 pixels in size
.attr("markerWidth", 6)
.attr("markerHeight", 6)
// orient the same as the line we will attach the arrow to
.attr("orient", "auto")
.append("path")
// top-left (M0,0) ->
// middle-right (L2,1) ->
// bottom-left (L0,2) ->
// top-left (Z)
// . 0 0
// 0 0 .
// . 0 0
.attr("d", "M0,0 L2,1 L0,2 Z")
// fill shape with white color
.attr("fill", "white");
g.append("line")
.attr("x1", 0)
.attr("y1", -circleRadius + 95)
.attr("x2", 0)
.attr("y2", -circleRadius + 2 + 6)
.attr("stroke", "white")
.attr("stroke-width", 0.7)
// attach arrow end
.attr("marker-end", "url(#arrowhead)");
svg.select("defs")
.append("marker") // add a circle in a similar fashion
.attr("id", "circleStart")
.attr("viewBox", "0 0 2 2")
.attr("refX", 1)
.attr("refY", 1)
.attr("markerWidth", 5)
.attr("markerHeight", 5)
.attr("orient", "auto")
.append("circle")
.attr("cx", 1)
.attr("cy", 1)
.attr("r", 1)
.attr("fill", "white");
g.append("line")
.attr("x1", 0)
.attr("y1", 0)
.attr("x2", 0)
.attr("y2", -circleRadius + 95)
.attr("stroke", "white")
.attr("stroke-width", 0.7)
.attr("stroke-dasharray", "4 4")
// attach circle
.attr("marker-start", "url(#circleStart)");
g.append("text")
.attr("x", -80)
.attr("y", -3)
.text("кількість годин (одна доба)")
.attr("fill", "white")
.attr("font-size", "8px")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("transform", `rotate(${90},${5},${0})`);
g.append("text")
.attr("x", 0)
.attr("y", 8)
.text("00:00")
.attr("fill", "white")
.attr("font-size", "6px")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("transform", `rotate(${0},${0},${0})`);
g.append("text")
.attr("x", 0)
.attr("y", -circleRadius + 2)
.text("24:00")
.attr("fill", "white")
.attr("font-size", "6px")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("transform", `rotate(${0},${0},${0})`);
const minDayLen = d3.min(data, d => d.dayLength);
const maxDayLen = d3.max(data, d => d.dayLength);
const minDay = data.find(d => d.dayLength === minDayLen);
const maxDay = data.find(d => d.dayLength === maxDayLen);
[minDay, maxDay].forEach(d => {
const index = 2 + d.i + d.monthIndex;
const angle = angleScale(index) - Math.PI/2;
const r_e = rScale(total_mins) + 15;
const r_s = r_e + 15 + 12;
const angleRt = (angle + Math.PI / 2) / Math.PI * 180;
const recx = Math.cos(angle - 0.0085) * (r_e - 3);
const recy = Math.sin(angle - 0.0085) * (r_e - 3);
g.append("rect")
.attr("x", recx)
.attr("y", recy)
.attr("width", 5)
.attr("height", 10)
.attr("rx", 2)
.attr("ry", 2)
.attr("stroke", "white")
.attr("stroke-width", 0.5)
.attr("transform", `rotate(${angleRt},${recx},${recy})`);
g.append("line")
.attr("x1", Math.cos(angle) * r_e)
.attr("y1", Math.sin(angle) * r_e)
.attr("x2", Math.cos(angle) * r_s)
.attr("y2", Math.sin(angle) * r_s)
.attr("stroke", "white")
.attr("stroke-width", 0.7)
.attr("stroke-dasharray", "4 4");
const dayx = Math.cos(angle - 0.003) * (r_s + 16);
const dayy = Math.sin(angle - 0.003) * (r_s + 16);
const dayRt = (angle + Math.PI/2) / Math.PI * 180;
g.append("text")
.attr("x", dayx)
.attr("y", dayy)
.text(d.dayLength === minDayLen ? "min" : "max")
.attr("fill", "white")
.attr("font-size", "6px")
.attr("font-family", "'Arial Narrow', Arial, sans-serif")
.attr("font-stretch", "condensed")
.attr("text-anchor", "start")
.attr("alignment-baseline", "middle")
.attr("transform", `rotate(${dayRt},${dayx},${dayy})`);
const daylenx = Math.cos(angle - 0.003) * (r_s + 7);
const dayleny = Math.sin(angle - 0.003) * (r_s + 7);
const daylenRt = (angle + Math.PI/2) / Math.PI * 180;
g.append("text")
.attr("x", daylenx)
.attr("y", dayleny)
.text(`${d.hours_len}ч ${d.minutes_len}м ${d.seconds_len}с`)
.attr("fill", "white")
.attr("font-size", "6px")
.attr("font-family", "'Arial Narrow', Arial, sans-serif")
.attr("font-stretch", "condensed")
.attr("text-anchor", "start")
.attr("alignment-baseline", "middle")
.attr("transform", `rotate(${daylenRt},${daylenx},${dayleny})`);
});
const monthsUk = [
"Січень","Лютий","Березень","Квітень",
"Травень","Червень","Липень","Серпень",
"Вересень","Жовтень","Листопад","Грудень"
];
const monthsdrawn = new Set();
const weekdaysUk = [
"ПН","ВТ","СР","ЧТ",
"ПТ","СБ","НД"
];
data.forEach(d => {
const index = 2 + d.i + d.monthIndex;
const angle = angleScale(index) - Math.PI/2;
const r_s = rScale(total_mins - d.dayLength), r_e = rScale(total_mins);
g.append("line")
// convert polar coordinates to x,y coords
.attr("x1", Math.cos(angle) * r_s)
.attr("y1", Math.sin(angle) * r_s)
.attr("x2", Math.cos(angle) * r_e)
.attr("y2", Math.sin(angle) * r_e)
.attr("stroke", "white")
.attr("stroke-width", 0.62);
const dayRt = (angle + Math.PI/2) / Math.PI * 180;
const r_day = r_e + 4;
const dayx = Math.cos(angle) * r_day;
const dayy = Math.sin(angle) * r_day;
g.append("text")
.attr("x", dayx)
.attr("y", dayy)
.text(d.dayIndex)
.attr("fill", "white")
.attr("font-size", "4px")
.attr("font-family", "'Arial Narrow', Arial, sans-serif")
.attr("font-stretch", "condensed")
.attr("font-weight", d.dayIndex % 2 === 1 ? "bold" : "normal")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("transform", `rotate(${dayRt},${dayx},${dayy})`);
const r_week = r_e + 9;
const weekx = Math.cos(angle) * r_week;
const weeky = Math.sin(angle) * r_week;
const isBoldDay = [0, 2, 4].includes(d.weekdayIndex);
const isCircleDay = [5, 6].includes(d.weekdayIndex);
if (isCircleDay) {
g.append("circle")
.attr("cx", weekx)
.attr("cy", weeky)
.attr("r", 2)
.attr("fill", "white")
.attr("transform", `rotate(${dayRt},${weekx},${weeky})`);
g.append("text")
.attr("x", weekx)
.attr("y", weeky)
.text(weekdaysUk[d.weekdayIndex])
.attr("fill", "black")
.attr("font-size", "3px")
.attr("font-family", "'Arial Narrow', Arial, sans-serif")
.attr("font-stretch", "condensed")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("transform", `rotate(${dayRt},${weekx},${weeky})`);
} else {
g.append("text")
.attr("x", weekx)
.attr("y", weeky)
.text(weekdaysUk[d.weekdayIndex])
.attr("fill", "white")
.attr("font-size", "3px")
.attr("font-family", "'Arial Narrow', Arial, sans-serif")
.attr("font-stretch", "condensed")
.attr("font-weight", isBoldDay ? "bold" : "normal")
.attr("text-anchor", "middle")
.attr("alignment-baseline", "middle")
.attr("transform", `rotate(${dayRt},${weekx},${weeky})`);
}
if (!monthsdrawn.has(d.monthIndex)) {
const angle_label = angleScale(index) - Math.PI/2;
monthsdrawn.add(d.monthIndex);
const monthName = monthsUk[d.monthIndex];
const labelR = r_e + 17;
const x = Math.cos(angle_label) * labelR;
const y = Math.sin(angle_label) * labelR;
const textRt = (angle_label + Math.PI/2) / Math.PI * 180;
g.append("text")
.attr("x", x)
.attr("y", y)
.text(monthName)
.attr("fill", "white")
.attr("font-size", "6px")
.attr("text-anchor", "start")
.attr("alignment-baseline", "middle")
.attr("transform", `rotate(${textRt},${x},${y})`);
}
});
const labelGroup = svg.append("g")
.attr("transform", `translate(${20},${80})`);
const year = parseDate(sun_time_data[0].date).getFullYear();
labelGroup.append("text")
.text(year)
.attr("fill", "white")
.attr("font-size", "40px")
.attr("font-family", "Inter")
.attr("text-anchor", "start");
labelGroup.append("text")
.text("тривалість світлового дня, Харків")
.attr("fill", "white")
.attr("font-size", "6px")
.attr("font-family", "Inter")
.attr("text-anchor", "start")
.attr("transform", `translate(${0},${30})`);
labelGroup.append("text")
.text("by Mykhailo Bondarenko")
.attr("fill", "white")
.attr("font-size", "4px")
.attr("font-family", "Inter")
.attr("text-anchor", "start")
.attr("transform", `translate(${0},${-50})`);
labelGroup.append("line")
.attr("x1", 0)
.attr("x2", 100)
.attr("y1", 40)
.attr("y2", 40)
.attr("stroke", "white")
.attr("stroke-width", 0.7);
labelGroup.append("text")
.text("дані: https://www.sunearthtools.com")
.attr("fill", "white")
.attr("font-size", "4px")
.attr("font-family", "Inter")
.attr("text-anchor", "start")
.attr("transform", `translate(${0},${50})`);
return svg.node();
}