Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 49 additions & 30 deletions app/components/Package/TrendsChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,44 @@ import { useCssVariables } from '~/composables/useColors'
import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch } from '~/utils/colors'
import { getFrameworkColor, isListedFramework } from '~/utils/frameworks'
import { drawNpmxLogoAndTaglineWatermark } from '~/composables/useChartWatermark'
import type {
ChartTimeGranularity,
DailyDataPoint,
DateRangeFields,
EvolutionData,
EvolutionOptions,
MonthlyDataPoint,
WeeklyDataPoint,
YearlyDataPoint,
} from '~/types/chart'

const props = withDefaults(
defineProps<{
// For single package downloads history
weeklyDownloads?: WeeklyDataPoint[]
inModal?: boolean

const props = defineProps<{
// For single package downloads history
weeklyDownloads?: WeeklyDataPoint[]
inModal?: boolean

/**
* Backward compatible single package mode.
* Used when `weeklyDownloads` is provided.
*/
packageName?: string
/**
* Backward compatible single package mode.
* Used when `weeklyDownloads` is provided.
*/
packageName?: string

/**
* Multi-package mode.
* Used when `weeklyDownloads` is not provided.
*/
packageNames?: string[]
createdIso?: string | null
/**
* Multi-package mode.
* Used when `weeklyDownloads` is not provided.
*/
packageNames?: string[]
createdIso?: string | null

/** When true, shows facet selector (e.g. Downloads / Likes). */
showFacetSelector?: boolean
}>()
/** When true, shows facet selector (e.g. Downloads / Likes). */
showFacetSelector?: boolean
permalink?: boolean
}>(),
{
permalink: false,
},
)

