Преглед изворни кода

[axis] allow skipping x values

tags/1.2.0
Prateeksha Singh пре 7 година
родитељ
комит
59ad41427f
14 измењених фајлова са 249 додато и 244 уклоњено
  1. +108
    -85
      dist/frappe-charts.esm.js
  2. +1
    -1
      dist/frappe-charts.min.cjs.js
  3. +1
    -1
      dist/frappe-charts.min.esm.js
  4. +1
    -1
      dist/frappe-charts.min.iife.js
  5. +1
    -1
      dist/frappe-charts.min.iife.js.map
  6. +1
    -1
      docs/assets/js/frappe-charts.min.js
  7. +1
    -1
      docs/assets/js/frappe-charts.min.js.map
  8. +21
    -62
      docs/assets/js/index.js
  9. +3
    -3
      docs/index.html
  10. +42
    -36
      src/js/charts/AxisChart.js
  11. +24
    -21
      src/js/objects/ChartComponents.js
  12. +42
    -9
      src/js/utils/axis-chart-utils.js
  13. +3
    -1
      src/js/utils/constants.js
  14. +0
    -21
      src/js/utils/draw-utils.js

+ 108
- 85
dist/frappe-charts.esm.js Прегледај датотеку

@@ -273,27 +273,6 @@ function equilizeNoOfElements(array1, array2,
return [array1, array2];
}

// let char_width = 8;
// let allowed_space = avgUnitWidth * 1.5;
// let allowed_letters = allowed_space / 8;

// return values.map((value, i) => {
// let space_taken = getStringWidth(value, char_width) + 2;
// if(space_taken > allowed_space) {
// if(isSeries) {
// // Skip some axis lines if X axis is a series
// let skips = 1;
// while((space_taken/skips)*2 > allowed_space) {
// skips++;
// }
// if(i % skips !== 0) {
// return;
// }
// } else {
// value = value.slice(0, allowed_letters-3) + " ...";
// }
// }

const UNIT_ANIM_DUR = 350;
const PATH_ANIM_DUR = 350;
const MARKER_LINE_ANIM_DUR = UNIT_ANIM_DUR;
@@ -411,6 +390,8 @@ const MIN_BAR_PERCENT_HEIGHT = 0.01;
const LINE_CHART_DOT_SIZE = 4;
const DOT_OVERLAY_SIZE_INCR = 4;

const DEFAULT_CHAR_WIDTH = 8;

const AXIS_TICK_LENGTH = 6;
const LABEL_MARGIN = 4;
const FONT_SIZE = 10;
@@ -2283,24 +2264,57 @@ function zeroDataPrep(realData) {
chartType: d.chartType
}
}),
yRegions: [
};

if(realData.yMarkers) {
zeroData.yMarkers = [
{
start: 0,
end: 0,
value: 0,
label: ''
}
],
yMarkers: [
];
}

if(realData.yRegions) {
zeroData.yRegions = [
{
value: 0,
start: 0,
end: 0,
label: ''
}
]
};
];
}

return zeroData;
}

function getShortenedLabels(chartWidth, labels=[], isSeries=true) {
let allowedSpace = chartWidth / labels.length;
let allowedLetters = allowedSpace / DEFAULT_CHAR_WIDTH;

let calcLabels = labels.map((label, i) => {
label += "";
if(label.length > allowedLetters) {

if(!isSeries) {
if(allowedLetters-3 > 0) {
label = label.slice(0, allowedLetters-3) + " ...";
} else {
label = label.slice(0, allowedLetters) + '..';
}
} else {
let multiple = Math.ceil(label.length/allowedLetters);
if(i % multiple !== 0) {
label = "";
}
}
}
return label;
});

return calcLabels;
}

