Skip to content

Commit edde87d

Browse files
authored
Migrate top crates chart to echarts (#3)
1 parent f045253 commit edde87d

File tree

5 files changed

+115
-125
lines changed

5 files changed

+115
-125
lines changed

src/routes/stats/+page.svelte

+15-7
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
} from "./stats";
1111
import { onMount } from "svelte";
1212
import Histogram from "./HistogramChart.svelte";
13+
import TopCratesChart from "./TopCratesChart.svelte";
1314
1415
/**
1516
* @type {number[]}
@@ -33,17 +34,25 @@
3334
3435
let currentYear = moment().year();
3536
37+
/**
38+
* @type {{label:string, name:string; value: number}[]}
39+
*/
40+
let topCratesData = [];
41+
42+
let searchTime = "the last year";
43+
3644
/**
3745
*
3846
* @param {number} now
3947
* @param {number} yearAgo
4048
*/
4149
async function getEchartData(now, yearAgo) {
42-
const { weeksArr, dateArr, hourArr } = await getHistogramEchartDatas(now, yearAgo);
50+
const { weeksArr, dateArr, hourArr, topCratesArr } = await getHistogramEchartDatas(now, yearAgo);
4351
weekData = weeksArr;
4452
dateData = dateArr;
4553
hourData = hourArr;
46-
await renderCharts(now, yearAgo, moment(yearAgo).format('YYYY'));
54+
topCratesData = topCratesArr;
55+
await renderCharts(now, yearAgo);
4756
}
4857
4958
/**
@@ -52,7 +61,8 @@
5261
*/
5362
function handleChangeYear(y){
5463
currentYear = y;
55-
const year = moment(String(y));
64+
searchTime = String(y);
65+
const year = moment(searchTime);
5666
const now = year.endOf('year').valueOf();
5767
const yearAgo = year.startOf('year').valueOf();
5868
getEchartData(now, yearAgo);
@@ -61,8 +71,6 @@
6171
onMount(async () => {
6272
const now = moment().valueOf();
6373
const yearAgo = moment().startOf("day").subtract(1, "year").valueOf();
64-
65-
await renderCharts(now, yearAgo);
6674
yearList = await getYearList(currentYear);
6775
getEchartData(now, yearAgo);
6876
})
@@ -74,7 +82,7 @@
7482
<div class="chart-heatmap hidden md:block"></div>
7583
</div>
7684
<div class="search-time w-full text-center text-xl">
77-
<b>0</b> searches in <b>the last year</b>, approximately saved
85+
<b>0</b> searches in <b>{searchTime}</b>, approximately saved
7886
<b>0 seconds</b>.
7987
<b
8088
aria-label="We consider one search save 5 seconds in average, just an estimated value."
@@ -111,7 +119,7 @@
111119

112120
<div>
113121
<h3>Top searched crates</h3>
114-
<div class="topCratesData relative box-border"></div>
122+
<TopCratesChart data={topCratesData} />
115123
</div>
116124
</div>
117125
</div>

src/routes/stats/HistogramChart.svelte

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,4 @@
6464
})
6565
</script>
6666

67-
<div class="relative h-[320px] w-[380px] md:w-[460px] m-auto" bind:this={echartElement}></div>
67+
<div class="relative h-[320px] w-[340px] md:w-[460px] m-auto" bind:this={echartElement}></div>
+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<script>
2+
import { onMount } from "svelte";
3+
import { init } from "echarts";
4+
import { CHART_COLOR } from "./stats";
5+
6+
/**
7+
* @type {{label:string, name:string; value: number}[]}
8+
*/
9+
export let data = [];
10+
11+
/**
12+
* @type {HTMLElement}
13+
*/
14+
let echartElement;
15+
16+
/**
17+
* @type {any}
18+
*/
19+
let elementChart;
20+
const option = {
21+
color: CHART_COLOR,
22+
xAxis: {
23+
position: 'top',
24+
},
25+
yAxis: {
26+
type: "category",
27+
data,
28+
inverse: true,
29+
30+
},
31+
series: [
32+
{
33+
data,
34+
type: "bar",
35+
label: {
36+
show: true,
37+
color: '#000',
38+
formatter:(/** @type {{ data: {name:string} }} */ params) => params.data.name,
39+
verticalAlign: "middle",
40+
position: [10, '50%'],
41+
fontSize: 14,
42+
}
43+
},
44+
],
45+
tooltip: {
46+
show:true,
47+
trigger: 'axis'
48+
}
49+
}
50+
51+
$: {
52+
if(elementChart) {
53+
elementChart.setOption({
54+
yAxis: {
55+
data: data.map((item) => item.label),
56+
},
57+
series: [
58+
{
59+
data,
60+
}
61+
]
62+
});
63+
elementChart.resize({
64+
height: 800 / 15 * data.length + 100,
65+
});
66+
}
67+
}
68+
69+
onMount(async () => {
70+
elementChart = init(echartElement);
71+
elementChart.setOption(option);
72+
})
73+
</script>
74+
75+
<div class="relative w-[340px] md:w-[460px] m-auto" bind:this={echartElement}></div>

