diff --git a/src/app/components/Habit/components/HabitFilter.tsx b/src/app/components/Habit/components/HabitFilter.tsx index 60ce63e8..d33a0a45 100644 --- a/src/app/components/Habit/components/HabitFilter.tsx +++ b/src/app/components/Habit/components/HabitFilter.tsx @@ -11,9 +11,13 @@ import { t } from "@app/locale" import { ElementDatePickerShortcut } from "@src/element-ui/date" import DateRangeFilterItem from "@app/components/common/DateRangeFilterItem" import TimeFormatFilterItem from "@app/components/common/TimeFormatFilterItem" -import { FilterOption } from "../type" import { useState } from "@hooks" +export type FilterOption = { + timeFormat: timer.app.TimeFormat + dateRange: [Date, Date] +} + type ShortCutProp = [label: string, dayAgo: number] const shortcutProps: ShortCutProp[] = [ diff --git a/src/app/components/Habit/components/Period/Average/Wrapper.tsx b/src/app/components/Habit/components/Period/Average/Wrapper.tsx index f6c3248c..cb686b79 100644 --- a/src/app/components/Habit/components/Period/Average/Wrapper.tsx +++ b/src/app/components/Habit/components/Period/Average/Wrapper.tsx @@ -5,11 +5,20 @@ * https://opensource.org/licenses/MIT */ -import { getCompareColor } from "@app/util/echarts" +import { t } from "@app/locale" +import { getCompareColor, tooltipDot } from "@app/util/echarts" import { EchartsWrapper } from "@hooks" -import { averageByDay } from "@util/period" +import { averageByDay, MINUTE_PER_PERIOD } from "@util/period" import { getPrimaryTextColor } from "@util/style" +import { formatPeriodCommon } from "@util/time" import { BarSeriesOption, ComposeOption, GridComponentOption, TitleComponentOption, TooltipComponentOption } from "echarts" +import { BarChart } from "echarts/charts" +import { GridComponent, TitleComponent, TooltipComponent } from "echarts/components" +import { use } from "echarts/core" +import { SVGRenderer } from "echarts/renderers" +import { TopLevelFormatterParams } from "echarts/types/dist/shared" + +use([SVGRenderer, BarChart, GridComponent, TitleComponent, , TooltipComponent]) type EcOption = ComposeOption< | BarSeriesOption @@ -19,6 +28,8 @@ type EcOption = ComposeOption< > export type BizOption = { + currRange: timer.period.KeyRange + prevRange: timer.period.KeyRange curr: timer.period.Row[] prev: timer.period.Row[] periodSize: number @@ -26,43 +37,107 @@ export type BizOption = { const [CURR_COLOR, PREV_COLOR] = getCompareColor() -const cvt2Item = (row: timer.period.Row, idx: number): BarSeriesOption['data'][number] => { +const cvt2Item = (row: timer.period.Row): BarSeriesOption['data'][number] => { const milliseconds = row.milliseconds return { - name: idx, value: milliseconds, row, } as unknown as BarSeriesOption['data'][number] } +const formatXAxis = (idx: number, periodSize: number) => { + let min = idx * periodSize * MINUTE_PER_PERIOD + const hour = Math.floor(min / 60) + min = min - hour * 60 + return hour.toString().padStart(2, '0') + ':' + min.toString().padStart(2, '0') +} + +const key2Str = (key: timer.period.Key) => { + const { month, date } = key + return `${month?.toString?.()?.padStart(2, '0')}/${date?.toString?.()?.padStart(2, '0')}` +} + +const range2Str = (keyRange: timer.period.KeyRange) => { + const [start, end] = keyRange + return `${key2Str(start)}-${key2Str(end)}` +} + +const formatValueLine = (mill: number, range: timer.period.KeyRange, color: string): string => { + return formatFlexLine( + `${tooltipDot(color)} ${formatPeriodCommon(mill ?? 0)}`, + range2Str(range), + ) +} + +const formatFlexLine = (left: string, right: string): string => { + return ` +
+ + ${left} + + + ${right} + +
+ ` +} + +const formatTooltip = (params: TopLevelFormatterParams, biz: BizOption): string => { + const { periodSize, prevRange, currRange } = biz + console.log(params) + if (!Array.isArray(params)) return '' + const [curr, prev] = params || [] + + const idx = curr.dataIndex + const start = formatXAxis(idx, periodSize) + const end = formatXAxis(idx + 1, periodSize) + const timeLine = formatFlexLine( + `${start}-${end}`, + t(msg => msg.habit.period.chartType.average), + ) + const spaceLine = `
` + + const currLine = formatValueLine(curr.value as number, currRange, CURR_COLOR) + const prevLine = formatValueLine(-(prev.value as number), prevRange, PREV_COLOR) + + return `${timeLine}${spaceLine}${currLine}${prevLine}` +} const generateOption = (biz: BizOption): EcOption => { let { curr, prev, periodSize } = biz curr = averageByDay(curr, periodSize) prev = averageByDay(prev, periodSize) - const currData = curr.map((r, idx) => cvt2Item(r, idx)) - const prevData = prev.map((r, idx) => cvt2Item(({ ...r, milliseconds: -r.milliseconds }), idx)) + const currData = curr.map(r => cvt2Item(r)) + const prevData = prev.map(r => cvt2Item(({ ...r, milliseconds: -r.milliseconds }))) const textColor = getPrimaryTextColor() + const borderRadius = 5 * periodSize return { tooltip: { - formatter: (params: any) => '' + trigger: 'axis', + formatter: (params: TopLevelFormatterParams) => formatTooltip(params, biz), }, grid: { - top: 60, - bottom: 30, - left: 100, - right: 80, + top: 30, + bottom: 0, + left: 40, + right: 20, }, xAxis: { - type: 'time', - axisLabel: { formatter: '{HH}:{mm}', color: textColor }, + type: 'category', + axisLabel: { + color: textColor, + interval: (16 / periodSize - 1), + formatter: (_, index) => formatXAxis(index, periodSize), + }, axisLine: { show: false }, + axisTick: { show: false }, min: 0, max: currData.length, + offset: -borderRadius * 2, }, yAxis: { type: 'value', @@ -78,14 +153,14 @@ const generateOption = (biz: BizOption): EcOption => { data: currData, barCategoryGap: '50%', color: CURR_COLOR, - itemStyle: { borderRadius: [5, 5, 0, 0] }, + itemStyle: { borderRadius: [borderRadius, borderRadius, 0, 0] }, }, { type: "bar", stack: 'one', large: true, data: prevData, color: PREV_COLOR, - itemStyle: { borderRadius: [0, 0, 5, 5] }, + itemStyle: { borderRadius: [0, 0, borderRadius, borderRadius] }, } ], } diff --git a/src/app/components/Habit/components/Period/Average/index.tsx b/src/app/components/Habit/components/Period/Average/index.tsx index 23742fa5..8371a4b7 100644 --- a/src/app/components/Habit/components/Period/Average/index.tsx +++ b/src/app/components/Habit/components/Period/Average/index.tsx @@ -8,7 +8,7 @@ import type { StyleValue } from "vue" import BarWrapper, { BizOption } from "./Wrapper" import { computed, defineComponent } from "vue" -import { usePeriodFilter, usePeriodValue } from "../context" +import { usePeriodFilter, usePeriodRange, usePeriodValue } from "../context" import { useEcharts } from "@hooks" const CONTAINER_STYLE: StyleValue = { @@ -19,9 +19,16 @@ const CONTAINER_STYLE: StyleValue = { const _default = defineComponent(() => { const value = usePeriodValue() const filter = usePeriodFilter() - const bizOption = computed(() => { - const { periodSize, average } = filter.value || {} - return { ...value.value || {}, averageByDate: average, periodSize } as BizOption + const periodRange = usePeriodRange() + const bizOption = computed(() => { + const { periodSize } = filter.value || {} + return { + curr: value.value?.curr, + prev: value.value?.prev, + periodSize, + currRange: periodRange.value?.curr, + prevRange: periodRange.value?.prev, + } }) const { elRef } = useEcharts(BarWrapper, bizOption, { manual: true }) return () =>
diff --git a/src/app/components/Habit/components/Period/Filter.tsx b/src/app/components/Habit/components/Period/Filter.tsx index d647c2aa..d9d5b623 100644 --- a/src/app/components/Habit/components/Period/Filter.tsx +++ b/src/app/components/Habit/components/Period/Filter.tsx @@ -6,9 +6,10 @@ */ import SelectFilterItem from '@app/components/common/SelectFilterItem' -import SwitchFilterItem from '@app/components/common/SwitchFilterItem' import { t } from '@app/locale' +import { useCached } from '@hooks' import { HabitMessage } from '@i18n/message/app/habit' +import { ElRadioButton, ElRadioGroup } from 'element-plus' import { PropType, defineComponent, ref, watch } from 'vue' // [value, label] @@ -28,9 +29,17 @@ function allOptions(): Record { return allOptions } +const ALL_CHARTS = ['average', 'trend', 'stack'] as const +export type ChartType = typeof ALL_CHARTS[number] +const CHART_CONFIG: { [type in ChartType]: string } = { + average: t(msg => msg.habit.period.chartType.average), + trend: t(msg => msg.habit.period.chartType.trend), + stack: t(msg => msg.habit.period.chartType.stack), +} + export type FilterOption = { periodSize: number - average: boolean + chartType: ChartType } const _default = defineComponent({ @@ -42,31 +51,43 @@ const _default = defineComponent({ }, setup(prop, ctx) { const periodSize = ref(prop.defaultValue?.periodSize || 1) - const average = ref(prop.defaultValue?.average || false) - watch([periodSize, average], () => + const { data: chartType, setter: setChartType } = useCached('habit-period-chart-type', prop.defaultValue?.chartType) + watch([periodSize, chartType], () => ctx.emit('change', { periodSize: periodSize.value, - average: average.value, + chartType: chartType.value, }) ) - return () => <> - { - const newPeriodSize = parseInt(val) - if (isNaN(newPeriodSize)) return - periodSize.value = newPeriodSize + return () => ( +
- msg.habit.period.averageLabel)} - defaultValue={average.value} - onChange={(val: boolean) => average.value = val} - /> - + > + { + const newPeriodSize = parseInt(val) + if (isNaN(newPeriodSize)) return + periodSize.value = newPeriodSize + }} + /> + val && setChartType(val as ChartType)} + > + {Object.entries(CHART_CONFIG).map(([type, name]) => ( + + {name} + + ))} + +
+ ) }, }) diff --git a/src/app/components/Habit/components/Period/Trend/Wrapper.ts b/src/app/components/Habit/components/Period/Trend/Wrapper.ts index 6202cd41..079a586e 100644 --- a/src/app/components/Habit/components/Period/Trend/Wrapper.ts +++ b/src/app/components/Habit/components/Period/Trend/Wrapper.ts @@ -32,7 +32,6 @@ export type BizOption = { } function formatXAxis(params) { - console.log(params) const ts = params as number const date = new Date(ts) if (date.getHours() === 0 && date.getMinutes() === 0) { diff --git a/src/app/components/Habit/components/Period/context.ts b/src/app/components/Habit/components/Period/context.ts index 8b4c5319..f63d1f0f 100644 --- a/src/app/components/Habit/components/Period/context.ts +++ b/src/app/components/Habit/components/Period/context.ts @@ -14,9 +14,15 @@ type Value = { prev: timer.period.Row[] } +export type PeriodRange = { + curr: timer.period.KeyRange + prev: timer.period.KeyRange +} + type Context = { value: Ref filter: Ref + periodRange: Ref } const NAMESPACE = 'habitPeriod' @@ -24,8 +30,11 @@ const NAMESPACE = 'habitPeriod' export const initProvider = ( value: Ref, filter: Ref, -) => useProvide(NAMESPACE, { value, filter }) + periodRange: Ref, +) => useProvide(NAMESPACE, { value, filter, periodRange }) export const usePeriodValue = (): Ref => useProvider(NAMESPACE, "value").value export const usePeriodFilter = (): Ref => useProvider(NAMESPACE, "filter").filter + +export const usePeriodRange = (): Ref => useProvider(NAMESPACE, "periodRange").periodRange \ No newline at end of file diff --git a/src/app/components/Habit/components/Period/index.tsx b/src/app/components/Habit/components/Period/index.tsx index c7d8c732..01097a7d 100644 --- a/src/app/components/Habit/components/Period/index.tsx +++ b/src/app/components/Habit/components/Period/index.tsx @@ -17,11 +17,11 @@ import periodService from "@service/period-service" import { useHabitFilter } from "../context" import { getDayLength, MILL_PER_DAY } from "@util/time" import { MAX_PERIOD_ORDER, keyOf } from "@util/period" -import { initProvider } from "./context" +import { initProvider, PeriodRange } from "./context" import { useRequest } from "@src/hooks/useRequest" import Average from "./Average" -const computeRange = (filterDateRange: [Date, Date]): { curr: timer.period.KeyRange, prev: timer.period.KeyRange } => { +const computeRange = (filterDateRange: [Date, Date]): PeriodRange => { const [startDate, endDate] = filterDateRange || [] const dateLength = getDayLength(startDate, endDate) const prevStartDate = new Date(startDate.getTime() - MILL_PER_DAY * dateLength) @@ -46,7 +46,7 @@ const _default = defineComponent({ setup: () => { const globalFilter = useHabitFilter() const periodRange = computed(() => computeRange(globalFilter.value?.dateRange)) - const filter = ref({ periodSize: 1, average: false }) + const filter = ref({ periodSize: 1, chartType: 'average' }) const { data } = useRequest(async () => { const { curr: currRange, prev: prevRange } = periodRange.value || {} @@ -58,7 +58,7 @@ const _default = defineComponent({ return { curr, prev } }, { deps: [periodRange, filter], defaultValue: { curr: [], prev: [] } }) - initProvider(data, filter) + initProvider(data, filter, periodRange) return () => msg.habit.period.title)} @@ -70,7 +70,7 @@ const _default = defineComponent({
{ - filter.value?.average + filter.value?.chartType === 'average' ? : } diff --git a/src/app/components/Habit/components/Site/Summary.tsx b/src/app/components/Habit/components/Site/Summary.tsx index 50668ecf..1764b71d 100644 --- a/src/app/components/Habit/components/Site/Summary.tsx +++ b/src/app/components/Habit/components/Site/Summary.tsx @@ -4,9 +4,9 @@ import { periodFormatter } from "@app/util/time" import { computed, defineComponent } from "vue" import { useHabitFilter } from "../context" import { useRows } from "./context" -import { FilterOption } from "../../type" import { computeAverageLen } from "./common" import { sum } from "@util/array" +import { FilterOption } from "../HabitFilter" type Result = { focus: { diff --git a/src/app/components/Habit/components/context.ts b/src/app/components/Habit/components/context.ts index 42840ea2..2341214d 100644 --- a/src/app/components/Habit/components/context.ts +++ b/src/app/components/Habit/components/context.ts @@ -6,8 +6,8 @@ */ import { Ref } from "vue" -import { FilterOption } from "../type" import { useProvide, useProvider } from "@hooks" +import { FilterOption } from "./HabitFilter" type Context = { filter: Ref diff --git a/src/app/components/Habit/index.tsx b/src/app/components/Habit/index.tsx index 28a846f1..797eb2f6 100644 --- a/src/app/components/Habit/index.tsx +++ b/src/app/components/Habit/index.tsx @@ -4,12 +4,10 @@ * This software is released under the MIT License. * https://opensource.org/licenses/MIT */ -import type { FilterOption } from "./type" - import { defineComponent } from "vue" import { daysAgo } from "@util/time" import ContentContainer from "@app/components/common/ContentContainer" -import HabitFilter from "./components/HabitFilter" +import HabitFilter, { FilterOption } from "./components/HabitFilter" import Site from "./components/Site" import Period from "./components/Period" import { initProvider } from "./components/context" diff --git a/src/app/components/Habit/type.d.ts b/src/app/components/Habit/type.d.ts deleted file mode 100644 index 15df48fb..00000000 --- a/src/app/components/Habit/type.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type FilterOption = { - timeFormat: timer.app.TimeFormat - dateRange: [Date, Date] -} \ No newline at end of file diff --git a/src/i18n/message/app/habit-resource.json b/src/i18n/message/app/habit-resource.json index d149db14..5fd0d47c 100644 --- a/src/i18n/message/app/habit-resource.json +++ b/src/i18n/message/app/habit-resource.json @@ -7,7 +7,11 @@ "title": "不同时段的访问习惯", "busiest": "每天最繁忙时段", "idle": "最长空闲时段", - "averageLabel": "平均每天", + "chartType": { + "average": "日均", + "trend": "趋势", + "stack": "累积" + }, "sizes": { "fifteen": "每十五分钟统计一次", "halfHour": "每半小时统计一次", @@ -35,7 +39,6 @@ }, "zh_TW": { "period": { - "averageLabel": "平均每天", "sizes": { "fifteen": "按十五分鐘統計", "halfHour": "按半小時統計", @@ -65,7 +68,11 @@ "title": "Habit of time periods", "busiest": "Busiest time of day", "idle": "Longest idle period", - "averageLabel": "Daily average", + "chartType": { + "average": "Daily average", + "trend": "Trend", + "stack": "Stack" + }, "sizes": { "fifteen": "Per 15 minutes", "halfHour": "Per half hour", @@ -93,7 +100,6 @@ }, "ja": { "period": { - "averageLabel": "1日平均", "sizes": { "fifteen": "15分で統計", "halfHour": "30分で統計", @@ -117,7 +123,6 @@ }, "pt_PT": { "period": { - "averageLabel": "Média diária", "sizes": { "fifteen": "Por 15 minutos", "halfHour": "Por meia hora", @@ -147,7 +152,6 @@ "hour": "Кожну годину", "twoHour": "Кожні 2 години" }, - "averageLabel": "Середнє за день", "title": "Проміжки часу", "busiest": "Найзавантаженіший час доби", "idle": "Найдовший період бездіяльності" @@ -171,7 +175,6 @@ "title": "Hábito de períodos de tiempo", "busiest": "Hora más ocupada del día", "idle": "Periodo inactivo más largo", - "averageLabel": "Promedio diario", "sizes": { "fifteen": "Por 15 minutos", "halfHour": "Por media hora", @@ -195,7 +198,6 @@ "title": "Gewohnheiten jeden Augenblick", "busiest": "Die geschäftigste Zeit des Tages", "idle": "Längste Leerlaufzeit", - "averageLabel": "Täglicher Durchschnitt", "sizes": { "fifteen": "Pro 15 Minuten", "halfHour": "Pro halbe Stunde", @@ -219,7 +221,6 @@ "title": "Habitude des périodes de temps", "busiest": "Période de la journée la plus chargée", "idle": "La plus longue période d'inactivité", - "averageLabel": "Moyenne quotidienne", "sizes": { "fifteen": "Par tranche de 15 minutes", "halfHour": "Par demi-heure", diff --git a/src/i18n/message/app/habit.ts b/src/i18n/message/app/habit.ts index 2d893125..25282083 100644 --- a/src/i18n/message/app/habit.ts +++ b/src/i18n/message/app/habit.ts @@ -15,7 +15,11 @@ export type HabitMessage = { title: string busiest: string idle: string - averageLabel: string + chartType: { + average: string + trend: string + stack: string + } sizes: { fifteen: string halfHour: string