Skip to content

Commit

Permalink
Merge branch 'feat/raport-page' into fix/reflectance-chart
Browse files Browse the repository at this point in the history
  • Loading branch information
wikipop authored Oct 7, 2024
2 parents 3f4b4da + 9977c88 commit 77e4326
Show file tree
Hide file tree
Showing 22 changed files with 5,294 additions and 27 deletions.
21 changes: 21 additions & 0 deletions frontend/components/ui/card/Card.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<template>
<div
:class="
cn(
'rounded-xl border bg-card text-card-foreground shadow',
props.class,
)
"
>
<slot />
</div>
</template>

<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"]
}>();
</script>
14 changes: 14 additions & 0 deletions frontend/components/ui/card/CardContent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<template>
<div :class="cn('p-6 pt-0', props.class)">
<slot />
</div>
</template>

<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"]
}>();
</script>
14 changes: 14 additions & 0 deletions frontend/components/ui/card/CardDescription.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<template>
<p :class="cn('text-sm text-muted-foreground', props.class)">
<slot />
</p>
</template>

<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"]
}>();
</script>
14 changes: 14 additions & 0 deletions frontend/components/ui/card/CardFooter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<template>
<div :class="cn('flex items-center p-6 pt-0', props.class)">
<slot />
</div>
</template>

<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"]
}>();
</script>
14 changes: 14 additions & 0 deletions frontend/components/ui/card/CardHeader.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<template>
<div :class="cn('flex flex-col gap-y-1.5 p-6', props.class)">
<slot />
</div>
</template>

<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"]
}>();
</script>
18 changes: 18 additions & 0 deletions frontend/components/ui/card/CardTitle.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<template>
<h3
:class="
cn('font-semibold leading-none tracking-tight', props.class)
"
>
<slot />
</h3>
</template>

<script setup lang="ts">
import type { HTMLAttributes } from "vue";
import { cn } from "@/lib/utils";
const props = defineProps<{
class?: HTMLAttributes["class"]
}>();
</script>
6 changes: 6 additions & 0 deletions frontend/components/ui/card/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { default as Card } from "./Card.vue";
export { default as CardHeader } from "./CardHeader.vue";
export { default as CardTitle } from "./CardTitle.vue";
export { default as CardDescription } from "./CardDescription.vue";
export { default as CardContent } from "./CardContent.vue";
export { default as CardFooter } from "./CardFooter.vue";
105 changes: 105 additions & 0 deletions frontend/components/ui/chart-line/LineChart.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<template>
<div :class="cn('w-full h-[400px] flex flex-col items-end', $attrs.class ?? '')">
<ChartLegend v-if="showLegend" v-model:items="legendItems" @legend-item-click="handleLegendItemClick" />
<VisXYContainer
:margin="{ left: 20, right: 20 }"
:data="data"
:style="{ height: isMounted ? '100%' : 'auto' }"
>
<ChartCrosshair v-if="showTooltip" :colors="colors" :items="legendItems" :index="index" :custom-tooltip="customTooltip" />
<template v-for="(category, i) in categories" :key="category">
<VisLine
:x="(d: Data, i: number) => i"
:y="(d: Data) => d[category]"
:curve-type="curveType"
:color="colors[i]"
:attributes="{
[Line.selectors.line]: {
opacity: legendItems.find(item => item.name === category)?.inactive ? filterOpacity : 1,
},
}"
/>
</template>
<VisAxis
v-if="showXAxis"
type="x"
:tick-format="xFormatter ?? ((v: number) => data[v]?.[index])"
:grid-line="false"
:tick-line="false"
tick-text-color="hsl(var(--vis-text-color))"
/>
<VisAxis
v-if="showYAxis"
type="y"
:tick-line="false"
:tick-format="yFormatter"
:domain-line="false"
:grid-line="showGridLine"
:attributes="{
[Axis.selectors.grid]: {
class: 'text-muted',
},
}"
tick-text-color="hsl(var(--vis-text-color))"
/>
<slot />
</VisXYContainer>
</div>
</template>
<script setup lang="ts" generic="T extends Record<string, any>">
import { type BulletLegendItemInterface, CurveType } from "@unovis/ts";
import { VisAxis, VisLine, VisXYContainer } from "@unovis/vue";
import { Axis, Line } from "@unovis/ts";
import { type Component, computed, ref } from "vue";
import { useMounted } from "@vueuse/core";
import type { BaseChartProps } from "./index";
import { ChartCrosshair, ChartLegend, defaultColors } from "@/components/ui/chart";
import { cn } from "@/lib/utils";
const props = withDefaults(defineProps<BaseChartProps<T> & {
/**
* Render custom tooltip component.
*/
customTooltip?: Component
/**
* Type of curve
*/
curveType?: CurveType
}>(), {
curveType: CurveType.MonotoneX,
filterOpacity: 0.2,
margin: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
showXAxis: true,
showYAxis: true,
showTooltip: true,
showLegend: true,
showGridLine: true
});
const emits = defineEmits<{
legendItemClick: [d: BulletLegendItemInterface, i: number]
}>();
type KeyOfT = Extract<keyof T, string>;
type Data = typeof props.data[number];
const index = computed(() => props.index as KeyOfT);
const colors = computed(() => props.colors?.length ? props.colors : defaultColors(props.categories.length));
const legendItems = ref<BulletLegendItemInterface[]>(props.categories.map((category, i) => ({
name: category,
color: colors.value[i],
inactive: false
})));
const isMounted = useMounted();
function handleLegendItemClick(d: BulletLegendItemInterface, i: number) {
emits("legendItemClick", d, i);
}
</script>
66 changes: 66 additions & 0 deletions frontend/components/ui/chart-line/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { Spacing } from "@unovis/ts";