src/routes/stats/charts.js

-74
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,6 @@
11
import moment from "moment";
22
import * as d3 from "d3";
33

4-
function barChart({ margin, height, width, data, selector, color, }) {
5-
let tooltip;
6-
7-
let yAxis = g => g
8-
.attr("transform", `translate(${margin.left},0)`)
9-
.call(d3.axisLeft(y).tickFormat(i => data[i].label).tickSizeOuter(0))
10-
.attr('font-size', 14);
11-
12-
let xAxis = g => g
13-
.attr("transform", `translate(0,${margin.top})`)
14-
.call(d3.axisTop(x).ticks(Math.min(10, d3.max(data, d => d.value))))
15-
.call(g => g.select(".domain"))
16-
.attr('font-size', 14);
17-
18-
let y = d3.scaleBand()
19-
.domain(d3.range(data.length))
20-
.rangeRound([margin.top, height - margin.bottom])
21-
.padding(0.2);
22-
23-
let x = d3.scaleLinear()
24-
.domain([0, d3.max(data, d => d.value)])
25-
.range([margin.left, width - margin.right])
26-
27-
const svg = d3.select(selector)
28-
.append("svg")
29-
.attr("width", width)
30-
.attr("height", height);
31-
// .attr("viewBox", [0, 0, width, height]);
32-
33-
svg.append("g")
34-
.attr("fill", color)
35-
.selectAll("rect")
36-
.data(data)
37-
.join("rect")
38-
.attr("x", x(0))
39-
.attr("y", (d, i) => y(i) + y.bandwidth() / 4)
40-
.attr("width", d => x(d.value) - x(0))
41-
.attr("height", y.bandwidth() / 2)
42-
.on('mouseover', function (d, i) {
43-
let tooltipWidth = 36;
44-
tooltip = d3.select(selector)
45-
.append('div')
46-
.attr('class', 'histogram-bar-tooltip')
47-
.html(`<span>${d.value}</span>`)
48-
.style('width', `${tooltipWidth}px`)
49-
.style('left', x(d.value) + 5 + "px")
50-
.style('top', y(i) + y.bandwidth() / 2 - tooltipWidth / 3 + "px")
51-
})
52-
.on("mouseout", function (d, i) {
53-
tooltip.remove();
54-
});
55-
56-
svg.append("g")
57-
.attr("fill", "black")
58-
.attr("text-anchor", "start")
59-
.attr("font-family", "sans-serif")
60-
.attr("font-size", 14)
61-
.selectAll("text")
62-
.data(data)
63-
.join("text")
64-
.attr("x", d => x(0) + 5)
65-
.attr("y", (d, i) => y(i))
66-
.attr("dy", "0.35em")
67-
.text(d => d.name);
68-
69-
svg.append("g")
70-
.call(xAxis);
71-
72-
svg.append("g")
73-
.call(yAxis);
74-
75-
}
76-
774
function calendarHeatmap(start, end) {
785
// defaults
796
const width = 800;
@@ -349,6 +276,5 @@ function calendarHeatmap(start, end) {
349276
}
350277

351278
export {
352-
barChart,
353279
calendarHeatmap,
354280
};

src/routes/stats/stats.js

+24-43
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { barChart, calendarHeatmap } from "./charts.js";
1+
import { calendarHeatmap } from "./charts.js";
22
import { STATS_PATTERNS, Statistics } from "querylib";
33
import moment from "moment";
44

5+
const TOP_CRATE_LENGTH = 15;
56
let chartWidth = 460;
67
const TYPE_OTHER = "other";
78
export const CHART_COLOR = "rgba(249, 188, 45, 0.5)";
@@ -79,13 +80,10 @@ function calculateSavedTime(times) {
7980
}
8081
}
8182