class ChartComponent {
constructor({
layerClass = '',
@@ -2396,23 +2410,23 @@ let componentConfigs = {
layerClass: 'x axis',
makeElements(data) {
return data.positions.map((position, i) =>
xLine(position, data.labels[i], this.constants.height,
xLine(position, data.calcLabels[i], this.constants.height,
{mode: this.constants.mode, pos: this.constants.pos})
);
},

animateElements(newData) {
let newPos = newData.positions;
let newLabels = newData.labels;
let newLabels = newData.calcLabels;
let oldPos = this.oldData.positions;
let oldLabels = this.oldData.labels;
let oldLabels = this.oldData.calcLabels;

[oldPos, newPos] = equilizeNoOfElements(oldPos, newPos);
[oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);

this.render({
positions: oldPos,
labels: newLabels
calcLabels: newLabels
});

return this.store.map((line, i) => {
@@ -2564,23 +2578,24 @@ let componentConfigs = {
makeElements(data) {
let c = this.constants;
this.unitType = 'dot';

this.paths = getPaths(
data.xPositions,
data.yPositions,
c.color,
{
heatline: c.heatline,
regionFill: c.regionFill
},
{
svgDefs: c.svgDefs,
zeroLine: data.zeroLine
}
);
this.paths = {};
if(!c.hideLine) {
this.paths = getPaths(
data.xPositions,
data.yPositions,
c.color,
{
heatline: c.heatline,
regionFill: c.regionFill
},
{
svgDefs: c.svgDefs,
zeroLine: data.zeroLine
}
);
}

this.units = [];

if(!c.hideDots) {
this.units = data.yPositions.map((y, j) => {
return datasetDot(
@@ -2621,8 +2636,10 @@ let componentConfigs = {

let animateElements = [];

animateElements = animateElements.concat(animatePath(
this.paths, newXPos, newYPos, newData.zeroLine));
if(Object.keys(this.paths).length) {
animateElements = animateElements.concat(animatePath(
this.paths, newXPos, newYPos, newData.zeroLine));
}

if(this.units.length) {
this.units.map((dot, i) => {
@@ -2649,24 +2666,29 @@ function getComponent(name, constants, getData) {
class AxisChart extends BaseChart {
constructor(parent, args) {
super(parent, args);
this.isSeries = args.isSeries;
this.valuesOverPoints = args.valuesOverPoints;
this.formatTooltipY = args.formatTooltipY;
this.formatTooltipX = args.formatTooltipX;

this.barOptions = args.barOptions || {};
this.lineOptions = args.lineOptions || {};
this.type = args.type || 'line';

this.xAxisMode = args.xAxisMode || 'span';
this.yAxisMode = args.yAxisMode || 'span';
this.type = args.type || 'line';

this.setup();
}

configure(args) {3;
configure(args) {
super.configure();
this.config.xAxisMode = args.xAxisMode;
this.config.yAxisMode = args.yAxisMode;

args.axisOptions = args.axisOptions || {};
args.tooltipOptions = args.tooltipOptions || {};

this.config.xAxisMode = args.axisOptions.xAxisMode || 'span';
this.config.yAxisMode = args.axisOptions.yAxisMode || 'span';
this.config.xIsSeries = args.axisOptions.xIsSeries || 1;

this.config.formatTooltipX = args.tooltipOptions.formatTooltipX;
this.config.formatTooltipY = args.tooltipOptions.formatTooltipY;

this.config.valuesOverPoints = args.valuesOverPoints;
}

setMargins() {
@@ -2770,7 +2792,7 @@ class AxisChart extends BaseChart {
if(this.data.yMarkers) {
this.state.yMarkers = this.data.yMarkers.map(d => {
d.position = scale(d.value, s.yAxis);
if(!d.label) {
if(!d.label.includes(':')) {
d.label += ': ' + d.value;
}
return d;
@@ -2806,19 +2828,29 @@ class AxisChart extends BaseChart {
[
'yAxis',
{
mode: this.yAxisMode,
mode: this.config.yAxisMode,
width: this.width,
// pos: 'right'
}
},
function() {
return this.state.yAxis;
}.bind(this)
],

[
'xAxis',
{
mode: this.xAxisMode,
mode: this.config.xAxisMode,
height: this.height,
// pos: 'right'
}
},
function() {
let s = this.state;
s.xAxis.calcLabels = getShortenedLabels(this.width,
s.xAxis.labels, this.config.xIsSeries);

return s.xAxis;
}.bind(this)
],

[
@@ -2826,17 +2858,12 @@ class AxisChart extends BaseChart {
{
width: this.width,
pos: 'right'
}
],
];

componentConfigs.map(args => {
args.push(
},
function() {
return this.state[args[0]];
return this.state.yRegions;
}.bind(this)
);
});
],
];

let barDatasets = this.state.datasets.filter(d => d.chartType === 'bar');
let lineDatasets = this.state.datasets.filter(d => d.chartType === 'line');
@@ -2853,7 +2880,7 @@ class AxisChart extends BaseChart {
stacked: this.barOptions.stacked,

// same for all datasets
valuesOverPoints: this.valuesOverPoints,
valuesOverPoints: this.config.valuesOverPoints,
minHeight: this.height * MIN_BAR_PERCENT_HEIGHT,
},
function() {
@@ -2910,9 +2937,10 @@ class AxisChart extends BaseChart {
heatline: this.lineOptions.heatline,
regionFill: this.lineOptions.regionFill,
hideDots: this.lineOptions.hideDots,
hideLine: this.lineOptions.hideLine,

// same for all datasets
valuesOverPoints: this.valuesOverPoints,
valuesOverPoints: this.config.valuesOverPoints,
},
function() {
let s = this.state;
@@ -2937,17 +2965,12 @@ class AxisChart extends BaseChart {
{
width: this.width,
pos: 'right'
}
]
];

markerConfigs.map(args => {
args.push(
},
function() {
return this.state[args[0]];
return this.state.yMarkers;
}.bind(this)
);
});
]
];

componentConfigs = componentConfigs.concat(barsConfigs, lineConfigs, markerConfigs);



+ 1
- 1
dist/frappe-charts.min.cjs.js
Разлика између датотеке није приказан због своје велике величине
Прегледај датотеку


+ 1
- 1
dist/frappe-charts.min.esm.js
Разлика између датотеке није приказан због своје велике величине
Прегледај датотеку


+ 1
- 1
dist/frappe-charts.min.iife.js
Разлика између датотеке није приказан због своје велике величине
Прегледај датотеку


+ 1
- 1
dist/frappe-charts.min.iife.js.map
Разлика између датотеке није приказан због своје велике величине
Прегледај датотеку


+ 1
- 1
docs/assets/js/frappe-charts.min.js
Разлика између датотеке није приказан због своје велике величине
Прегледај датотеку


+ 1
- 1
docs/assets/js/frappe-charts.min.js.map
Разлика између датотеке није приказан због своје велике величине
Прегледај датотеку


+ 21
- 62
docs/assets/js/index.js Прегледај датотеку

@@ -63,7 +63,7 @@ let c1 = document.querySelector("#chart-composite-1");
let c2 = document.querySelector("#chart-composite-2");

let bar_composite_chart = new Chart (c1, {
title: "Fireball/Bolide Events - Yearly (more than 5 reports)",
title: "Fireball/Bolide Events - Yearly (reported)",
data: bar_composite_data,
type: 'line',
height: 180,
@@ -179,9 +179,11 @@ let type_chart = new Chart("#chart-types", {
isNavigable: 1,
barOptions: {
stacked: 1
},
tooltipOptions: {
formatTooltipX: d => (d + '').toUpperCase(),
formatTooltipY: d => d + ' pts'
}
// formatTooltipX: d => (d + '').toUpperCase(),
// formatTooltipY: d => d + ' pts'
});

Array.prototype.slice.call(
@@ -222,7 +224,7 @@ let trends_data = {
]
};

let plot_chart_args = {
let plotChartArgs = {
title: "Mean Total Sunspot Count - Yearly",
data: trends_data,
type: 'line',
@@ -233,11 +235,14 @@ let plot_chart_args = {
hideDots: 1,
heatline: 1,
},
xAxisMode: 'tick',
yAxisMode: 'span'
axisOptions: {
xAxisMode: 'tick',
yAxisMode: 'span',
xIsSeries: 1
}
};

new Chart("#chart-trends", plot_chart_args);
new Chart("#chart-trends", plotChartArgs);

Array.prototype.slice.call(
document.querySelectorAll('.chart-plot-buttons button')
@@ -245,23 +250,17 @@ Array.prototype.slice.call(
el.addEventListener('click', (e) => {
let btn = e.target;
let type = btn.getAttribute('data-type');
let config = [];

if(type === 'line') {
config = [0, 0, 0];
} else if(type === 'region') {
config = [0, 0, 1];
} else {
config = [0, 1, 0];
}
let config = {};
config[type] = 1;

plot_chart_args.hideDots = config[0];
plot_chart_args.heatline = config[1];
plot_chart_args.regionFill = config[2];
if(['regionFill', 'heatline'].includes(type)) {
config.hideDots = 1;
}

plot_chart_args.init = false;
// plotChartArgs.init = false;
plotChartArgs.lineOptions = config;

new Chart("#chart-trends", plot_chart_args);
new Chart("#chart-trends", plotChartArgs);

Array.prototype.slice.call(
btn.parentNode.querySelectorAll('button')).map(el => {
@@ -308,6 +307,7 @@ let update_chart = new Chart("#chart-update", {
colors: ['red'],
isSeries: 1,
lineOptions: {
// hideLine: 1,
regionFill: 1
},
});
@@ -398,48 +398,7 @@ events_chart.parent.addEventListener('data-select', (e) => {

// Aggregation chart
// ================================================================================
let aggr_data = {
labels: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
datasets: [
{
"values": [25, 40, 30, 35, 8, 52, 17]
},
{
"values": [25, 50, 10, 15, 18, 32, 27],
}
]
};

let aggr_chart = new Chart("#chart-aggr", {
data: aggr_data,
type: 'bar',
height: 250,
colors: ['light-green', 'blue'],
valuesOverPoints: 1,
barOptions: {
stacked: 1
}
});

document.querySelector('[data-aggregation="sums"]').addEventListener("click", (e) => {
if(e.target.innerHTML === "Show Sums") {
aggr_chart.show_sums();
e.target.innerHTML = "Hide Sums";
} else {
aggr_chart.hide_sums();
e.target.innerHTML = "Show Sums";
}
});

document.querySelector('[data-aggregation="average"]').addEventListener("click", (e) => {
if(e.target.innerHTML === "Show Averages") {
aggr_chart.show_averages();
e.target.innerHTML = "Hide Averages";
} else {
aggr_chart.hide_averages();
e.target.innerHTML = "Show Averages";
}
});

// Heatmap
// ================================================================================


+ 3
- 3
docs/index.html Прегледај датотеку

@@ -165,10 +165,10 @@
...</code></pre>
<div id="chart-trends" class="border"></div>
<div class="btn-group chart-plot-buttons mt-1 mx-auto" role="group">
<button type="button" class="btn btn-sm btn-secondary" data-type="line">Line</button>
<button type="button" class="btn btn-sm btn-secondary" data-type="dots">Dots</button>
<button type="button" class="btn btn-sm btn-secondary" data-type="hideDots">Line</button>
<button type="button" class="btn btn-sm btn-secondary" data-type="hideLine">Dots</button>
<button type="button" class="btn btn-sm btn-secondary active" data-type="heatline">HeatLine</button>
<button type="button" class="btn btn-sm btn-secondary" data-type="region">Region</button>
<button type="button" class="btn btn-sm btn-secondary" data-type="regionFill">Region</button>
</div>
<pre><code class="hljs javascript margin-vertical-px"> ...
type: 'line', // Line Chart specific properties:


+ 42
- 36
src/js/charts/AxisChart.js Прегледај датотеку

@@ -1,5 +1,5 @@
import BaseChart from './BaseChart';
import { dataPrep, zeroDataPrep } from './axis-chart-utils';
import { dataPrep, zeroDataPrep, getShortenedLabels } from '../utils/axis-chart-utils';
import { Y_AXIS_MARGIN } from '../utils/constants';
import { getComponent } from '../objects/ChartComponents';
import { getOffset, fire } from '../utils/dom';
@@ -11,24 +11,29 @@ import { MIN_BAR_PERCENT_HEIGHT, DEFAULT_AXIS_CHART_TYPE, BAR_CHART_SPACE_RATIO,
export default class AxisChart extends BaseChart {
constructor(parent, args) {
super(parent, args);
this.isSeries = args.isSeries;
this.valuesOverPoints = args.valuesOverPoints;
this.formatTooltipY = args.formatTooltipY;
this.formatTooltipX = args.formatTooltipX;

this.barOptions = args.barOptions || {};
this.lineOptions = args.lineOptions || {};
this.type = args.type || 'line';

this.xAxisMode = args.xAxisMode || 'span';
this.yAxisMode = args.yAxisMode || 'span';
this.type = args.type || 'line';

this.setup();
}

configure(args) {3
configure(args) {
super.configure();
this.config.xAxisMode = args.xAxisMode;
this.config.yAxisMode = args.yAxisMode;

args.axisOptions = args.axisOptions || {};
args.tooltipOptions = args.tooltipOptions || {};

this.config.xAxisMode = args.axisOptions.xAxisMode || 'span';
this.config.yAxisMode = args.axisOptions.yAxisMode || 'span';
this.config.xIsSeries = args.axisOptions.xIsSeries || 1;

this.config.formatTooltipX = args.tooltipOptions.formatTooltipX;
this.config.formatTooltipY = args.tooltipOptions.formatTooltipY;

this.config.valuesOverPoints = args.valuesOverPoints;
}

setMargins() {
@@ -132,7 +137,7 @@ export default class AxisChart extends BaseChart {
if(this.data.yMarkers) {
this.state.yMarkers = this.data.yMarkers.map(d => {
d.position = scale(d.value, s.yAxis);
if(!d.label) {
if(!d.label.includes(':')) {
d.label += ': ' + d.value;
}
return d;
@@ -169,19 +174,29 @@ export default class AxisChart extends BaseChart {
[
'yAxis',
{
mode: this.yAxisMode,
mode: this.config.yAxisMode,
width: this.width,
// pos: 'right'
}
},
function() {
return this.state.yAxis;
}.bind(this)
],

[
'xAxis',
{
mode: this.xAxisMode,
mode: this.config.xAxisMode,
height: this.height,
// pos: 'right'
}
},
function() {
let s = this.state;
s.xAxis.calcLabels = getShortenedLabels(this.width,
s.xAxis.labels, this.config.xIsSeries);

return s.xAxis;
}.bind(this)
],

[
@@ -189,17 +204,12 @@ export default class AxisChart extends BaseChart {
{
width: this.width,
pos: 'right'
}
],
];

componentConfigs.map(args => {
args.push(
},
function() {
return this.state[args[0]];
return this.state.yRegions;
}.bind(this)
);
});
],
];

let barDatasets = this.state.datasets.filter(d => d.chartType === 'bar');
let lineDatasets = this.state.datasets.filter(d => d.chartType === 'line');
@@ -216,7 +226,7 @@ export default class AxisChart extends BaseChart {
stacked: this.barOptions.stacked,

// same for all datasets
valuesOverPoints: this.valuesOverPoints,
valuesOverPoints: this.config.valuesOverPoints,
minHeight: this.height * MIN_BAR_PERCENT_HEIGHT,
},
function() {
@@ -273,9 +283,10 @@ export default class AxisChart extends BaseChart {
heatline: this.lineOptions.heatline,
regionFill: this.lineOptions.regionFill,
hideDots: this.lineOptions.hideDots,
hideLine: this.lineOptions.hideLine,

// same for all datasets
valuesOverPoints: this.valuesOverPoints,
valuesOverPoints: this.config.valuesOverPoints,
},
function() {
let s = this.state;
@@ -300,17 +311,12 @@ export default class AxisChart extends BaseChart {
{
width: this.width,
pos: 'right'
}
]
];

markerConfigs.map(args => {
args.push(
},
function() {
return this.state[args[0]];
return this.state.yMarkers;
}.bind(this)
);
});
]
];

componentConfigs = componentConfigs.concat(barsConfigs, lineConfigs, markerConfigs);



+ 24
- 21
src/js/objects/ChartComponents.js Прегледај датотеку

@@ -98,23 +98,23 @@ let componentConfigs = {
layerClass: 'x axis',
makeElements(data) {
return data.positions.map((position, i) =>
xLine(position, data.labels[i], this.constants.height,
xLine(position, data.calcLabels[i], this.constants.height,
{mode: this.constants.mode, pos: this.constants.pos})
);
},

animateElements(newData) {
let newPos = newData.positions;
let newLabels = newData.labels;
let newLabels = newData.calcLabels;
let oldPos = this.oldData.positions;
let oldLabels = this.oldData.labels;
let oldLabels = this.oldData.calcLabels;

[oldPos, newPos] = equilizeNoOfElements(oldPos, newPos);
[oldLabels, newLabels] = equilizeNoOfElements(oldLabels, newLabels);

this.render({
positions: oldPos,
labels: newLabels
calcLabels: newLabels
});

return this.store.map((line, i) => {
@@ -266,23 +266,24 @@ let componentConfigs = {
makeElements(data) {
let c = this.constants;
this.unitType = 'dot';

this.paths = getPaths(
data.xPositions,
data.yPositions,
c.color,
{
heatline: c.heatline,
regionFill: c.regionFill
},
{
svgDefs: c.svgDefs,
zeroLine: data.zeroLine
}
)
this.paths = {};
if(!c.hideLine) {
this.paths = getPaths(
data.xPositions,
data.yPositions,
c.color,
{
heatline: c.heatline,
regionFill: c.regionFill
},
{
svgDefs: c.svgDefs,
zeroLine: data.zeroLine
}
)
}

this.units = []

if(!c.hideDots) {
this.units = data.yPositions.map((y, j) => {
return datasetDot(
@@ -325,8 +326,10 @@ let componentConfigs = {

let animateElements = [];

animateElements = animateElements.concat(animatePath(
this.paths, newXPos, newYPos, newData.zeroLine));
if(Object.keys(this.paths).length) {
animateElements = animateElements.concat(animatePath(
this.paths, newXPos, newYPos, newData.zeroLine));
}

if(this.units.length) {
this.units.map((dot, i) => {


src/js/charts/axis-chart-utils.js → src/js/utils/axis-chart-utils.js Прегледај датотеку

@@ -1,5 +1,5 @@
import { floatTwo, fillArray } from '../utils/helpers';
import { DEFAULT_AXIS_CHART_TYPE, AXIS_DATASET_CHART_TYPES } from '../utils/constants';
import { DEFAULT_AXIS_CHART_TYPE, AXIS_DATASET_CHART_TYPES, DEFAULT_CHAR_WIDTH } from '../utils/constants';

export function dataPrep(data, type) {
data.labels = data.labels || [];
@@ -72,20 +72,53 @@ export function zeroDataPrep(realData) {
chartType: d.chartType
}
}),
yRegions: [
};

if(realData.yMarkers) {
zeroData.yMarkers = [
{
start: 0,
end: 0,
value: 0,
label: ''
}
],
yMarkers: [
];
}

if(realData.yRegions) {
zeroData.yRegions = [
{
value: 0,
start: 0,
end: 0,
label: ''
}
]
};
];
}

return zeroData;
}

export function getShortenedLabels(chartWidth, labels=[], isSeries=true) {
let allowedSpace = chartWidth / labels.length;
let allowedLetters = allowedSpace / DEFAULT_CHAR_WIDTH;

let calcLabels = labels.map((label, i) => {
label += "";
if(label.length > allowedLetters) {

if(!isSeries) {
if(allowedLetters-3 > 0) {
label = label.slice(0, allowedLetters-3) + " ...";
} else {
label = label.slice(0, allowedLetters) + '..';
}
} else {
let multiple = Math.ceil(label.length/allowedLetters);
if(i % multiple !== 0) {
label = "";
}
}
}
return label;
});

return calcLabels;
}

+ 3
- 1
src/js/utils/constants.js Прегледај датотеку

@@ -14,4 +14,6 @@ export const BAR_CHART_SPACE_RATIO = 0.5;
export const MIN_BAR_PERCENT_HEIGHT = 0.01;

export const LINE_CHART_DOT_SIZE = 4;
export const DOT_OVERLAY_SIZE_INCR = 4;
export const DOT_OVERLAY_SIZE_INCR = 4;

export const DEFAULT_CHAR_WIDTH = 8;

+ 0
- 21
src/js/utils/draw-utils.js Прегледај датотеку

@@ -24,24 +24,3 @@ export function equilizeNoOfElements(array1, array2,
}
return [array1, array2];
}

// let char_width = 8;
// let allowed_space = avgUnitWidth * 1.5;
// let allowed_letters = allowed_space / 8;

// return values.map((value, i) => {
// let space_taken = getStringWidth(value, char_width) + 2;
// if(space_taken > allowed_space) {
// if(isSeries) {
// // Skip some axis lines if X axis is a series
// let skips = 1;
// while((space_taken/skips)*2 > allowed_space) {
// skips++;
// }
// if(i % skips !== 0) {
// return;
// }
// } else {
// value = value.slice(0, allowed_letters-3) + " ...";
// }
// }

Loading…
Откажи
Сачувај