mirror of
https://github.com/duhanbalci/dreport.git
synced 2026-07-01 18:39:16 +00:00
fixes
This commit is contained in:
@@ -230,6 +230,26 @@ pub struct ChartAxis {
|
|||||||
pub y_label: Option<String>,
|
pub y_label: Option<String>,
|
||||||
pub show_grid: Option<bool>,
|
pub show_grid: Option<bool>,
|
||||||
pub grid_color: Option<String>,
|
pub grid_color: Option<String>,
|
||||||
|
/// Show vertical grid lines at each category (line charts). Defaults to true.
|
||||||
|
pub show_vertical_grid: Option<bool>,
|
||||||
|
pub vertical_grid_color: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub reference_lines: Vec<ChartReferenceLine>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ChartReferenceLine {
|
||||||
|
/// Category index (0-based) where the vertical line is drawn
|
||||||
|
pub category_index: usize,
|
||||||
|
#[serde(default)]
|
||||||
|
pub color: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub width: Option<f64>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub label: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub dash: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import RichTextProperties from '../properties/RichTextProperties.vue'
|
|||||||
import ContainerProperties from '../properties/ContainerProperties.vue'
|
import ContainerProperties from '../properties/ContainerProperties.vue'
|
||||||
import RepeatingTableProperties from '../properties/RepeatingTableProperties.vue'
|
import RepeatingTableProperties from '../properties/RepeatingTableProperties.vue'
|
||||||
import ChartProperties from '../properties/ChartProperties.vue'
|
import ChartProperties from '../properties/ChartProperties.vue'
|
||||||
|
import PropCondition from '../properties/shared/PropCondition.vue'
|
||||||
import '../../styles/properties.css'
|
import '../../styles/properties.css'
|
||||||
|
|
||||||
const templateStore = useTemplateStore()
|
const templateStore = useTemplateStore()
|
||||||
@@ -233,6 +234,13 @@ function deleteSelected() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Condition -->
|
||||||
|
<PropCondition
|
||||||
|
v-if="selectedElement.id !== 'root'"
|
||||||
|
:condition="selectedElement.condition"
|
||||||
|
@update:condition="(v) => templateStore.updateElement(selectedElement!.id, { condition: v } as any)"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Delete -->
|
<!-- Delete -->
|
||||||
<div v-if="selectedElement.id !== 'root'" class="prop-section">
|
<div v-if="selectedElement.id !== 'root'" class="prop-section">
|
||||||
<button class="prop-delete-btn" @click="deleteElement">Sil</button>
|
<button class="prop-delete-btn" @click="deleteElement">Sil</button>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useEditorStore } from '../../stores/editor'
|
|||||||
import { useSchemaStore } from '../../stores/schema'
|
import { useSchemaStore } from '../../stores/schema'
|
||||||
import type {
|
import type {
|
||||||
TemplateElement,
|
TemplateElement,
|
||||||
|
TextElement,
|
||||||
RepeatingTableElement,
|
RepeatingTableElement,
|
||||||
TableColumn,
|
TableColumn,
|
||||||
ImageElement,
|
ImageElement,
|
||||||
@@ -46,6 +47,18 @@ const tools: ToolItem[] = [
|
|||||||
content: 'Yeni metin',
|
content: 'Yeni metin',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Veri Metni',
|
||||||
|
icon: 'D',
|
||||||
|
create: (): TextElement => ({
|
||||||
|
id: nextId('dtxt'),
|
||||||
|
type: 'text',
|
||||||
|
position: { type: 'flow' },
|
||||||
|
size: { width: sz.auto(), height: sz.auto() },
|
||||||
|
style: { fontSize: 11, color: '#000000' },
|
||||||
|
binding: { type: 'scalar', path: '' },
|
||||||
|
}),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Zengin Metin',
|
label: 'Zengin Metin',
|
||||||
icon: 'R',
|
icon: 'R',
|
||||||
|
|||||||
@@ -42,10 +42,12 @@ const formatOptions = [
|
|||||||
<PropTextStyleGroup
|
<PropTextStyleGroup
|
||||||
:font-size="style().fontSize ?? 11"
|
:font-size="style().fontSize ?? 11"
|
||||||
:font-weight="style().fontWeight ?? 'normal'"
|
:font-weight="style().fontWeight ?? 'normal'"
|
||||||
|
:font-family="style().fontFamily"
|
||||||
:color="style().color ?? '#000000'"
|
:color="style().color ?? '#000000'"
|
||||||
:align="style().align ?? 'left'"
|
:align="style().align ?? 'left'"
|
||||||
@update:font-size="(v) => updateStyle('fontSize', v)"
|
@update:font-size="(v) => updateStyle('fontSize', v)"
|
||||||
@update:font-weight="(v) => updateStyle('fontWeight', v)"
|
@update:font-weight="(v) => updateStyle('fontWeight', v)"
|
||||||
|
@update:font-family="(v) => updateStyle('fontFamily', v)"
|
||||||
@update:color="(v) => updateStyle('color', v)"
|
@update:color="(v) => updateStyle('color', v)"
|
||||||
@update:align="(v) => updateStyle('align', v)"
|
@update:align="(v) => updateStyle('align', v)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -247,6 +247,19 @@ function removeColor(index: number) {
|
|||||||
:model-value="element.axis?.gridColor ?? '#E5E7EB'"
|
:model-value="element.axis?.gridColor ?? '#E5E7EB'"
|
||||||
@update:model-value="(v) => updateNested('axis', 'gridColor', v, {})"
|
@update:model-value="(v) => updateNested('axis', 'gridColor', v, {})"
|
||||||
/>
|
/>
|
||||||
|
<template v-if="element.chartType === 'line'">
|
||||||
|
<PropCheckbox
|
||||||
|
label="Dikey Izgara"
|
||||||
|
:model-value="element.axis?.showVerticalGrid ?? true"
|
||||||
|
@update:model-value="(v) => updateNested('axis', 'showVerticalGrid', v, {})"
|
||||||
|
/>
|
||||||
|
<PropColorInput
|
||||||
|
v-if="element.axis?.showVerticalGrid !== false"
|
||||||
|
label="Dikey Izgara Renk"
|
||||||
|
:model-value="element.axis?.verticalGridColor ?? '#E5E7EB'"
|
||||||
|
@update:model-value="(v) => updateNested('axis', 'verticalGridColor', v, {})"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
</PropSection>
|
</PropSection>
|
||||||
|
|
||||||
<!-- Stil -->
|
<!-- Stil -->
|
||||||
@@ -292,6 +305,12 @@ function removeColor(index: number) {
|
|||||||
:min="0.1"
|
:min="0.1"
|
||||||
@update:model-value="(v) => updateStyle('lineWidth', v)"
|
@update:model-value="(v) => updateStyle('lineWidth', v)"
|
||||||
/>
|
/>
|
||||||
|
<PropSelect
|
||||||
|
label="Egri Tipi"
|
||||||
|
:model-value="element.style.curveType ?? 'linear'"
|
||||||
|
:options="[{ value: 'linear', label: 'Duz' }, { value: 'smooth', label: 'Yumusak' }]"
|
||||||
|
@update:model-value="(v) => updateStyle('curveType', v)"
|
||||||
|
/>
|
||||||
<PropCheckbox
|
<PropCheckbox
|
||||||
label="Noktalar"
|
label="Noktalar"
|
||||||
:model-value="element.style.showPoints ?? true"
|
:model-value="element.style.showPoints ?? true"
|
||||||
|
|||||||
@@ -1,18 +1,41 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||||
|
import { useSchemaStore } from '../../stores/schema'
|
||||||
import PropSection from './shared/PropSection.vue'
|
import PropSection from './shared/PropSection.vue'
|
||||||
import PropNumberInput from './shared/PropNumberInput.vue'
|
import PropNumberInput from './shared/PropNumberInput.vue'
|
||||||
import PropColorInput from './shared/PropColorInput.vue'
|
import PropColorInput from './shared/PropColorInput.vue'
|
||||||
import PropCheckbox from './shared/PropCheckbox.vue'
|
import PropCheckbox from './shared/PropCheckbox.vue'
|
||||||
|
import PropFieldSelect from './shared/PropFieldSelect.vue'
|
||||||
import type { CheckboxElement } from '../../core/types'
|
import type { CheckboxElement } from '../../core/types'
|
||||||
import '../../styles/properties.css'
|
import '../../styles/properties.css'
|
||||||
|
|
||||||
const props = defineProps<{ element: CheckboxElement }>()
|
const props = defineProps<{ element: CheckboxElement }>()
|
||||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||||
|
const schemaStore = useSchemaStore()
|
||||||
|
|
||||||
|
const booleanFields = computed(() =>
|
||||||
|
schemaStore.scalarFields.filter((f) => f.type === 'boolean' || f.type === 'string'),
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PropSection title="Onay Kutusu">
|
<PropSection title="Onay Kutusu">
|
||||||
|
<PropFieldSelect
|
||||||
|
label="Veri Alani"
|
||||||
|
:model-value="element.binding?.path ?? ''"
|
||||||
|
:fields="booleanFields"
|
||||||
|
:allow-empty="true"
|
||||||
|
empty-label="Yok (statik)"
|
||||||
|
data-tip="Onay durumunun gelecegi veri alani"
|
||||||
|
@update:model-value="
|
||||||
|
(v) =>
|
||||||
|
update({
|
||||||
|
binding: v ? { type: 'scalar', path: v } : undefined,
|
||||||
|
checked: v ? undefined : element.checked ?? false,
|
||||||
|
} as any)
|
||||||
|
"
|
||||||
|
/>
|
||||||
<PropCheckbox
|
<PropCheckbox
|
||||||
v-if="!element.binding"
|
v-if="!element.binding"
|
||||||
label="Isaretli"
|
label="Isaretli"
|
||||||
@@ -40,5 +63,13 @@ const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
|||||||
data-tip="Kutu kenarlik rengi"
|
data-tip="Kutu kenarlik rengi"
|
||||||
@update:model-value="(v) => updateStyle('borderColor', v)"
|
@update:model-value="(v) => updateStyle('borderColor', v)"
|
||||||
/>
|
/>
|
||||||
|
<PropNumberInput
|
||||||
|
label="Kenar Kalinligi"
|
||||||
|
:model-value="element.style.borderWidth ?? 0.3"
|
||||||
|
:step="0.1"
|
||||||
|
:min="0"
|
||||||
|
data-tip="Kutu kenarlik kalinligi (mm)"
|
||||||
|
@update:model-value="(v) => updateStyle('borderWidth', v)"
|
||||||
|
/>
|
||||||
</PropSection>
|
</PropSection>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -29,10 +29,13 @@ const formatOptions = [
|
|||||||
/>
|
/>
|
||||||
<PropTextStyleGroup
|
<PropTextStyleGroup
|
||||||
:font-size="style().fontSize ?? 10"
|
:font-size="style().fontSize ?? 10"
|
||||||
|
:font-weight="style().fontWeight ?? 'normal'"
|
||||||
|
:font-family="style().fontFamily"
|
||||||
:color="style().color ?? '#666666'"
|
:color="style().color ?? '#666666'"
|
||||||
:align="style().align ?? 'left'"
|
:align="style().align ?? 'left'"
|
||||||
:show-weight="false"
|
|
||||||
@update:font-size="(v) => updateStyle('fontSize', v)"
|
@update:font-size="(v) => updateStyle('fontSize', v)"
|
||||||
|
@update:font-weight="(v) => updateStyle('fontWeight', v)"
|
||||||
|
@update:font-family="(v) => updateStyle('fontFamily', v)"
|
||||||
@update:color="(v) => updateStyle('color', v)"
|
@update:color="(v) => updateStyle('color', v)"
|
||||||
@update:align="(v) => updateStyle('align', v)"
|
@update:align="(v) => updateStyle('align', v)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -29,10 +29,13 @@ const formatOptions = [
|
|||||||
/>
|
/>
|
||||||
<PropTextStyleGroup
|
<PropTextStyleGroup
|
||||||
:font-size="style().fontSize ?? 10"
|
:font-size="style().fontSize ?? 10"
|
||||||
|
:font-weight="style().fontWeight ?? 'normal'"
|
||||||
|
:font-family="style().fontFamily"
|
||||||
:color="style().color ?? '#666666'"
|
:color="style().color ?? '#666666'"
|
||||||
:align="style().align ?? 'center'"
|
:align="style().align ?? 'center'"
|
||||||
:show-weight="false"
|
|
||||||
@update:font-size="(v) => updateStyle('fontSize', v)"
|
@update:font-size="(v) => updateStyle('fontSize', v)"
|
||||||
|
@update:font-weight="(v) => updateStyle('fontWeight', v)"
|
||||||
|
@update:font-family="(v) => updateStyle('fontFamily', v)"
|
||||||
@update:color="(v) => updateStyle('color', v)"
|
@update:color="(v) => updateStyle('color', v)"
|
||||||
@update:align="(v) => updateStyle('align', v)"
|
@update:align="(v) => updateStyle('align', v)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||||
|
import { useSchemaStore } from '../../stores/schema'
|
||||||
import PropSection from './shared/PropSection.vue'
|
import PropSection from './shared/PropSection.vue'
|
||||||
import PropColorInput from './shared/PropColorInput.vue'
|
import PropColorInput from './shared/PropColorInput.vue'
|
||||||
import PropSelect from './shared/PropSelect.vue'
|
import PropSelect from './shared/PropSelect.vue'
|
||||||
|
import PropFieldSelect from './shared/PropFieldSelect.vue'
|
||||||
import PropTextStyleGroup from './shared/PropTextStyleGroup.vue'
|
import PropTextStyleGroup from './shared/PropTextStyleGroup.vue'
|
||||||
import type { RichTextElement, RichTextSpan, TextStyle } from '../../core/types'
|
import type { RichTextElement, RichTextSpan, TextStyle } from '../../core/types'
|
||||||
import '../../styles/properties.css'
|
import '../../styles/properties.css'
|
||||||
|
|
||||||
const props = defineProps<{ element: RichTextElement }>()
|
const props = defineProps<{ element: RichTextElement }>()
|
||||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||||
|
const schemaStore = useSchemaStore()
|
||||||
|
|
||||||
function updateSpan(index: number, updates: Partial<RichTextSpan>) {
|
function updateSpan(index: number, updates: Partial<RichTextSpan>) {
|
||||||
const content = [...props.element.content]
|
const content = [...props.element.content]
|
||||||
@@ -43,10 +46,13 @@ const weightOptions = [
|
|||||||
<PropSection title="Varsayilan Stil">
|
<PropSection title="Varsayilan Stil">
|
||||||
<PropTextStyleGroup
|
<PropTextStyleGroup
|
||||||
:font-size="element.style.fontSize ?? 11"
|
:font-size="element.style.fontSize ?? 11"
|
||||||
|
:font-weight="element.style.fontWeight ?? 'normal'"
|
||||||
|
:font-family="element.style.fontFamily"
|
||||||
:color="element.style.color ?? '#000000'"
|
:color="element.style.color ?? '#000000'"
|
||||||
:align="element.style.align ?? 'left'"
|
:align="element.style.align ?? 'left'"
|
||||||
:show-weight="false"
|
|
||||||
@update:font-size="(v) => updateStyle('fontSize', v)"
|
@update:font-size="(v) => updateStyle('fontSize', v)"
|
||||||
|
@update:font-weight="(v) => updateStyle('fontWeight', v)"
|
||||||
|
@update:font-family="(v) => updateStyle('fontFamily', v)"
|
||||||
@update:color="(v) => updateStyle('color', v)"
|
@update:color="(v) => updateStyle('color', v)"
|
||||||
@update:align="(v) => updateStyle('align', v)"
|
@update:align="(v) => updateStyle('align', v)"
|
||||||
/>
|
/>
|
||||||
@@ -79,6 +85,15 @@ const weightOptions = [
|
|||||||
@input="(e) => updateSpan(idx, { text: (e.target as HTMLInputElement).value })"
|
@input="(e) => updateSpan(idx, { text: (e.target as HTMLInputElement).value })"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<PropFieldSelect
|
||||||
|
label="Binding"
|
||||||
|
:model-value="span.binding?.path ?? ''"
|
||||||
|
:fields="schemaStore.scalarFields"
|
||||||
|
:allow-empty="true"
|
||||||
|
empty-label="Yok (statik)"
|
||||||
|
data-tip="Span'in baglanacagi veri alani"
|
||||||
|
@update:model-value="(v) => updateSpan(idx, { binding: v ? { type: 'scalar', path: v } : undefined })"
|
||||||
|
/>
|
||||||
<div class="prop-row" data-tip="Span yazi boyutu — bos birakilirsa varsayilan kullanilir">
|
<div class="prop-row" data-tip="Span yazi boyutu — bos birakilirsa varsayilan kullanilir">
|
||||||
<label class="prop-label">Boyut</label>
|
<label class="prop-label">Boyut</label>
|
||||||
<input
|
<input
|
||||||
@@ -109,6 +124,13 @@ const weightOptions = [
|
|||||||
data-tip="Span metin rengi"
|
data-tip="Span metin rengi"
|
||||||
@update:model-value="(v) => updateSpanStyle(idx, 'color', v)"
|
@update:model-value="(v) => updateSpanStyle(idx, 'color', v)"
|
||||||
/>
|
/>
|
||||||
|
<PropSelect
|
||||||
|
label="Hizalama"
|
||||||
|
:model-value="(span.style as TextStyle).align ?? ''"
|
||||||
|
:options="[{ value: '', label: 'Varsayilan' }, { value: 'left', label: 'Sol' }, { value: 'center', label: 'Orta' }, { value: 'right', label: 'Sag' }]"
|
||||||
|
data-tip="Span hizalamasi"
|
||||||
|
@update:model-value="(v) => updateSpanStyle(idx, 'align', v || undefined)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</PropSection>
|
</PropSection>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -15,6 +15,12 @@ const shapeOptions = [
|
|||||||
{ value: 'rounded_rectangle', label: 'Yuvarlak Dikdortgen' },
|
{ value: 'rounded_rectangle', label: 'Yuvarlak Dikdortgen' },
|
||||||
{ value: 'ellipse', label: 'Elips' },
|
{ value: 'ellipse', label: 'Elips' },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const borderStyleOptions = [
|
||||||
|
{ value: 'solid', label: 'Duz' },
|
||||||
|
{ value: 'dashed', label: 'Kesikli' },
|
||||||
|
{ value: 'dotted', label: 'Noktali' },
|
||||||
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -46,6 +52,13 @@ const shapeOptions = [
|
|||||||
data-tip="Kenarlik cizgi kalinligi (mm)"
|
data-tip="Kenarlik cizgi kalinligi (mm)"
|
||||||
@update:model-value="(v) => updateStyle('borderWidth', v)"
|
@update:model-value="(v) => updateStyle('borderWidth', v)"
|
||||||
/>
|
/>
|
||||||
|
<PropSelect
|
||||||
|
label="Kenar Stili"
|
||||||
|
:model-value="element.style.borderStyle ?? 'solid'"
|
||||||
|
:options="borderStyleOptions"
|
||||||
|
data-tip="Kenarlik cizgi stili"
|
||||||
|
@update:model-value="(v) => updateStyle('borderStyle', v)"
|
||||||
|
/>
|
||||||
<PropNumberInput
|
<PropNumberInput
|
||||||
v-if="element.shapeType === 'rounded_rectangle'"
|
v-if="element.shapeType === 'rounded_rectangle'"
|
||||||
label="Kose Yuvarlakligi"
|
label="Kose Yuvarlakligi"
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ function updateSize(axis: 'width' | 'height', sv: SizeValue) {
|
|||||||
templateStore.updateElementSize(props.element.id, { [axis]: sv })
|
templateStore.updateElementSize(props.element.id, { [axis]: sv })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateSizeConstraint(key: string, value: number | undefined) {
|
||||||
|
templateStore.updateElementSize(props.element.id, { [key]: value })
|
||||||
|
}
|
||||||
|
|
||||||
function onTypeChange(axis: 'width' | 'height', type: string) {
|
function onTypeChange(axis: 'width' | 'height', type: string) {
|
||||||
if (type === 'auto') updateSize(axis, { type: 'auto' })
|
if (type === 'auto') updateSize(axis, { type: 'auto' })
|
||||||
else if (type === 'fr') updateSize(axis, { type: 'fr', value: 1 })
|
else if (type === 'fr') updateSize(axis, { type: 'fr', value: 1 })
|
||||||
@@ -79,5 +83,49 @@ function onTypeChange(axis: 'width' | 'height', type: string) {
|
|||||||
data-tip="Sabit yukseklik degeri (mm)"
|
data-tip="Sabit yukseklik degeri (mm)"
|
||||||
@update:model-value="(v) => updateSize('height', { type: 'fixed', value: v })"
|
@update:model-value="(v) => updateSize('height', { type: 'fixed', value: v })"
|
||||||
/>
|
/>
|
||||||
|
<PropNumberInput
|
||||||
|
v-if="element.size.height.type === 'fr'"
|
||||||
|
label="fr"
|
||||||
|
:model-value="(element.size.height as any).value"
|
||||||
|
:step="1"
|
||||||
|
:min="1"
|
||||||
|
data-tip="Kalan alani oransal doldurma degeri"
|
||||||
|
@update:model-value="(v) => updateSize('height', { type: 'fr', value: v })"
|
||||||
|
/>
|
||||||
|
</PropSection>
|
||||||
|
|
||||||
|
<PropSection title="Min / Max">
|
||||||
|
<PropNumberInput
|
||||||
|
label="Min Gen."
|
||||||
|
:model-value="element.size.minWidth ?? 0"
|
||||||
|
:step="1"
|
||||||
|
:min="0"
|
||||||
|
data-tip="Minimum genislik (mm) — 0 = sinir yok"
|
||||||
|
@update:model-value="(v) => updateSizeConstraint('minWidth', v > 0 ? v : undefined)"
|
||||||
|
/>
|
||||||
|
<PropNumberInput
|
||||||
|
label="Max Gen."
|
||||||
|
:model-value="element.size.maxWidth ?? 0"
|
||||||
|
:step="1"
|
||||||
|
:min="0"
|
||||||
|
data-tip="Maksimum genislik (mm) — 0 = sinir yok"
|
||||||
|
@update:model-value="(v) => updateSizeConstraint('maxWidth', v > 0 ? v : undefined)"
|
||||||
|
/>
|
||||||
|
<PropNumberInput
|
||||||
|
label="Min Yuk."
|
||||||
|
:model-value="element.size.minHeight ?? 0"
|
||||||
|
:step="1"
|
||||||
|
:min="0"
|
||||||
|
data-tip="Minimum yukseklik (mm) — 0 = sinir yok"
|
||||||
|
@update:model-value="(v) => updateSizeConstraint('minHeight', v > 0 ? v : undefined)"
|
||||||
|
/>
|
||||||
|
<PropNumberInput
|
||||||
|
label="Max Yuk."
|
||||||
|
:model-value="element.size.maxHeight ?? 0"
|
||||||
|
:step="1"
|
||||||
|
:min="0"
|
||||||
|
data-tip="Maksimum yukseklik (mm) — 0 = sinir yok"
|
||||||
|
@update:model-value="(v) => updateSizeConstraint('maxHeight', v > 0 ? v : undefined)"
|
||||||
|
/>
|
||||||
</PropSection>
|
</PropSection>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
import { usePropertyUpdate } from '../../composables/usePropertyUpdate'
|
||||||
|
import { useSchemaStore } from '../../stores/schema'
|
||||||
import PropSection from './shared/PropSection.vue'
|
import PropSection from './shared/PropSection.vue'
|
||||||
|
import PropFieldSelect from './shared/PropFieldSelect.vue'
|
||||||
import PropTextStyleGroup from './shared/PropTextStyleGroup.vue'
|
import PropTextStyleGroup from './shared/PropTextStyleGroup.vue'
|
||||||
import type { StaticTextElement, TextStyle, TemplateElement } from '../../core/types'
|
import type { StaticTextElement, TextElement, TextStyle, TemplateElement } from '../../core/types'
|
||||||
import '../../styles/properties.css'
|
import '../../styles/properties.css'
|
||||||
|
|
||||||
const props = defineProps<{ element: TemplateElement }>()
|
const props = defineProps<{ element: TemplateElement }>()
|
||||||
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
const { update, updateStyle } = usePropertyUpdate(() => props.element)
|
||||||
|
const schemaStore = useSchemaStore()
|
||||||
const style = () => props.element.style as TextStyle
|
const style = () => props.element.style as TextStyle
|
||||||
|
|
||||||
|
const isText = computed(() => props.element.type === 'text')
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PropSection title="Metin Stili">
|
<PropSection title="Metin">
|
||||||
<div v-if="element.type === 'static_text'" class="prop-row" data-tip="Sabit metin icerigi">
|
<div v-if="element.type === 'static_text'" class="prop-row" data-tip="Sabit metin icerigi">
|
||||||
<label class="prop-label">Metin</label>
|
<label class="prop-label">Metin</label>
|
||||||
<input
|
<input
|
||||||
@@ -21,13 +27,38 @@ const style = () => props.element.style as TextStyle
|
|||||||
@input="(e) => update({ content: (e.target as HTMLInputElement).value } as any)"
|
@input="(e) => update({ content: (e.target as HTMLInputElement).value } as any)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template v-if="isText">
|
||||||
|
<PropFieldSelect
|
||||||
|
label="Veri Alani"
|
||||||
|
:model-value="(element as TextElement).binding?.path ?? ''"
|
||||||
|
:fields="schemaStore.scalarFields"
|
||||||
|
data-tip="Metnin baglanacagi veri alani"
|
||||||
|
@update:model-value="(v) => update({ binding: { type: 'scalar', path: v } } as any)"
|
||||||
|
/>
|
||||||
|
<div class="prop-row" data-tip="Veri alaninin onune eklenecek sabit metin">
|
||||||
|
<label class="prop-label">Ön Ek</label>
|
||||||
|
<input
|
||||||
|
class="prop-input"
|
||||||
|
type="text"
|
||||||
|
:value="(element as TextElement).content ?? ''"
|
||||||
|
placeholder="ör: Fatura No: "
|
||||||
|
@input="(e) => update({ content: (e.target as HTMLInputElement).value || undefined } as any)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</PropSection>
|
||||||
|
|
||||||
|
<PropSection title="Metin Stili">
|
||||||
<PropTextStyleGroup
|
<PropTextStyleGroup
|
||||||
:font-size="style().fontSize ?? 11"
|
:font-size="style().fontSize ?? 11"
|
||||||
:font-weight="style().fontWeight ?? 'normal'"
|
:font-weight="style().fontWeight ?? 'normal'"
|
||||||
|
:font-family="style().fontFamily"
|
||||||
:color="style().color ?? '#000000'"
|
:color="style().color ?? '#000000'"
|
||||||
:align="style().align ?? 'left'"
|
:align="style().align ?? 'left'"
|
||||||
@update:font-size="(v) => updateStyle('fontSize', v)"
|
@update:font-size="(v) => updateStyle('fontSize', v)"
|
||||||
@update:font-weight="(v) => updateStyle('fontWeight', v)"
|
@update:font-weight="(v) => updateStyle('fontWeight', v)"
|
||||||
|
@update:font-family="(v) => updateStyle('fontFamily', v)"
|
||||||
@update:color="(v) => updateStyle('color', v)"
|
@update:color="(v) => updateStyle('color', v)"
|
||||||
@update:align="(v) => updateStyle('align', v)"
|
@update:align="(v) => updateStyle('align', v)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
84
frontend/src/components/properties/shared/PropCondition.vue
Normal file
84
frontend/src/components/properties/shared/PropCondition.vue
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useSchemaStore } from '../../../stores/schema'
|
||||||
|
import PropFieldSelect from './PropFieldSelect.vue'
|
||||||
|
import PropSelect from './PropSelect.vue'
|
||||||
|
import PropSection from './PropSection.vue'
|
||||||
|
import type { Condition } from '../../../core/types'
|
||||||
|
import '../../../styles/properties.css'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
condition?: Condition
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:condition': [value: Condition | undefined]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const schemaStore = useSchemaStore()
|
||||||
|
|
||||||
|
const enabled = computed(() => !!props.condition)
|
||||||
|
|
||||||
|
const operatorOptions = [
|
||||||
|
{ value: 'eq', label: '= Esit' },
|
||||||
|
{ value: 'neq', label: '≠ Esit Degil' },
|
||||||
|
{ value: 'gt', label: '> Buyuk' },
|
||||||
|
{ value: 'gte', label: '>= Buyuk Esit' },
|
||||||
|
{ value: 'lt', label: '< Kucuk' },
|
||||||
|
{ value: 'lte', label: '<= Kucuk Esit' },
|
||||||
|
{ value: 'truthy', label: 'Dolu (truthy)' },
|
||||||
|
{ value: 'falsy', label: 'Bos (falsy)' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const needsValue = computed(() => {
|
||||||
|
const op = props.condition?.operator
|
||||||
|
return op && op !== 'truthy' && op !== 'falsy'
|
||||||
|
})
|
||||||
|
|
||||||
|
function toggle(on: boolean) {
|
||||||
|
if (on) {
|
||||||
|
emit('update:condition', { path: '', operator: 'truthy' })
|
||||||
|
} else {
|
||||||
|
emit('update:condition', undefined)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateField(key: keyof Condition, value: unknown) {
|
||||||
|
emit('update:condition', { ...props.condition!, [key]: value })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PropSection title="Kosullu Gosterim">
|
||||||
|
<div class="prop-row" data-tip="Elemani belirli bir kosulla goster/gizle">
|
||||||
|
<label class="prop-label">Aktif</label>
|
||||||
|
<input type="checkbox" :checked="enabled" @change="toggle(($event.target as HTMLInputElement).checked)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-if="enabled">
|
||||||
|
<PropFieldSelect
|
||||||
|
label="Alan"
|
||||||
|
:model-value="condition!.path"
|
||||||
|
:fields="schemaStore.scalarFields"
|
||||||
|
data-tip="Kosulun degerlendirilecegi veri alani"
|
||||||
|
@update:model-value="(v) => updateField('path', v)"
|
||||||
|
/>
|
||||||
|
<PropSelect
|
||||||
|
label="Operator"
|
||||||
|
:model-value="condition!.operator"
|
||||||
|
:options="operatorOptions"
|
||||||
|
data-tip="Karsilastirma operatoru"
|
||||||
|
@update:model-value="(v) => updateField('operator', v)"
|
||||||
|
/>
|
||||||
|
<div v-if="needsValue" class="prop-row" data-tip="Karsilastirilacak deger">
|
||||||
|
<label class="prop-label">Deger</label>
|
||||||
|
<input
|
||||||
|
class="prop-input"
|
||||||
|
type="text"
|
||||||
|
:value="condition!.value ?? ''"
|
||||||
|
@input="(e) => updateField('value', (e.target as HTMLInputElement).value)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</PropSection>
|
||||||
|
</template>
|
||||||
@@ -1,26 +1,36 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { useTemplateStore } from '../../../stores/template'
|
||||||
import PropNumberInput from './PropNumberInput.vue'
|
import PropNumberInput from './PropNumberInput.vue'
|
||||||
import PropColorInput from './PropColorInput.vue'
|
import PropColorInput from './PropColorInput.vue'
|
||||||
import PropSelect from './PropSelect.vue'
|
import PropSelect from './PropSelect.vue'
|
||||||
|
|
||||||
withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
fontSize: number
|
fontSize: number
|
||||||
fontWeight?: string
|
fontWeight?: string
|
||||||
|
fontFamily?: string
|
||||||
color: string
|
color: string
|
||||||
align: string
|
align: string
|
||||||
showWeight?: boolean
|
showWeight?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{ fontWeight: 'normal', showWeight: true },
|
{ fontWeight: 'normal', fontFamily: undefined, showWeight: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
'update:fontSize': [value: number]
|
'update:fontSize': [value: number]
|
||||||
'update:fontWeight': [value: string]
|
'update:fontWeight': [value: string]
|
||||||
|
'update:fontFamily': [value: string | undefined]
|
||||||
'update:color': [value: string]
|
'update:color': [value: string]
|
||||||
'update:align': [value: string]
|
'update:align': [value: string]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const templateStore = useTemplateStore()
|
||||||
|
|
||||||
|
const fontOptions = computed(() =>
|
||||||
|
templateStore.template.fonts.map((f) => ({ value: f, label: f })),
|
||||||
|
)
|
||||||
|
|
||||||
const weightOptions = [
|
const weightOptions = [
|
||||||
{ value: 'normal', label: 'Normal' },
|
{ value: 'normal', label: 'Normal' },
|
||||||
{ value: 'bold', label: 'Kalin' },
|
{ value: 'bold', label: 'Kalin' },
|
||||||
@@ -34,6 +44,14 @@ const alignOptions = [
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<PropSelect
|
||||||
|
v-if="fontOptions.length > 1"
|
||||||
|
label="Font"
|
||||||
|
:model-value="fontFamily ?? fontOptions[0]?.value ?? ''"
|
||||||
|
:options="fontOptions"
|
||||||
|
data-tip="Yazi tipi ailesi"
|
||||||
|
@update:model-value="$emit('update:fontFamily', $event)"
|
||||||
|
/>
|
||||||
<PropNumberInput
|
<PropNumberInput
|
||||||
label="Boyut (pt)"
|
label="Boyut (pt)"
|
||||||
:model-value="fontSize"
|
:model-value="fontSize"
|
||||||
|
|||||||
@@ -81,7 +81,25 @@ const emit = defineEmits<{
|
|||||||
×
|
×
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span class="ts-clbl">Zebra</span>
|
<span class="ts-clbl">Tek</span>
|
||||||
|
</div>
|
||||||
|
<div class="ts-color-item" data-tip="Zebra satir rengi — cift satirlar">
|
||||||
|
<div class="ts-swatch-wrap">
|
||||||
|
<input
|
||||||
|
class="ts-swatch"
|
||||||
|
type="color"
|
||||||
|
:value="style.zebraEven ?? '#ffffff'"
|
||||||
|
@input="(e) => emit('update:style', 'zebraEven', (e.target as HTMLInputElement).value)"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="style.zebraEven"
|
||||||
|
class="ts-swatch-clr"
|
||||||
|
@click="emit('update:style', 'zebraEven', undefined)"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<span class="ts-clbl">Cift</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -250,11 +250,22 @@ export interface ChartLabels {
|
|||||||
color?: string
|
color?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ChartReferenceLine {
|
||||||
|
categoryIndex: number
|
||||||
|
color?: string
|
||||||
|
width?: number
|
||||||
|
label?: string
|
||||||
|
dash?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChartAxis {
|
export interface ChartAxis {
|
||||||
xLabel?: string
|
xLabel?: string
|
||||||
yLabel?: string
|
yLabel?: string
|
||||||
showGrid?: boolean
|
showGrid?: boolean
|
||||||
gridColor?: string
|
gridColor?: string
|
||||||
|
showVerticalGrid?: boolean
|
||||||
|
verticalGridColor?: string
|
||||||
|
referenceLines?: ChartReferenceLine[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChartStyle {
|
export interface ChartStyle {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
4
justfile
4
justfile
@@ -55,6 +55,10 @@ test-visual-editor:
|
|||||||
test-visual: visual-refs
|
test-visual: visual-refs
|
||||||
cd frontend && bun run test:visual
|
cd frontend && bun run test:visual
|
||||||
|
|
||||||
|
# Visual snapshot'lari guncelle (UI degisikliklerinden sonra)
|
||||||
|
update-snapshots: visual-refs
|
||||||
|
cd frontend && bun run test:visual -- --update-snapshots
|
||||||
|
|
||||||
# Tum testler (Rust + frontend unit + visual)
|
# Tum testler (Rust + frontend unit + visual)
|
||||||
test-all: test-rust test-front test-visual
|
test-all: test-rust test-front test-visual
|
||||||
|
|
||||||
|
|||||||
@@ -129,10 +129,23 @@ pub struct LineChartLayout {
|
|||||||
pub show_labels: bool,
|
pub show_labels: bool,
|
||||||
pub label_font: f64,
|
pub label_font: f64,
|
||||||
pub label_color: String,
|
pub label_color: String,
|
||||||
|
pub smooth: bool,
|
||||||
/// X axis line endpoints
|
/// X axis line endpoints
|
||||||
pub x_axis_y: f64,
|
pub x_axis_y: f64,
|
||||||
pub x_axis_x1: f64,
|
pub x_axis_x1: f64,
|
||||||
pub x_axis_x2: f64,
|
pub x_axis_x2: f64,
|
||||||
|
/// Vertical reference lines
|
||||||
|
pub ref_lines: Vec<RefLineLayout>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RefLineLayout {
|
||||||
|
pub x: f64,
|
||||||
|
pub y1: f64,
|
||||||
|
pub y2: f64,
|
||||||
|
pub color: String,
|
||||||
|
pub width: f64,
|
||||||
|
pub dash: bool,
|
||||||
|
pub label: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PieSlice {
|
pub struct PieSlice {
|
||||||
@@ -223,6 +236,10 @@ pub trait ChartDataSource {
|
|||||||
fn inner_radius(&self) -> Option<f64>;
|
fn inner_radius(&self) -> Option<f64>;
|
||||||
fn show_points(&self) -> Option<bool>;
|
fn show_points(&self) -> Option<bool>;
|
||||||
fn line_width(&self) -> Option<f64>;
|
fn line_width(&self) -> Option<f64>;
|
||||||
|
fn curve_type(&self) -> Option<&str>;
|
||||||
|
fn reference_lines(&self) -> &[dreport_core::models::ChartReferenceLine];
|
||||||
|
fn show_vertical_grid(&self) -> bool;
|
||||||
|
fn vertical_grid_color(&self) -> Option<&str>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -314,6 +331,18 @@ impl ChartDataSource for crate::data_resolve::ResolvedChartData {
|
|||||||
fn line_width(&self) -> Option<f64> {
|
fn line_width(&self) -> Option<f64> {
|
||||||
self.style.line_width
|
self.style.line_width
|
||||||
}
|
}
|
||||||
|
fn curve_type(&self) -> Option<&str> {
|
||||||
|
self.style.curve_type.as_deref()
|
||||||
|
}
|
||||||
|
fn reference_lines(&self) -> &[dreport_core::models::ChartReferenceLine] {
|
||||||
|
self.axis.as_ref().map_or(&[], |a| &a.reference_lines)
|
||||||
|
}
|
||||||
|
fn show_vertical_grid(&self) -> bool {
|
||||||
|
self.axis.as_ref().and_then(|a| a.show_vertical_grid).unwrap_or(true)
|
||||||
|
}
|
||||||
|
fn vertical_grid_color(&self) -> Option<&str> {
|
||||||
|
self.axis.as_ref().and_then(|a| a.vertical_grid_color.as_deref())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -403,6 +432,18 @@ impl ChartDataSource for crate::ChartRenderData {
|
|||||||
fn line_width(&self) -> Option<f64> {
|
fn line_width(&self) -> Option<f64> {
|
||||||
self.line_width
|
self.line_width
|
||||||
}
|
}
|
||||||
|
fn curve_type(&self) -> Option<&str> {
|
||||||
|
self.curve_type.as_deref()
|
||||||
|
}
|
||||||
|
fn reference_lines(&self) -> &[dreport_core::models::ChartReferenceLine] {
|
||||||
|
&self.reference_lines
|
||||||
|
}
|
||||||
|
fn show_vertical_grid(&self) -> bool {
|
||||||
|
self.show_vertical_grid
|
||||||
|
}
|
||||||
|
fn vertical_grid_color(&self) -> Option<&str> {
|
||||||
|
self.vertical_grid_color.as_deref()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -693,22 +734,14 @@ pub fn compute_x_labels_line(
|
|||||||
rotate_angle: 0.0,
|
rotate_angle: 0.0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let spacing = if n_cats == 1 {
|
let step = pw / n_cats as f64;
|
||||||
pw
|
|
||||||
} else {
|
|
||||||
pw / (n_cats - 1) as f64
|
|
||||||
};
|
|
||||||
let max_label_len = categories.iter().map(|c| c.len()).max().unwrap_or(0);
|
let max_label_len = categories.iter().map(|c| c.len()).max().unwrap_or(0);
|
||||||
let rotate_angle = compute_label_rotation(max_label_len, spacing);
|
let rotate_angle = compute_label_rotation(max_label_len, step);
|
||||||
let labels = categories
|
let labels = categories
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(ci, cat)| {
|
.map(|(ci, cat)| {
|
||||||
let x = if n_cats == 1 {
|
let x = px + step / 2.0 + ci as f64 * step;
|
||||||
px + pw / 2.0
|
|
||||||
} else {
|
|
||||||
px + ci as f64 * pw / (n_cats - 1) as f64
|
|
||||||
};
|
|
||||||
XLabel {
|
XLabel {
|
||||||
text: cat.clone(),
|
text: cat.clone(),
|
||||||
x,
|
x,
|
||||||
@@ -833,6 +866,11 @@ pub fn compute_line_layout(data: &dyn ChartDataSource, cl: &ChartLayout) -> Line
|
|||||||
let show_labels = data.show_labels();
|
let show_labels = data.show_labels();
|
||||||
let label_font = data.label_font_size().unwrap_or(2.2);
|
let label_font = data.label_font_size().unwrap_or(2.2);
|
||||||
let label_color = data.label_color().unwrap_or("#333").to_string();
|
let label_color = data.label_color().unwrap_or("#333").to_string();
|
||||||
|
let smooth = data.curve_type() == Some("smooth");
|
||||||
|
|
||||||
|
// Slot-based positioning: each category gets a slot, point centered in slot
|
||||||
|
// This adds padding on left/right so first/last points don't touch axes
|
||||||
|
let step = if n_cats > 0 { pw / n_cats as f64 } else { pw };
|
||||||
|
|
||||||
let series = (0..data.series_count())
|
let series = (0..data.series_count())
|
||||||
.map(|si| {
|
.map(|si| {
|
||||||
@@ -841,11 +879,7 @@ pub fn compute_line_layout(data: &dyn ChartDataSource, cl: &ChartLayout) -> Line
|
|||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(ci, val)| {
|
.map(|(ci, val)| {
|
||||||
let x = if n_cats == 1 {
|
let x = px + step / 2.0 + ci as f64 * step;
|
||||||
px + pw / 2.0
|
|
||||||
} else {
|
|
||||||
px + ci as f64 * pw / (n_cats - 1) as f64
|
|
||||||
};
|
|
||||||
let y = py + ph - ((val - min_val) / range) * ph;
|
let y = py + ph - ((val - min_val) / range) * ph;
|
||||||
LinePoint { x, y, value: *val }
|
LinePoint { x, y, value: *val }
|
||||||
})
|
})
|
||||||
@@ -859,6 +893,42 @@ pub fn compute_line_layout(data: &dyn ChartDataSource, cl: &ChartLayout) -> Line
|
|||||||
|
|
||||||
let x_labels = compute_x_labels_line(data.categories(), px, py + ph, pw);
|
let x_labels = compute_x_labels_line(data.categories(), px, py + ph, pw);
|
||||||
|
|
||||||
|
// Vertical grid lines at each category
|
||||||
|
let vgrid_color = data.vertical_grid_color().unwrap_or("#E5E7EB").to_string();
|
||||||
|
let mut ref_lines: Vec<RefLineLayout> = if data.show_vertical_grid() {
|
||||||
|
(0..n_cats).map(|ci| {
|
||||||
|
let x = px + step / 2.0 + ci as f64 * step;
|
||||||
|
RefLineLayout {
|
||||||
|
x,
|
||||||
|
y1: py,
|
||||||
|
y2: py + ph,
|
||||||
|
color: vgrid_color.clone(),
|
||||||
|
width: 0.15,
|
||||||
|
dash: false,
|
||||||
|
label: None,
|
||||||
|
}
|
||||||
|
}).collect()
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Explicit reference lines (overlay on top of grid)
|
||||||
|
for rl in data.reference_lines() {
|
||||||
|
if rl.category_index >= n_cats {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let x = px + step / 2.0 + rl.category_index as f64 * step;
|
||||||
|
ref_lines.push(RefLineLayout {
|
||||||
|
x,
|
||||||
|
y1: py,
|
||||||
|
y2: py + ph,
|
||||||
|
color: rl.color.clone().unwrap_or_else(|| "#9CA3AF".to_string()),
|
||||||
|
width: rl.width.unwrap_or(0.3),
|
||||||
|
dash: rl.dash.unwrap_or(true),
|
||||||
|
label: rl.label.clone(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
LineChartLayout {
|
LineChartLayout {
|
||||||
min_val,
|
min_val,
|
||||||
max_val,
|
max_val,
|
||||||
@@ -870,9 +940,11 @@ pub fn compute_line_layout(data: &dyn ChartDataSource, cl: &ChartLayout) -> Line
|
|||||||
show_labels,
|
show_labels,
|
||||||
label_font,
|
label_font,
|
||||||
label_color,
|
label_color,
|
||||||
|
smooth,
|
||||||
x_axis_y: py + ph,
|
x_axis_y: py + ph,
|
||||||
x_axis_x1: px,
|
x_axis_x1: px,
|
||||||
x_axis_x2: px + pw,
|
x_axis_x2: px + pw,
|
||||||
|
ref_lines,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,14 +121,13 @@ fn render_line(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
|
|||||||
// Y axis
|
// Y axis
|
||||||
render_y_axis_svg(svg, &ll.y_axis);
|
render_y_axis_svg(svg, &ll.y_axis);
|
||||||
|
|
||||||
|
let mut label_texts = String::new();
|
||||||
|
|
||||||
for series_layout in &ll.series {
|
for series_layout in &ll.series {
|
||||||
let color = color_at(&cl.palette, series_layout.color_idx);
|
let color = color_at(&cl.palette, series_layout.color_idx);
|
||||||
let mut points = String::new();
|
|
||||||
let mut point_circles = String::new();
|
let mut point_circles = String::new();
|
||||||
|
|
||||||
for pt in &series_layout.points {
|
for pt in &series_layout.points {
|
||||||
write!(points, "{:.2},{:.2} ", pt.x, pt.y).unwrap();
|
|
||||||
|
|
||||||
if ll.show_points {
|
if ll.show_points {
|
||||||
write!(
|
write!(
|
||||||
point_circles,
|
point_circles,
|
||||||
@@ -139,19 +138,75 @@ fn render_line(svg: &mut String, data: &ResolvedChartData, cl: &ChartLayout) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ll.show_labels {
|
if ll.show_labels {
|
||||||
svg_text(svg, pt.x, pt.y - 1.5, ll.label_font, &ll.label_color, SvgAnchor::Middle, &format_value(pt.value));
|
svg_text(&mut label_texts, pt.x, pt.y - 1.5, ll.label_font, &ll.label_color, SvgAnchor::Middle, &format_value(pt.value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(
|
if ll.smooth && series_layout.points.len() >= 2 {
|
||||||
svg,
|
// Catmull-Rom → cubic bezier smooth curve
|
||||||
r##"<polyline points="{}" fill="none" stroke="{}" stroke-width="{:.2}" stroke-linejoin="round" stroke-linecap="round"/>"##,
|
let pts = &series_layout.points;
|
||||||
points.trim(), color, ll.line_width
|
let mut d = format!("M{:.2},{:.2}", pts[0].x, pts[0].y);
|
||||||
)
|
for i in 0..pts.len() - 1 {
|
||||||
.unwrap();
|
let p0 = if i > 0 { &pts[i - 1] } else { &pts[i] };
|
||||||
|
let p1 = &pts[i];
|
||||||
|
let p2 = &pts[i + 1];
|
||||||
|
let p3 = if i + 2 < pts.len() { &pts[i + 2] } else { &pts[i + 1] };
|
||||||
|
|
||||||
|
let cp1x = p1.x + (p2.x - p0.x) / 6.0;
|
||||||
|
let cp1y = p1.y + (p2.y - p0.y) / 6.0;
|
||||||
|
let cp2x = p2.x - (p3.x - p1.x) / 6.0;
|
||||||
|
let cp2y = p2.y - (p3.y - p1.y) / 6.0;
|
||||||
|
|
||||||
|
write!(d, " C{:.2},{:.2} {:.2},{:.2} {:.2},{:.2}",
|
||||||
|
cp1x, cp1y, cp2x, cp2y, p2.x, p2.y
|
||||||
|
).unwrap();
|
||||||
|
}
|
||||||
|
write!(
|
||||||
|
svg,
|
||||||
|
r##"<path d="{}" fill="none" stroke="{}" stroke-width="{:.2}" stroke-linejoin="round" stroke-linecap="round"/>"##,
|
||||||
|
d, color, ll.line_width
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
let mut points = String::new();
|
||||||
|
for pt in &series_layout.points {
|
||||||
|
write!(points, "{:.2},{:.2} ", pt.x, pt.y).unwrap();
|
||||||
|
}
|
||||||
|
write!(
|
||||||
|
svg,
|
||||||
|
r##"<polyline points="{}" fill="none" stroke="{}" stroke-width="{:.2}" stroke-linejoin="round" stroke-linecap="round"/>"##,
|
||||||
|
points.trim(), color, ll.line_width
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
svg.push_str(&point_circles);
|
svg.push_str(&point_circles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Data labels (rendered after lines/points so they appear on top)
|
||||||
|
svg.push_str(&label_texts);
|
||||||
|
|
||||||
|
// Reference lines (vertical)
|
||||||
|
for rl in &ll.ref_lines {
|
||||||
|
if rl.dash {
|
||||||
|
write!(
|
||||||
|
svg,
|
||||||
|
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="{}" stroke-width="{:.2}" stroke-dasharray="1.5,1"/>"##,
|
||||||
|
rl.x, rl.y1, rl.x, rl.y2, rl.color, rl.width
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
} else {
|
||||||
|
write!(
|
||||||
|
svg,
|
||||||
|
r##"<line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="{}" stroke-width="{:.2}"/>"##,
|
||||||
|
rl.x, rl.y1, rl.x, rl.y2, rl.color, rl.width
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
if let Some(ref label) = rl.label {
|
||||||
|
svg_text(svg, rl.x, rl.y1 - 1.0, 2.0, &rl.color, SvgAnchor::Middle, label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// X axis labels
|
// X axis labels
|
||||||
render_x_labels_svg(svg, &ll.x_labels);
|
render_x_labels_svg(svg, &ll.x_labels);
|
||||||
|
|
||||||
@@ -560,6 +615,9 @@ mod tests {
|
|||||||
y_label: Some("Revenue".to_string()),
|
y_label: Some("Revenue".to_string()),
|
||||||
show_grid: None,
|
show_grid: None,
|
||||||
grid_color: None,
|
grid_color: None,
|
||||||
|
show_vertical_grid: None,
|
||||||
|
vertical_grid_color: None,
|
||||||
|
reference_lines: vec![],
|
||||||
});
|
});
|
||||||
let svg = render_svg(&data, 100.0, 60.0);
|
let svg = render_svg(&data, 100.0, 60.0);
|
||||||
|
|
||||||
@@ -575,6 +633,9 @@ mod tests {
|
|||||||
y_label: Some("Y Label".to_string()),
|
y_label: Some("Y Label".to_string()),
|
||||||
show_grid: None,
|
show_grid: None,
|
||||||
grid_color: None,
|
grid_color: None,
|
||||||
|
show_vertical_grid: None,
|
||||||
|
vertical_grid_color: None,
|
||||||
|
reference_lines: vec![],
|
||||||
});
|
});
|
||||||
let svg = render_svg(&data, 80.0, 80.0);
|
let svg = render_svg(&data, 80.0, 80.0);
|
||||||
|
|
||||||
|
|||||||
@@ -161,8 +161,21 @@ pub struct ChartRenderData {
|
|||||||
// Title align
|
// Title align
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub title_align: Option<String>,
|
pub title_align: Option<String>,
|
||||||
|
// Curve type for line charts
|
||||||
|
#[serde(default)]
|
||||||
|
pub curve_type: Option<String>,
|
||||||
|
// Vertical reference lines
|
||||||
|
#[serde(default)]
|
||||||
|
pub reference_lines: Vec<dreport_core::models::ChartReferenceLine>,
|
||||||
|
// Vertical grid
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub show_vertical_grid: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub vertical_grid_color: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_true() -> bool { true }
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ChartSeriesData {
|
pub struct ChartSeriesData {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -340,6 +353,10 @@ impl From<&data_resolve::ResolvedChartData> for ChartRenderData {
|
|||||||
legend_font_size: cd.legend.as_ref().and_then(|l| l.font_size),
|
legend_font_size: cd.legend.as_ref().and_then(|l| l.font_size),
|
||||||
x_label: cd.axis.as_ref().and_then(|a| a.x_label.clone()),
|
x_label: cd.axis.as_ref().and_then(|a| a.x_label.clone()),
|
||||||
y_label: cd.axis.as_ref().and_then(|a| a.y_label.clone()),
|
y_label: cd.axis.as_ref().and_then(|a| a.y_label.clone()),
|
||||||
|
curve_type: cd.style.curve_type.clone(),
|
||||||
|
reference_lines: cd.axis.as_ref().map_or_else(Vec::new, |a| a.reference_lines.clone()),
|
||||||
|
show_vertical_grid: cd.axis.as_ref().and_then(|a| a.show_vertical_grid).unwrap_or(true),
|
||||||
|
vertical_grid_color: cd.axis.as_ref().and_then(|a| a.vertical_grid_color.clone()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1015,11 +1015,33 @@ fn render_chart(
|
|||||||
}));
|
}));
|
||||||
let path = {
|
let path = {
|
||||||
let mut pb = PathBuilder::new();
|
let mut pb = PathBuilder::new();
|
||||||
for (i, (lx, ly)) in points.iter().enumerate() {
|
if ll.smooth && points.len() >= 2 {
|
||||||
if i == 0 {
|
let pts = &series_layout.points;
|
||||||
pb.move_to(mm(*lx), mm(*ly));
|
pb.move_to(mm(pts[0].x), mm(pts[0].y));
|
||||||
} else {
|
for i in 0..pts.len() - 1 {
|
||||||
pb.line_to(mm(*lx), mm(*ly));
|
let p0 = if i > 0 { &pts[i - 1] } else { &pts[i] };
|
||||||
|
let p1 = &pts[i];
|
||||||
|
let p2 = &pts[i + 1];
|
||||||
|
let p3 = if i + 2 < pts.len() { &pts[i + 2] } else { &pts[i + 1] };
|
||||||
|
|
||||||
|
let cp1x = p1.x + (p2.x - p0.x) / 6.0;
|
||||||
|
let cp1y = p1.y + (p2.y - p0.y) / 6.0;
|
||||||
|
let cp2x = p2.x - (p3.x - p1.x) / 6.0;
|
||||||
|
let cp2y = p2.y - (p3.y - p1.y) / 6.0;
|
||||||
|
|
||||||
|
pb.cubic_to(
|
||||||
|
mm(cp1x), mm(cp1y),
|
||||||
|
mm(cp2x), mm(cp2y),
|
||||||
|
mm(p2.x), mm(p2.y),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (i, (lx, ly)) in points.iter().enumerate() {
|
||||||
|
if i == 0 {
|
||||||
|
pb.move_to(mm(*lx), mm(*ly));
|
||||||
|
} else {
|
||||||
|
pb.line_to(mm(*lx), mm(*ly));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pb.finish()
|
pb.finish()
|
||||||
@@ -1055,6 +1077,32 @@ fn render_chart(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Reference lines (vertical)
|
||||||
|
for rl in &ll.ref_lines {
|
||||||
|
let rl_color = parse_color(&rl.color);
|
||||||
|
chart_line_seg(
|
||||||
|
surface,
|
||||||
|
rl.x,
|
||||||
|
rl.y1,
|
||||||
|
rl.x,
|
||||||
|
rl.y2,
|
||||||
|
rl_color,
|
||||||
|
(rl.width * 2.5) as f32,
|
||||||
|
);
|
||||||
|
if let Some(ref label) = rl.label {
|
||||||
|
chart_text(
|
||||||
|
surface,
|
||||||
|
rl.x,
|
||||||
|
rl.y1 - 1.0,
|
||||||
|
label,
|
||||||
|
2.0,
|
||||||
|
&rl.color,
|
||||||
|
ChartTextAlign::Center,
|
||||||
|
fonts,
|
||||||
|
measurer,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
render_chart_x_labels(surface, &ll.x_labels, fonts, measurer);
|
render_chart_x_labels(surface, &ll.x_labels, fonts, measurer);
|
||||||
let ac = parse_color("#9CA3AF");
|
let ac = parse_color("#9CA3AF");
|
||||||
chart_line_seg(
|
chart_line_seg(
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user