82-
function renderSearchTimes(length = 0, searchTime) {
83+
function renderSearchTimes(length = 0) {
8384
let searchTimes = document.querySelector(".search-time");
8485
let frequency = searchTimes.querySelectorAll("b");
8586
frequency[0].textContent = `${length}`;
86-
if (searchTime) {
87-
frequency[1].textContent = `${searchTime}`;
88-
}
8987
frequency[2].textContent = calculateSavedTime(length);
9088
}
9189

@@ -157,35 +155,7 @@ function renderSearchStats(typeDataObj, total) {
157155
});
158156
}
159157

160-
function renderTopCratesChart(topCratesObj) {
161-
const topCratesContainer = document.querySelector(".topCratesData");
162-
if (topCratesContainer.hasChildNodes()) {
163-
topCratesContainer.innerHTML = null;
164-
}
165-
const topCratesData = Object.entries(topCratesObj)
166-
.sort((a, b) => b[1] - a[1])
167-
.map(([key, value], index) => {
168-
return {
169-
label: `#${index + 1}`,
170-
name: key,
171-
value
172-
};
173-
});
174-
topCratesData.splice(15);
175-
barChart({
176-
margin: ({ top: 30, right: 0, bottom: 10, left: 30 }),
177-
// Calculate height dynamically to keep the bar with consistence width regardless of the topCratesData length.
178-
height: 800 / 15 * topCratesData.length + 40,
179-
barHeight: 25,
180-
width: chartWidth,
181-
data: topCratesData,
182-
selector: ".topCratesData",
183-
color: CHART_COLOR,
184-
});
185-
}
186-
187-
188-
export async function renderCharts(now, yearAgo, searchTime) {
158+
export async function renderCharts(now, yearAgo) {
189159
chartWidth = Math.min(chartWidth, document.getElementById("chart").clientWidth) - 10;
190160
const { timeline } = await Statistics.load();
191161

@@ -209,9 +179,7 @@ export async function renderCharts(now, yearAgo, searchTime) {
209179
let typeTotal = 0;
210180
const typeDataObj = Object.create(null);
211181

212-
const topCratesObj = Object.create(null);
213-
214-
data.forEach(([t, content, type]) => {
182+
data.forEach(([t, content]) => {
215183
const time = moment(t);
216184
const hour = time.hour();
217185

@@ -225,15 +193,11 @@ export async function renderCharts(now, yearAgo, searchTime) {
225193
typeDataObj[typeName] = (typeDataObj[typeName] || 0) + 1;
226194
typeTotal += 1;
227195
}
228-
if (type) {
229-
topCratesObj[type] = (topCratesObj[type] || 0) + 1;
230-
}
231196
});
232197

233-
renderSearchTimes(data.length, searchTime);
198+
renderSearchTimes(data.length);
234199
renderHeatmap(heatMapData, now, yearAgo);
235200
renderSearchStats(typeDataObj, typeTotal);
236-
renderTopCratesChart(topCratesObj);
237201
}
238202

239203
/**
@@ -251,7 +215,9 @@ export async function getHistogramEchartDatas(now, yearAgo) {
251215
const dateArr = DATES_LABEL.map(() => 0);
252216
const hourArr = HOURS_LABEL.map(() => 0);
253217

254-
for (const [t] of data) {
218+
const topCratesObj = Object.create(null);
219+
220+
for (const [t, , type] of data) {
255221
const time = moment(t);
256222
const hour = time.hour();
257223

@@ -260,11 +226,26 @@ export async function getHistogramEchartDatas(now, yearAgo) {
260226
if (hour !== 0) {
261227
hourArr[hour - 1] += 1;
262228
}
229+
if (type) {
230+
topCratesObj[type] = (topCratesObj[type] || 0) + 1;
231+
}
263232
};
233+
234+
const topCratesArr = Object.entries(topCratesObj)
235+
.sort((a, b) => b[1] - a[1])
236+
.map(([key, value], index) => {
237+
return {
238+
label: `#${index + 1}`,
239+
name: key,
240+
value
241+
};
242+
});
243+
topCratesArr.splice(TOP_CRATE_LENGTH);
264244
return {
265245
weeksArr,
266246
dateArr,
267247
hourArr,
248+
topCratesArr,
268249
}
269250
}
270251

0 commit comments

Comments
 (0)