export { default as LineChart } from "./LineChart.vue";

type KeyOf<T extends Record<string, any>> = Extract<keyof T, string>;

export interface BaseChartProps<T extends Record<string, any>> {
/**
* The source data, in which each entry is a dictionary.
*/
data: T[]
/**
* Select the categories from your data. Used to populate the legend and toolip.
*/
categories: KeyOf<T>[]
/**
* Sets the key to map the data to the axis.
*/
index: KeyOf<T>
/**
* Change the default colors.
*/
colors?: string[]
/**
* Margin of each the container
*/
margin?: Spacing
/**
* Change the opacity of the non-selected field
* @default 0.2
*/
filterOpacity?: number
/**
* Function to format X label
*/
xFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
/**
* Function to format Y label
*/
yFormatter?: (tick: number | Date, i: number, ticks: number[] | Date[]) => string
/**
* Controls the visibility of the X axis.
* @default true
*/
showXAxis?: boolean
/**
* Controls the visibility of the Y axis.
* @default true
*/
showYAxis?: boolean
/**
* Controls the visibility of tooltip.
* @default true
*/
showTooltip?: boolean
/**
* Controls the visibility of legend.
* @default true
*/
showLegend?: boolean
/**
* Controls the visibility of gridline.
* @default true
*/
showGridLine?: boolean
}
43 changes: 43 additions & 0 deletions frontend/components/ui/chart/ChartCrosshair.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<template>
<VisTooltip :horizontal-shift="20" :vertical-shift="20" />
<VisCrosshair :template="template" :color="color" />
</template>

<script setup lang="ts">
import { VisCrosshair, VisTooltip } from "@unovis/vue";
import type { BulletLegendItemInterface } from "@unovis/ts";
import { omit } from "@unovis/ts";
import { type Component, createApp } from "vue";
import { ChartTooltip } from "./index";
const props = withDefaults(defineProps<{
colors: string[]
index: string
items: BulletLegendItemInterface[]
customTooltip?: Component
}>(), {
colors: () => []
});
// Use weakmap to store reference to each datapoint for Tooltip
const wm = new WeakMap();
function template(d: any) {
if (wm.has(d)) {
return wm.get(d);
} else {
const componentDiv = document.createElement("div");
const omittedData = Object.entries(omit(d, [props.index])).map(([key, value]) => {
const legendReference = props.items.find((i) => i.name === key);
return { ...legendReference, value };
});
const TooltipComponent = props.customTooltip ?? ChartTooltip;
createApp(TooltipComponent, { title: d[props.index].toString(), data: omittedData }).mount(componentDiv);
wm.set(d, componentDiv.innerHTML);
return componentDiv.innerHTML;
}
}
function color(d: unknown, i: number) {
return props.colors[i] ?? "transparent";
}
</script>
50 changes: 50 additions & 0 deletions frontend/components/ui/chart/ChartLegend.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<template>
<div ref="elRef" class="w-max">
<VisBulletLegend
:items="items"
:on-legend-item-click="onLegendItemClick"
/>
</div>
</template>

<script setup lang="ts">
import { VisBulletLegend } from "@unovis/vue";
import type { BulletLegendItemInterface } from "@unovis/ts";
import { BulletLegend } from "@unovis/ts";
import { nextTick, onMounted, ref } from "vue";
import { buttonVariants } from "@/components/ui/button";
const props = withDefaults(defineProps<{ items: BulletLegendItemInterface[] }>(), {
items: () => []
});
const emits = defineEmits<{
legendItemClick: [d: BulletLegendItemInterface, i: number]
"update:items": [payload: BulletLegendItemInterface[]]
}>();
const elRef = ref<HTMLElement>();
onMounted(() => {
const selector = `.${BulletLegend.selectors.item}`;
nextTick(() => {
const elements = elRef.value?.querySelectorAll(selector);
const classes = buttonVariants({ variant: "ghost", size: "xs" }).split(" ");
elements?.forEach((el) => el.classList.add(...classes, "!inline-flex", "!mr-2"));
});
});
function onLegendItemClick(d: BulletLegendItemInterface, i: number) {
emits("legendItemClick", d, i);
const isBulletActive = !props.items[i].inactive;
const isFilterApplied = props.items.some((i) => i.inactive);
if (isFilterApplied && isBulletActive) {
// reset filter
emits("update:items", props.items.map((item) => ({ ...item, inactive: false })));
} else {
// apply selection, set other item as inactive
emits("update:items", props.items.map((item) => item.name === d.name ? ({ ...d, inactive: false }) : { ...item, inactive: true }));
}
}
</script>
Loading

0 comments on commit 77e4326

Please sign in to comment.