const { locale } = useI18n()
const { accentColors, selectedAccentColor } = useAccentColor()
Expand Down Expand Up @@ -110,14 +126,7 @@ const watermarkColors = computed(() => ({
const mobileBreakpointWidth = 640
const isMobile = computed(() => width.value > 0 && width.value < mobileBreakpointWidth)

type ChartTimeGranularity = 'daily' | 'weekly' | 'monthly' | 'yearly'
const DEFAULT_GRANULARITY: ChartTimeGranularity = 'weekly'
type EvolutionData = DailyDataPoint[] | WeeklyDataPoint[] | MonthlyDataPoint[] | YearlyDataPoint[]

type DateRangeFields = {
startDate?: string
endDate?: string
}

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null
Expand Down Expand Up @@ -322,7 +331,10 @@ const effectivePackageNames = computed<string[]>(() => {
return single ? [single] : []
})

const selectedGranularity = shallowRef<ChartTimeGranularity>(DEFAULT_GRANULARITY)
const selectedGranularity = usePermalink<ChartTimeGranularity>('granularity', DEFAULT_GRANULARITY, {
permanent: props.permalink,
})

const displayedGranularity = shallowRef<ChartTimeGranularity>(DEFAULT_GRANULARITY)

const isEndDateOnPeriodEnd = computed(() => {
Expand Down Expand Up @@ -352,8 +364,13 @@ const shouldRenderEstimationOverlay = computed(
() => !pending.value && isEstimationGranularity.value,
)

const startDate = shallowRef<string>('') // YYYY-MM-DD
const endDate = shallowRef<string>('') // YYYY-MM-DD
const startDate = usePermalink<string>('start', '', {
permanent: props.permalink,
})
const endDate = usePermalink<string>('end', '', {
permanent: props.permalink,
})

const hasUserEditedDates = shallowRef(false)

/**
Expand Down Expand Up @@ -578,7 +595,9 @@ const METRICS = computed<MetricDef[]>(() => [
},
])

const selectedMetric = shallowRef<MetricId>(DEFAULT_METRIC_ID)
const selectedMetric = usePermalink<MetricId>('facet', DEFAULT_METRIC_ID, {
permanent: props.permalink,
})

// Per-metric state keyed by metric id
const metricStates = reactive<
Expand Down
39 changes: 36 additions & 3 deletions app/components/Package/WeeklyDownloadStats.vue
Original file line number Diff line number Diff line change
@@ -1,20 +1,36 @@
<script setup lang="ts">
import { VueUiSparkline } from 'vue-data-ui/vue-ui-sparkline'
import { useCssVariables } from '~/composables/useColors'
import type { WeeklyDataPoint } from '~/types/chart'
import { OKLCH_NEUTRAL_FALLBACK, lightenOklch } from '~/utils/colors'

const props = defineProps<{
packageName: string
createdIso: string | null
}>()

const router = useRouter()
const route = useRoute()

const chartModal = useModal('chart-modal')
const hasChartModalTransitioned = shallowRef(false)
const isChartModalOpen = shallowRef(false)

const isChartModalOpen = shallowRef<boolean>(false)

function handleModalClose() {
isChartModalOpen.value = false
hasChartModalTransitioned.value = false

router.replace({
query: {
...route.query,
modal: undefined,
granularity: undefined,
end: undefined,
start: undefined,
facet: undefined,
},
})
}

function handleModalTransitioned() {
Expand Down Expand Up @@ -95,6 +111,14 @@ async function openChartModal() {

isChartModalOpen.value = true
hasChartModalTransitioned.value = false

await router.replace({
query: {
...route.query,
modal: 'chart',
},
})

// ensure the component renders before opening the dialog
await nextTick()
await nextTick()
Expand All @@ -119,8 +143,16 @@ async function loadWeeklyDownloads() {
}
}

onMounted(() => {
loadWeeklyDownloads()
onMounted(async () => {
await loadWeeklyDownloads()

if (route.query.modal === 'chart') {
isChartModalOpen.value = true
}

if (isChartModalOpen.value && hasWeeklyDownloads.value) {
openChartModal()
}
})

watch(
Expand Down Expand Up @@ -284,6 +316,7 @@ const config = computed(() => {
:inModal="true"
:packageName="props.packageName"
:createdIso="createdIso"
permalink
show-facet-selector
/>
</Transition>
Expand Down
48 changes: 8 additions & 40 deletions app/composables/useCharts.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,19 @@
import type { MaybeRefOrGetter } from 'vue'
import { toValue } from 'vue'
import type {
DailyDataPoint,
DailyRawPoint,
EvolutionOptions,
MonthlyDataPoint,
WeeklyDataPoint,
YearlyDataPoint,
} from '~/types/chart'
import { fetchNpmDownloadsRange } from '~/utils/npm/api'

export type PackumentLikeForTime = {
time?: Record<string, string>
}

export type DailyDataPoint = { value: number; day: string; timestamp: number }
export type WeeklyDataPoint = {
value: number
weekKey: string
weekStart: string
weekEnd: string
timestampStart: number
timestampEnd: number
}
export type MonthlyDataPoint = { value: number; month: string; timestamp: number }
export type YearlyDataPoint = { value: number; year: string; timestamp: number }

type EvolutionOptionsBase = {
startDate?: string
endDate?: string
}

export type EvolutionOptionsDay = EvolutionOptionsBase & {
granularity: 'day'
}
export type EvolutionOptionsWeek = EvolutionOptionsBase & {
granularity: 'week'
weeks?: number
}
export type EvolutionOptionsMonth = EvolutionOptionsBase & {
granularity: 'month'
months?: number
}
export type EvolutionOptionsYear = EvolutionOptionsBase & {
granularity: 'year'
}

export type EvolutionOptions =
| EvolutionOptionsDay
| EvolutionOptionsWeek
| EvolutionOptionsMonth
| EvolutionOptionsYear

type DailyRawPoint = { day: string; value: number }

function toIsoDateString(date: Date): string {
return date.toISOString().slice(0, 10)
}
Expand Down
26 changes: 26 additions & 0 deletions app/composables/usePermalink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Creates a computed property that uses route query parameters by default,
* with an option to use local state instead.
*/
export function usePermalink<T extends string = string>(
queryKey: string,
defaultValue: T = '' as T,
options: { permanent?: boolean } = {},
): WritableComputedRef<T> {
const { permanent = true } = options
const localValue = shallowRef<T>(defaultValue)
const routeValue = useRouteQuery<T>(queryKey, defaultValue)

const permalinkValue = computed({
get: () => (permanent ? routeValue.value : localValue.value),
set: (value: T) => {
if (permanent) {
routeValue.value = value
} else {
localValue.value = value
}
},
})

return permalinkValue
}
52 changes: 52 additions & 0 deletions app/types/chart.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export type ChartTimeGranularity = 'daily' | 'weekly' | 'monthly' | 'yearly'

export type DateRangeFields = {
startDate?: string
endDate?: string
}

export type DailyDataPoint = { value: number; day: string; timestamp: number }
export type WeeklyDataPoint = {
value: number
weekKey: string
weekStart: string
weekEnd: string
timestampStart: number
timestampEnd: number
}
export type MonthlyDataPoint = { value: number; month: string; timestamp: number }
export type YearlyDataPoint = { value: number; year: string; timestamp: number }

export type EvolutionData =
| DailyDataPoint[]
| WeeklyDataPoint[]
| MonthlyDataPoint[]
| YearlyDataPoint[]

type EvolutionOptionsBase = {
startDate?: string
endDate?: string
}

export type EvolutionOptionsDay = EvolutionOptionsBase & {
granularity: 'day'
}
export type EvolutionOptionsWeek = EvolutionOptionsBase & {
granularity: 'week'
weeks?: number
}
export type EvolutionOptionsMonth = EvolutionOptionsBase & {
granularity: 'month'
months?: number
}
export type EvolutionOptionsYear = EvolutionOptionsBase & {
granularity: 'year'
}

export type EvolutionOptions =
| EvolutionOptionsDay
| EvolutionOptionsWeek
| EvolutionOptionsMonth
| EvolutionOptionsYear

export type DailyRawPoint = { day: string; value: number }
Loading