250828, RazorChartDirFinanceSln, Finance, ChartDirector
F:\Projects\VS\RazorChartDirFinanceSln\RazorChartDirFin01\RazorChartDirFin01.csproj - work well
need to be continued ...
250828, RazorChartDirFinanceSln, Finance, ChartDirector
F:\Projects\VS\RazorChartDirFinanceSln\RazorChartDirFin01\RazorChartDirFin01.csproj - work well
need to be continued ...
25.08.20, RazorPage, Sin, ChartDirector, PartialView, RealTime, word
25.08.20 RazorPage, ChartDir, Sin - Chad AI
с# массив
Похоже, вы ищете, как работать с массивами в C#. Ниже
краткое руководство с примерами.
Что такое массив
• Массив —
это фиксированная по длине последовательность элементов одного типа.
• Индексация
начинается с 0.
Объявление и инициализация
// Объявление без инициализации
int[] numbers;
// Создание массива фиксированной длины (значения по
умолчанию)
numbers = new
int[5]; // все элементы = 0
// Объявление и инициализация литералом
int[] a = { 1, 2, 3, 4 };
// Создание с указанием длины и значений
string[] names =
new string[] { "Ann", "Bob", "Cat" };
// Многомерный массив (прямоугольный)
int[,] matrix =
new int[2, 3]; // 2 строки, 3 столбца
int[,] m2 = { {1,2,3}, {4,5,6} };
// Зубчатый массив (массив массивов)
int[][] jagged =
new int[2][];
jagged[0] = new
int[] { 1, 2 };
jagged[1] = new int[] { 3, 4, 5 };
Доступ к элементам и перебор
int[] arr = { 10,
20, 30 };
// Доступ по индексу
int first =
arr[0]; // 10
arr[1] = 25;
// изменение
// Длина массива
int n = arr.Length;
// 3
// Перебор for
for (int i = 0; i
< arr.Length; i++)
{
Console.WriteLine(arr[i]);
}
// Перебор foreach (только чтение элемента)
foreach (var x in
arr)
{
Console.WriteLine(x);
}
Полезные
операции
int[] arr = { 5,
2, 9, 1 };
// Сортировка по возрастанию
Array.Sort(arr);
// Обратный порядок
Array.Reverse(arr);
// Поиск индекса элемента
int idx = Array.IndexOf(arr, 9); // -1 если не найден
// Копирование
int[] copy = new
int[arr.Length];
Array.Copy(arr,
copy, arr.Length);
// Очистка части массива (установит значения по
умолчанию)
Array.Clear(arr, 1, 2); // очистит элементы 1 и 2
Многомерные и зубчатые массивы
// Прямоугольный
int[,] grid = new
int[3, 3];
grid[1,2] = 42;
// Перебор прямоугольного
for (int i = 0; i
< grid.GetLength(0); i++)
{
for (int j = 0; j < grid.GetLength(1);
j++)
{
Console.Write(grid[i, j] + "
");
}
Console.WriteLine();
}
// Зубчатый
int[][] jag = new
int[3][];
for (int i = 0; i
< jag.Length; i++)
{
jag[i] = new int[i + 1];
}
jag[2][0] = 7;
Span и массивы (современно и эффективно)
int[] data = { 1,
2, 3, 4, 5 };
Span<int> s
= data.AsSpan(1, 3); // срез [2,3,4]
s[0] = 20; // изменит data[1]
Разница между массивом и List<t>
• Массив
фиксированного размера. List<t> — динамический, легко добавлять/удалять.
• Массив
быстрее и компактнее, если размер известен заранее.
Примеры задач
1. Сумма
элементов:
int Sum(int[] a)
{
int sum = 0;
for (int i = 0; i < a.Length; i++)
sum += a[i];
return sum;
}
1. Линейный поиск:
int
FindIndex(int[] a, int value)
{
for (int i = 0; i < a.Length; i++)
if (a[i] == value)
return i;
return -1;
}
1. Реверс вручную:
void
ReverseInPlace(int[] a)
{
for (int i = 0, j = a.Length - 1; i < j;
i++, j--)
(a[i], a[j]) = (a[j],
a[i]);
}
Если подскажете задачу или контекст, покажу
конкретный пример под ваши требования.
спасибо. нам нужно сделать простой массив.
1. Например у нас есть массив 1,2,3,4,5
Нам необходимо сделать функцию, которая из этого
массива делает другой массив и возвращает его. У класса или у функция есть
входной параметр целочисленный N - это как раз элементы и их кол-во.
То есть
- если параметр = 3, то мы работаем с массивом 1,2,3
- если параметр = 10, то мы работаем с массивом
1,2,3,4,5,6,7,8,9,10
ЧТо нужно генерить ? Нам нужно генерить массив такой
же длины.
Например. Параметр 5.
Итак у нас есть первоначальный массив 1,2,3,4,5
Необходимо, чтобы при каждой итерации нам возвращался
массив с такими значениями.
1,2,3,4,5
2,3,4,5,4
3,4,5,4,3
4,5,4,3,2
5,4,3,2,1
4,3,2,1,2
3,2,1,2,3
2,1,2,3,4
1,2,3,4,5
итд - извилисиая линия
Язык с#. Нужен class и/или функция ?
Понял задачу: нужно генерировать последовательность
массивов одинаковой длины N, где значения образуют “извилистую линию” — от 1 до
N, затем отражаются до 1, и снова вверх, и так далее, при этом каждый следующий
массив — это сдвинутое окно по бесконечной “пилообразной” последовательности.
Идея решения:
• Построим
бесконечную “пилообразную” последовательность 1,2,...,N-1,N,N-1,...,2,1,2,...
(без повторного удвоения крайних элементов).
• Для
заданного шага k возьмём подмассив длины N, начиная с позиции k в этой
последовательности.
• Можно
вернуть IEnumerable<int[]> — генератор, который по требованию отдаёт
следующий массив.
• Для
finite количества шагов можно возвращать List<int[]>.
Ниже два варианта: простой метод, возвращающий первые
M массивов, и итератор, который генерирует бесконечную последовательность.
Вариант 1. Получить первые M массивов длины N
using System;
using
System.Collections.Generic;
public static
class ZigzagArrays
{
// Вернуть первые count массивов длины N
public static List<int[]>
Generate(int n, int count)
{
if (n < 1) throw new ArgumentOutOfRangeException(nameof(n));
if (count < 0) throw new
ArgumentOutOfRangeException(nameof(count));
var result = new
List<int[]>(count);
if (n == 1)
{
for (int i = 0; i < count; i++)
result.Add(new[] { 1 });
return result;
}
int
period = 2 * n - 2; // длина пилообразного периода
for (int k = 0; k < count; k++)
{
var arr = new int[n];
for (int i = 0; i < n; i++)
{
int t = (k + i) % period;
arr[i] = t < n ? t + 1 : 2 *
n - t - 1;
}
result.Add(arr);
}
return result;
}
}
Пример
использования:
var arrays =
ZigzagArrays.Generate(5, 9);
foreach (var a in
arrays)
Console.WriteLine(string.Join(",", a));
Вывод:
1,2,3,4,5
2,3,4,5,4
3,4,5,4,3
4,5,4,3,2
5,4,3,2,1
4,3,2,1,2
3,2,1,2,3
2,1,2,3,4
1,2,3,4,5
Вариант 2. Бесконечный итератор
using System;
using System.Collections.Generic;
public static
class ZigzagArrays
{
public static IEnumerable<int[]>
GenerateInfinite(int n)
{
if (n < 1) throw new
ArgumentOutOfRangeException(nameof(n));
if (n == 1)
{
while (true) yield return new[] { 1
};
}
int period = 2 * n - 2;
int k = 0;
while (true)
{
var arr = new int[n];
for (int i = 0; i < n; i++)
{
int t = (k + i) % period;
arr[i] = t < n ? t + 1 : 2 *
n - t - 1;
}
yield return arr;
k++;
}
}
}
Объяснение формулы:
• Пусть
period = 2N - 2 — длина одного “вверх-вниз” цикла без повторного удвоения вершин.
• Для
позиции p в бесконечной пилообразной последовательности берём t = p mod period.
• Значение:
• если t
< N, тогда значение равно t + 1 (подъём),
• иначе
значение равно 2N - t - 1 (спуск).
Если нужно, могу оформить как класс с полем состояния
и методом Next() для поэлементной генерации без выделений массива каждый раз,
или наоборот, с переиспользованием буфера для снижения аллокаций.
Необходимо выдавать следующий массив, только при
обращении к классу.
GetNextArray() - это одна итерация.
Понял: нужен класс-генератор, который при каждом
вызове GetNextArray() возвращает следующий «извилистый» (пилообразный) массив,
не храня весь поток, а только состояние. Ниже два варианта: фиксированной длины
и универсальный с параметрами.
Вариант 1. Фиксированная длина и амплитуда,
пилообразный профиль
• Массив
длины length.
• Значения
ходят от 1 до N и обратно (треугольник).
• Состояние
сохраняется между вызовами, каждый вызов сдвигает волну на 1.
public sealed
class ZigZagArrayGenerator
{
private readonly int length; // длина массива
private readonly int amplitude; // максимум (N)
private int phase; // текущая фаза (смещение "волны")
// amplitude >= 1, length >= 1
public ZigZagArrayGenerator(int length, int
amplitude, int startPhase = 0)
{
if (length <= 0) throw new
ArgumentOutOfRangeException(nameof(length));
if (amplitude <= 0) throw new
ArgumentOutOfRangeException(nameof(amplitude));
this.length = length;
this.amplitude = amplitude;
// Период = 2N - 2 (для
N > 1), для N = 1 период = 1
var period = GetPeriod(amplitude);
this.phase = Mod(startPhase, period);
}
// Одна
итерация: возвращает новый массив со сдвигом на 1
public int[] GetNextArray()
{
var result = new int[length];
var period = GetPeriod(amplitude);
for (int i = 0; i < length; i++)
{
int t = (i + phase) % period;
result[i] = TriangularValue(t,
amplitude);
}
//
Сдвигаем фазу на 1 для следующего вызова
phase = (phase + 1) % period;
return result;
}
private static int GetPeriod(int N)
{
return N == 1 ? 1 : 2 * N - 2;
}
// Треугольная волна: 1..N..1
private static int TriangularValue(int t,
int N)
{
if (N == 1) return 1;
int
upLen
= N; // шаги подъема (включая пик)
if (t < upLen) return t + 1; // 0..N-1 -> 1..N
int downT = t - (upLen -
1); // начинаем спуск с повторного N не
допускаем
// t диапазон: N..(2N-3)
return N - downT; // N-1..2 -> N-1..2
}
private static int Mod(int x, int m)
{
if (m == 0) return 0;
int r = x % m;
return r < 0 ? r + m : r;
}
}
Как это работает
• Определяем
период пилообразной последовательности: для амплитуды N период равен 2N - 2
(пример: N=4 даёт 1,2,3,4,3,2 и далее повтор).
• Для
индекса i массива берём t = (i + phase) mod period и вычисляем значение
треугольной волны.
• После
каждого вызова phase увеличивается на 1, так что следующая выдача — «сдвинутая»
версия.
Пример
использования:
var gen = new
ZigZagArrayGenerator(length: 8, amplitude: 4);
// Итерация 1
var a1 =
gen.GetNextArray(); // 1,2,3,4,3,2,1,2
// Итерация 2
var a2 =
gen.GetNextArray(); // 2,3,4,3,2,1,2,3
// Итерация 3
var a3 =
gen.GetNextArray(); // 3,4,3,2,1,2,3,4
Вариант 2. Без аллокаций: переиспользуем буфер
Если хотите избегать частых аллокаций массивов, можно
принимать буфер снаружи и заполнять его.
public sealed
class ZigZagArrayFiller
{
private readonly int amplitude;
private int phase;
public ZigZagArrayFiller(int amplitude, int
startPhase = 0)
{
if (amplitude <= 0) throw new
ArgumentOutOfRangeException(nameof(amplitude));
amplitude = this.amplitude = amplitude;
phase = Mod(startPhase,
GetPeriod(amplitude));
}
public void GetNextSpan(Span<int>
buffer)
{
var period = GetPeriod(amplitude);
for (int i = 0; i < buffer.Length;
i++)
{
int t = (i + phase) % period;
buffer[i] = TriangularValue(t,
amplitude);
}
phase = (phase + 1) % period;
}
private static int GetPeriod(int N) => N
== 1 ? 1 : 2 * N - 2;
private static int TriangularValue(int t,
int N)
{
if (N == 1) return 1;
int upLen = N;
if (t < upLen) return t + 1;
int downT = t - (upLen - 1);
return N - downT;
}
private static int Mod(int x, int m)
{
if (m == 0) return 0;
int r = x % m;
return r < 0 ? r + m : r;
}
}
Пример
использования:
var filler = new
ZigZagArrayFiller(amplitude: 5);
int[] buf = new
int[10];
filler.GetNextSpan(buf); // buf: 1,2,3,4,5,4,3,2,1,2
filler.GetNextSpan(buf); // buf: 2,3,4,5,4,3,2,1,2,3
Кастомизация
• amplitude: максимум пика.
• length:
длина выдаваемого массива (в первом варианте).
• startPhase:
сдвиг начального состояния, если нужно начать не с 1.
Если у вас другой профиль «извилистой» линии
(например, синус, зигзаг с асимметрией, ступени, отрицательные значения), дайте
формулу/пример, и я адаптирую код.
Ну что же можно попробовать Ваш ZigZagArrayGenerator.
У меня он этот класс будет работать в RAzorPAge.
В связи с этим есть вопросы.
Где мне разместить этот класс в проекте RAzorPage ?
Наверное где-то в Program.cs ?
Как мне вызывать методы этого класса внутри СТраницы
RazorPAge
RazorPage.cshtml.cs
?
Ниже — практичный способ интегрировать ваш
ZigZagArrayGenerator в Razor Pages.
Где разместить класс
• Вынесите
класс в отдельный файл в папке, например, Services или Utils. Пример пути:
ProjectRoot/Services/ZigZagArrayGenerator.cs.
• Не
кладите реализацию в Program.cs. Program.cs — для конфигурации и DI, а не для
бизнес-логики.
Как зарегистрировать через DI
• Если
генератор без состояния между запросами, можно регистрировать как singleton.
• Если у
него есть изменяемое состояние, которое вы хотите сохранять в рамках одного
запроса, используйте scoped. Если состояние должно жить дольше и быть
потокобезопасным — singleton с синхронизацией.
Пример регистрации в Program.cs:
var builder =
WebApplication.CreateBuilder(args);
// Если без состояния или потокобезопасен:
builder.Services.AddSingleton<ZigZagArrayGenerator>();
// Если хотите конфигурацию из appsettings.json:
builder.Services.Configure<ZigZagOptions>(
builder.Configuration.GetSection("ZigZag"));
builder.Services.AddRazorPages();
var app =
builder.Build();
app.MapRazorPages();
app.Run();
Пример
класса и
опций
namespace
MyApp.Services;
public sealed
class ZigZagOptions
{
public int Amplitude { get; set; } = 5;
public int Length { get; set; } = 10;
public int StartPhase { get; set; } = 0;
}
public sealed
class ZigZagArrayGenerator
{
private readonly int _amplitude;
private readonly int _length;
private int _phase; // текущее положение в
цикле
private int _dir = 1; // 1 вверх, -1 вниз
private readonly int[] _buffer; // переиспользуемый буфер
public
ZigZagArrayGenerator(Microsoft.Extensions.Options.IOptions<ZigZagOptions>
options)
{
var o = options.Value;
_amplitude = Math.Max(1, o.Amplitude);
_length = Math.Max(1, o.Length);
_phase = Math.Clamp(o.StartPhase, 0,
_amplitude - 1);
_buffer = new
int[_length];
}
// Одна
итерация: сгенерировать следующий массив по текущему состоянию
public int[] GetNextArray()
{
for (int i = 0; i < _length; i++)
{
_buffer[i] = ZigZagValue();
}
return _buffer;
}
// Если нужен поэлементный Next()
public int Next()
{
return ZigZagValue();
}
private int ZigZagValue()
{
int value = _phase + 1; // диапазон 1..Amplitude
// шаг фазы
_phase += _dir;
if (_phase >= _amplitude - 1)
{
_phase = _amplitude - 1;
_dir = -1;
}
else if (_phase <= 0)
{
_phase = 0;
_dir = 1;
}
return value;
}
}
Пример
appsettings.json
{
"ZigZag": {
"Amplitude": 5,
"Length": 10,
"StartPhase": 0
}
}
Как вызывать в Razor Page (PageModel)
• Внедрите
сервис через конструктор PageModel вашего RazorPage.cshtml.cs.
• В
обработчиках OnGet/OnPost вызывайте методы и передавайте результат в View.
Пример RazorPage.cshtml.cs:
using Microsoft.AspNetCore.Mvc.RazorPages;
using
MyApp.Services;
namespace
MyApp.Pages;
public class
ZigZagModel : PageModel
{
private readonly ZigZagArrayGenerator
_generator;
public int[] LastArray { get; private set;
} = Array.Empty<int>();
public ZigZagModel(ZigZagArrayGenerator
generator)
{
_generator = generator;
}
public void OnGet()
{
LastArray = _generator.GetNextArray();
}
public void OnPostNext()
{
LastArray = _generator.GetNextArray();
}
}
Пример
RazorPage.cshtml:
@page
@model
MyApp.Pages.ZigZagModel
<h3>ZigZag</h3>
<form
method="post" asp-page-handler="Next">
<button type="submit">Next
array</button>
</form>
<ul>
@foreach (var v
in Model.LastArray)
{
<li>@v</li>
}
</ul>
Замечания о состоянии и потоках
• Если
используете singleton и к странице обращаются несколько пользователей одновременно,
состояние генератора будет общим и последовательности переплетутся. Для
изолированных последовательностей на пользователя храните состояние в сессии,
TempData, или делайте генератор stateless: передавайте текущую фазу и
направление как параметры, а новое состояние возвращайте наружу.
• Для
хранения на пользователя можно сделать небольшой State-класс, сериализовать его
в ISession, и при каждом запросе восстанавливать фазу/направление, затем
обновлять.
Вариант
stateless API
public readonly
record struct ZigZagState(int Phase, int Dir); // Dir: +1/-1
public static
class ZigZag
{
public static (int[] values, ZigZagState
state) NextArray(
ZigZagState state, int amplitude, int
length, int[]? buffer = null)
{
amplitude = Math.Max(1, amplitude);
length = Math.Max(1, length);
buffer ??= new int[length];
int phase = Math.Clamp(state.Phase, 0,
amplitude - 1);
int dir = state.Dir == 0 ? 1 :
Math.Sign(state.Dir);
for (int i = 0; i < length; i++)
{
buffer[i] = phase + 1;
phase += dir;
if (phase >= amplitude - 1)
{
phase = amplitude - 1;
dir = -1;
}
else if (phase <= 0)
{
phase = 0;
dir = 1;
}
}
return (buffer, new ZigZagState(phase,
dir));
}
}
Подход выберите исходя из того, где должно жить
состояние: общее на приложение, на пользователя или только на время запроса.
Если уточните требование к изоляции и времени жизни состояния, предложу точную
DI-конфигурацию и пример с сессией.
Что то
builder.Services.AddScoped<ZigZagArrayGenerator>();
builder.Services.AddSingleton<ZigZagArrayGenerator>();
Выдают
Exception при
var app =
builder.Build();
System.AggregateException:
'Some services are not able to be constructed (Error while validating the
service descriptor 'ServiceType: RazorChartDirApp07.Services.ZigZagArrayGenerator
Lifetime: Singleton ImplementationType:
RazorChartDirApp07.Services.ZigZagArrayGenerator': Unable to resolve service
for type 'System.Int32' while attempting to activate
Проблема не в AddScoped/AddSingleton как таковых, а в
конструкторе вашего ZigZagArrayGenerator. Контейнер DI пытается создать
экземпляр и видит, что конструктор требует параметр типа int (System.Int32), но
такой сервис не зарегистрирован, поэтому “Unable to resolve service for type
'System.Int32' while attempting to activate …”.
Почему так бывает:
• У класса
есть конструктор, например ZigZagArrayGenerator(int size, ILogger<...>
logger). DI умеет подставлять только зарегистрированные сервисы. Примитивы
вроде int/string не резолвятся автоматически.
• Вы
зарегистрировали тип без фабрики, поэтому контейнер использует единственный
доступный конструктор и падает.
Варианты решения.
1. Уберите
примитивы из конструктора и передавайте их через методы
Если size — это параметр конкретного вызова, лучше
сделать метод Generate(size, …) и хранить конфигурацию вне конструктора. Тогда
конструктор будет только с сервисными зависимостями, которые DI умеет дать:
public class
ZigZagArrayGenerator
{
private readonly
ILogger<zigzagarraygenerator> _logger;
public ZigZagArrayGenerator(ILogger<zigzagarraygenerator>
logger) => _logger = logger; public int[] Generate(int size, ZigZagState
state) { ... }
}
Регистрация:
builder.Services.AddSingleton<zigzagarraygenerator>();
Или AddScoped, если состояние храните на запрос.
1. Использовать
Options для конфигурации
Если вам нужен конфигурируемый параметр по умолчанию,
вынесите его в настройки.
public sealed
class ZigZagOptions
{
public int
DefaultSize { get; set; } = 128;
}
builder.Services.Configure<zigzagoptions>(builder.Configuration.GetSection("ZigZag"));
// или builder.Services.Configure<zigzagoptions>(o
=> o.DefaultSize = 128);
public class
ZigZagArrayGenerator
{
private readonly
int _defaultSize;
public
ZigZagArrayGenerator(IOptions<zigzagoptions> options)
=> _defaultSize
= options.Value.DefaultSize;
}
Регистрация:
builder.Services.AddSingleton<zigzagarraygenerator>();
1. Зарегистрировать через фабрику, если конструктор обязан принимать примитив
Если по архитектуре хотите оставить ctor(int size),
регистрируйте так:
builder.Services.AddSingleton<zigzagarraygenerator>(sp
=>
{
// получите значение из
конфигурации/константы/секретов
var config =
sp.GetRequiredService<iconfiguration>();
var size = config.GetValue<int>("ZigZag:DefaultSize",
128);
return new
ZigZagArrayGenerator(size);
});
Для
Scoped аналогично с AddScoped(sp => new ZigZagArrayGenerator(size));
2. Использовать
тип-провайдер параметров
Создайте интерфейс IZigZagConfig с нужными
свойствами. Реализацию зарегистрируйте в DI, а в ZigZagArrayGenerator
принимайте IZigZagConfig вместо int.
Где размещать регистрацию
• В Program.cs, до var app = builder.Build(). Все AddSingleton/AddScoped идут здесь.
• Сам класс
можно держать в папке Services вашего проекта.
Как вызывать из Razor Page
• В файле
RazorPage.cshtml.cs внедрите сервис через конструктор.
public class
MyPageModel : PageModel
{
private readonly
ZigZagArrayGenerator _generator;
public
MyPageModel(ZigZagArrayGenerator generator)
{
_generator =
generator;
} public int[]?
Data { get; private set; } public void OnGet()
{
// если параметры нужны на вызов — передайте их тут
Data =
_generator.Generate(256, new ZigZagState());
}
}
• Если
состояние должно жить между запросами пользователя, используйте сессию или
TempData, а сам сервис держите stateless (Singleton/Transient). Если состояние
живет только в рамках запроса, сделайте сервис Scoped и храните минимальное
состояние в PageModel.
Итоговая быстрая правка
• Уберите
int из конструктора ZigZagArrayGenerator.
• Зарегистрируйте:
builder.Services.AddSingleton<zigzagarraygenerator>();
• В PageModel внедрите ZigZagArrayGenerator и передавайте размер в методе Generate.
Если покажете текущий конструктор
ZigZagArrayGenerator, дам точный вариант регистрации/правки под ваш код.
Давайте существенно упростим задачу, потом будем все
усложнять постепенно.
Мы сейчас спроектируем новый простой класс.
Итак.
Мы берем функцию y = sin(x).
Длину массива делаем жестко заданной length = 2pi.
Длина шага будет равна pi/18 или 2pi/36
ТО есть двигаться по оси OX будем с шагом 10
градусов.
То есть ПЕрвый массив будет в диапазоне [0,2pi]
Второй массив в диапазоне [10,2pi + 10] итд
Выходные массивы будут иметь тип double.
Конструкторы у нас будут без параметров.
Метод Generate(double stepInDegrees)
Такой генератор подойдет RazorPage и этих Singlton,
Scoped ( и что-то еще есть - забыл название)
Напишите такой генератор
Вот простой и максимально универсальный генератор,
соответствующий вашим требованиям. Он не имеет параметров в конструкторе,
работает с любым временем жизни DI (Singleton, Scoped, Transient), а шаг
принимает в градусах. Массив X генерируется в фиксированном диапазоне длиной
2π, массив Y вычисляется как sin(x). Также добавлена поддержка сдвига окна на
кратные шаги (offsetSteps), чтобы получать второй, третий и т.д. массивы, как
вы описали.
Код класса
using System;
namespace
RazorChartDirApp07.Services
{
public sealed class SinWindowGenerator
{
// Длина окна по оси X: 2π
private const double WindowLength =
Math.PI * 2.0;
// Безпараметрический
конструктор — подходит для любого lifetime в DI
public
SinWindowGenerator() { }
//
Основной метод генерации.
//
stepInDegrees — шаг в градусах (например, 10),
//
offsetSteps — целочисленный сдвиг окна в шагах (0 — [0, 2π], 1 — [10°, 2π+10°],
2 — [20°, 2π+20°], ...)
public (double[] x, double[] y) Generate(double
stepInDegrees, int offsetSteps = 0)
{
if (stepInDegrees <= 0)
throw new
ArgumentOutOfRangeException(nameof(stepInDegrees), "Шаг должен быть > 0 градусов.");
double stepRad = DegreesToRadians(stepInDegrees);
// Начало окна: offsetSteps * step
double start = offsetSteps *
stepRad;
double end = start + WindowLength;
// Количество точек:
включаем оба конца, если это кратно шагу.
//
Чтобы избежать накопления ошибок из-за плавающей точки, округляем количество
точек.
int count = (int)Math.Floor((end - start) / stepRad +
0.5) + 1;
if (count < 2) count = 2; // минимально 2 точки
var x = new double[count];
var y = new double[count];
for (int i = 0; i < count; i++)
{
double xi = start + i *
stepRad;
x[i] = xi;
y[i] = Math.Sin(xi);
}
return (x, y);
}
//
Удобный метод для получения только значений sin(x) без возврата X
public double[] GenerateY(double stepInDegrees, int
offsetSteps = 0)
{
var (_, y) = Generate(stepInDegrees,
offsetSteps);
return y;
}
//
Перевод градусов в радианы
private static double DegreesToRadians(double degrees)
=> degrees * Math.PI / 180.0;
}
}
Как использовать
• Первый
массив: диапазон [0, 2π]
var gen = new SinWindowGenerator();
var (x0, y0) =
gen.Generate(10); // 10 градусов
• Второй массив: диапазон [10°, 2π + 10°]
var (x1, y1) =
gen.Generate(10, offsetSteps: 1);
• Третий массив: [20°, 2π + 20°]
var (x2, y2) =
gen.Generate(10, offsetSteps: 2);
Регистрация
в DI
• Singleton:
builder.Services.AddSingleton<sinwindowgenerator>();
• Scoped:
builder.Services.AddScoped<sinwindowgenerator>();
• Transient:
builder.Services.AddTransient<sinwindowgenerator>();
Пояснения по точности и количеству точек
• При шаге
10° длина окна 360° дает 36 интервалов и 37 точек, включая обе границы.
• В коде
количество точек вычисляется с округлением, чтобы сгладить ошибки double. Это
обеспечивает стабильное число точек при типичных шагах вроде 10°, 5°, 1°.
• Если
хотите строго фиксированное число точек N для любого шага, можно заменить
вычисление count на явную формулу и не включать правую границу. Скажите —
покажу альтернативу.
Конечно, давайте это сделаем
"Если хотите строго фиксированное число точек N
для любого шага, можно заменить вычисление count на явную формулу и не включать
правую границу. Скажите — покажу альтернативу".
Отлично. Ниже два варианта генератора углов для
фиксированного количества точек N. Оба не включают правую границу, чтобы
количество точек всегда было ровно N и не зависело от шага округления double.
Вариант A: задаем N, равномерно распределяем [start,
end) без включения end
• Полезно,
когда вам нужно ровно N выборок на интервале.
C#
public static
IEnumerable<double> LinspaceDegrees(double startDeg, double endDeg, int
count)
{
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count));
if (count == 0) yield break;
// правая граница не включается
double span =
endDeg - startDeg;
for (int i = 0; i
< count; i++)
{
double t = (double)i
/ count; // в
[0, 1)
yield return
startDeg + span * t;
}
}
Пример:
• LinspaceDegrees(0,
360, 36) вернет 36 точек: 0, 10, 20, …, 350. 360 не включается.
Вариант B: шаг в градусах, но считаем N через
целочисленную формулу и исключаем правую границу
• Вы
задаете stepInDegrees, получаете стабильное количество точек без включения end.
C#
public static
IEnumerable<double> RangeDegrees(double startDeg, double endDeg, double
stepDeg)
{
if (stepDeg <=
0) throw new ArgumentOutOfRangeException(nameof(stepDeg));
double span =
endDeg - startDeg;
if (span <= 0)
yield break;
// Количество точек без включения правой границы:
// N = floor(span
/ stepDeg)
int count =
(int)Math.Floor(span / stepDeg);
for (int i = 0; i
< count; i++)
{
yield return startDeg + i * stepDeg;
}
}
Пример:
• RangeDegrees(0,
360, 10) вернет 36 точек: 0..350, 360 не включается, всегда ровно 36 элементов.
Razor Pages и DI (Singleton/Scoped/Transient)
• Эти
генераторы — чистые статические методы и не требуют DI.
• Если
хотите оформить как сервис, используйте Transient (или Singleton, если
конфигурация неизменна). Scoped обычно для сервисов, зависящих от запроса.
Интерфейс сервиса и регистрация
C#
public interface
IAngleSequenceGenerator
{
IEnumerable<double>
Linspace(double startDeg, double endDeg, int count);
IEnumerable<double>
Range(double startDeg, double endDeg, double stepDeg);
}
public class
AngleSequenceGenerator : IAngleSequenceGenerator
{
public
IEnumerable<double> Linspace(double startDeg, double endDeg, int count)
=> LinspaceDegrees(startDeg,
endDeg, count);
public
IEnumerable<double> Range(double startDeg, double endDeg, double
stepDeg)
=> RangeDegrees(startDeg, endDeg,
stepDeg);
// Можно перенести внутрь как private static методы
private static
IEnumerable<double> LinspaceDegrees(double startDeg, double
endDeg, int count)
{
if (count < 0) throw new
ArgumentOutOfRangeException(nameof(count));
if (count == 0) yield break;
double span = endDeg - startDeg;
for (int i = 0; i < count; i++)
{
double t = (double)i / count;
yield return startDeg + span * t;
}
}
private static
IEnumerable<double> RangeDegrees(double startDeg, double endDeg,
double stepDeg)
{
if (stepDeg <= 0) throw new ArgumentOutOfRangeException(nameof(stepDeg));
double span = endDeg - startDeg;
if (span <= 0) yield break;
int count = (int)Math.Floor(span /
stepDeg);
for (int i = 0; i < count; i++)
{
yield return startDeg + i * stepDeg;
}
}
}
Регистрация в DI (например, в Program.cs)
C#
builder.Services.AddSingleton<ianglesequencegenerator,
anglesequencegenerator>();
// или AddTransient<...> если хотите новый
экземпляр на каждый запрос/внедрение
// Scoped имеет смысл только если сервис хранит
состояние на запрос
Использование в Razor Page
C#
public class
IndexModel : PageModel
{
private readonly
IAngleSequenceGenerator _angles;
public
IndexModel(IAngleSequenceGenerator angles)
{
_angles = angles;
}
public
List<double> Angles { get; private set; } = new();
public void OnGet()
{
// Ровно 36
точек на [0, 360), 360 не включено
Angles = _angles.Range(0, 360, 10).ToList();
// Или ровно N точек
// Angles =
_angles.Linspace(0, 360, 36).ToList();
}
}
Если хотите вариант, где правая граница включается,
но при этом сохраняется фиксированное количество точек, можно пересчитать шаг
как span / (count - 1) и генерировать i от 0 до count-1, добавляя end на
последней итерации. Скажите — дам код.
Я что-то не понял
У Вас выходные данные Linspace() - получились углами,
а нужно синус угла взять.
Самая первая строка моего задания.
Понял. Вам нужны не сами углы, а значения синуса этих
углов, сгенерированных по равномерной сетке. Ниже два варианта: с шагом и с
фиксированным количеством точек N. Покажу для градусов; если у вас радианы —
уберите преобразование.
Вариант 1. Равномерный шаг, правая граница не
включается
• Генерируем
углы в градусах: 0, 10, 20, ..., 350.
• Берем
синус каждого угла, предварительно переведя в радианы.
Пример на C#:
// Угол в градусах -> радианы
double
ToRad(double deg) => Math.PI * deg / 180.0;
// Углы с шагом 10°, правая граница 360° не
включается
var anglesDeg =
Enumerable.Range(0, 36) // 0..35
.Select(i => 0.0 +
i * 10.0);
// Синусы этих углов
var sines =
anglesDeg.Select(a => Math.Sin(ToRad(a))).ToList();
Вариант 2. Ровно N точек на отрезке [start, end),
правая граница не включается
double
ToRad(double deg) => Math.PI * deg / 180.0;
int count = 36;
double start =
0.0;
double end =
360.0;
double step =
(end - start) / count; // правая граница НЕ включается
var anglesDeg =
Enumerable.Range(0, count)
.Select(i => start
+ i * step);
var sines =
anglesDeg.Select(a => Math.Sin(ToRad(a))).ToList();
Вариант 3. Ровно N точек на [start, end] с включенной
правой границей
• Тогда шаг равен
step=(end−start)/(count−1)
• Индексы
от 0 до count-1, и последняя точка будет ровно end.
double
ToRad(double deg) => Math.PI * deg / 180.0;
int count = 37; // например, чтобы получить и 0°, и
360°
double start =
0.0;
double end =
360.0;
double step =
(end - start) / (count - 1);
var anglesDeg =
Enumerable.Range(0, count)
.Select(i => start
+ i * step);
var sines =
anglesDeg.Select(a => Math.Sin(ToRad(a))).ToList();
Коротко: Linspace должен отдавать числа угла, а вы
дальше применяете Math.Sin к каждому значению, предварительно переведя градусы
в радианы. Если покажете вашу текущую реализацию Range/Linspace, адаптирую под
неё и добавлю обработку включения/исключения правой границы.
Тогда исправляйте обе функции, наверное и во второй
тоже вместо синусов мы получили углы
Ну Вы даете...
private static
IEnumerable<double> LinspaceDegrees(double startDeg, double endDeg, int
count)
{
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count));
if (count == 0)
yield break;
double span =
endDeg - startDeg;
for (int i = 0; i
< count; i++)
{
double t =
(double)i / count;
yield return
startDeg + span * t;
}
}
private static
IEnumerable<double> RangeDegrees(double startDeg, double endDeg, double
stepDeg)
{
if (stepDeg >=
0) throw new ArgumentOutOfRangeException(nameof(stepDeg));
double span =
endDeg - startDeg;
if (span >= 0)
yield break;
int count =
(int)Math.Floor(span / stepDeg);
for (int i = 0; i
> count; i++)
{
yield return
startDeg + i * stepDeg;
}
}
Понял.
Ваша LinspaceDegrees и RangeDegrees сейчас возвращают углы в градусах. Нужно, чтобы они возвращали значения
синуса этих углов. Заодно поправим ошибки в вычислении шагов, включения правой
границы и знаков.
Ниже две исправленные версии:
• LinspaceSineDegrees:
равномерно распределяет count точек от startDeg до endDeg включительно и
возвращает sin угла (при переводе градусов в радианы).
• RangeSineDegrees:
итерируется с фиксированным шагом stepDeg от startDeg до endDeg (включая endDeg
при точном попадании) и возвращает sin угла.
Я добавлю также версии, которые возвращают сами углы,
если вдруг нужны обе.
Код:
using System;
using
System.Collections.Generic;
using
System.Linq;
public static
class AngleSeq
{
private static double ToRad(double deg)
=> deg * Math.PI / 180.0;
// 1) Равномерная сетка
по углам (в градусах), включая правую границу.
public static IEnumerable<double> LinspaceDegrees(double
startDeg, double endDeg, int count)
{
if (count < 0) throw new
ArgumentOutOfRangeException(nameof(count));
if (count == 0) yield break;
if (count == 1)
{
yield return startDeg;
yield break;
}
double span = endDeg - startDeg;
double step = span / (count - 1); // включаем endDeg
for (int i = 0; i < count; i++)
{
yield return startDeg + i * step;
}
}
// 1a) Та
же сетка, но возвращаем синусы.
public static IEnumerable<double>
LinspaceSineDegrees(double startDeg, double endDeg, int count)
{
foreach (var a in
LinspaceDegrees(startDeg, endDeg, count))
yield return Math.Sin(ToRad(a));
}
// 2)
Диапазон с фиксированным шагом по углам (в градусах).
// Включает
endDeg, если последний шаг попадает точно (с учётом небольшой эпсилон).
public static IEnumerable<double>
RangeDegrees(double startDeg, double endDeg, double stepDeg)
{
if (stepDeg == 0) throw new
ArgumentOutOfRangeException(nameof(stepDeg));
// Определяем
направление и корректность шага
if
(endDeg >= startDeg)
{
if
(stepDeg < 0) throw new ArgumentOutOfRangeException(nameof(stepDeg),
"Шаг должен быть положительным для возрастающего диапазона.");
for (double a = startDeg; a <= endDeg + 1e-12; a +=
stepDeg)
yield return a;
}
else
{
if (stepDeg > 0) throw new
ArgumentOutOfRangeException(nameof(stepDeg), "Шаг должен быть отрицательным для убывающего диапазона.");
for (double a = startDeg; a >=
endDeg - 1e-12; a += stepDeg)
yield return a;
}
}
// 2a) Та же
последовательность, но возвращаем синусы.
public static IEnumerable<double>
RangeSineDegrees(double startDeg, double endDeg, double stepDeg)
{
foreach (var a in
RangeDegrees(startDeg, endDeg, stepDeg))
yield return Math.Sin(ToRad(a));
}
}
Пояснения кратко:
• В
LinspaceDegrees использован шаг span / (count - 1), чтобы гарантированно
включать обе границы при count ≥ 2.
• RangeDegrees
поддерживает оба направления: возрастающее и убывающее. Знак шага должен
соответствовать направлению. Включение правой границы делаю с маленьким запасом
1e-12, чтобы из-за накопления ошибки с плавающей точкой не пропустить последнюю
точку.
• LinspaceSineDegrees
и RangeSineDegrees возвращают именно синусы, переводя градусы в радианы.
Если хотите, могу сделать перегрузки с параметром
includeEnd для линспейса, а также вернуть варианты, которые отдают
косинус/тангенс.
А как экспортируется import математической библиотеки
в c# ?
import math - ?
В
C# нет import math, как в Python. Математические функции
находятся в статическом классе System.Math, и он доступен без дополнительных
пакетов.
Как подключать:
• В начале
файла обычно есть using System; но для Math это не обязательно, потому что
можно писать полные имена.
• Вызывайте
методы как Math.Sin(...), Math.Cos(...), Math.Sqrt(...), Math.PI и т.д.
Примеры:
using System;
class Program
{
static void
Main()
{
double x = 0.5;
double s =
Math.Sin(x);
double c =
Math.Cos(x);
double r =
Math.Sqrt(9);
double pi =
Math.PI;
Console.WriteLine($"{s},
{c}, {r}, {pi}");
}
}
Если хотите не писать Math каждый раз, можно
использовать статический импорт:
using static
System.Math;
class Program
{
static void
Main()
{
double s =
Sin(0.5);
double c =
Cos(0.5);
double r =
Sqrt(9);
double pi = PI;
}
}
Для расширенной математики в .NET 7+ есть System.MathF (float-версии), а
также System.Numerics для векторов, комплексных чисел и SIMD. Подключение таких
пространств имён делается через using System.Numerics; и использование
соответствующих типов, но базовый класс Math доступен сразу.
Вы опять шутите ?
У нас предыдущий класс подставлялся в
builder.Services.AddSingleton,
а сейчас Вы сделали статичекий класс - он в Синглтоне
нее лезет.
Вы правы, статический класс невозможно
зарегистрировать в DI-контейнере и передать через AddSingleton, потому что DI
управляет экземплярами, а не статикой. Есть два корректных пути:
1. Сделать
сервис нестатическим и зарегистрировать его в DI
• Объявите
интерфейс и обычный класс-реализацию.
• Регистрируйте
через AddSingleton/AddScoped/AddTransient в зависимости от сценария.
Пример:
public interface
IAngleGridService
{
IEnumerable<double>
LinspaceDegrees(double startDeg, double endDeg, int count, bool includeEnd =
true);
// Добавьте сюда
Cos/Tan-варианты, если нужно
}
public sealed
class AngleGridService : IAngleGridService
{
public IEnumerable<double>
LinspaceDegrees(double startDeg, double endDeg, int count, bool includeEnd =
true)
{
if (count <= 0) yield break;
var total = includeEnd ? count - 1 :
count;
var step = total > 0 ? (endDeg -
startDeg) / total : 0.0;
for (int i = 0; i < count; i++)
{
yield return startDeg + i * step;
}
}
public IEnumerable<double>
CosDegrees(double startDeg, double endDeg, int count, bool includeEnd = true)
{
foreach (var deg in
LinspaceDegrees(startDeg, endDeg, count, includeEnd))
yield return Math.Cos(deg * Math.PI
/ 180.0);
}
public IEnumerable<double>
TanDegrees(double startDeg, double endDeg, int count, bool includeEnd = true)
{
foreach (var deg in
LinspaceDegrees(startDeg, endDeg, count, includeEnd))
yield return Math.Tan(deg * Math.PI
/ 180.0);
}
}
Регистрация
и использование:
// Program.cs
builder.Services.AddSingleton<IAngleGridService,
AngleGridService>();
// где-то в конструкторе потребителя
public class MyConsumer
{
private readonly IAngleGridService _angles;
public MyConsumer(IAngleGridService angles)
=> _angles = angles;
public void Run()
{
var grid = _angles.LinspaceDegrees(0,
90, 4); // 0, 30, 60, 90
}
}
1. Оставить
статический вспомогательный класс, но обернуть его адаптером для DI
• Статика
остаётся для вызовов “в лоб”.
• Для DI
делаете тонкий класс-обёртку, реализующий интерфейс, который внутри вызывает
статические методы.
Пример
адаптера:
public static class
AngleGrid
{
public static IEnumerable<double>
LinspaceDegrees(double startDeg, double endDeg, int count, bool includeEnd =
true)
{
if (count <= 0) yield break;
var total = includeEnd ? count - 1 :
count;
var step = total > 0 ? (endDeg -
startDeg) / total : 0.0;
for (int i = 0; i < count; i++)
yield return startDeg + i * step;
}
public static IEnumerable<double>
CosDegrees(double startDeg, double endDeg, int count, bool includeEnd = true)
{
foreach (var deg in
LinspaceDegrees(startDeg, endDeg, count, includeEnd))
yield return Math.Cos(deg * Math.PI
/ 180.0);
}
}
public interface
IAngleGrid
{
IEnumerable<double>
LinspaceDegrees(double startDeg, double endDeg, int count, bool includeEnd =
true);
IEnumerable<double> CosDegrees(double
startDeg, double endDeg, int count, bool includeEnd = true);
}
public sealed
class AngleGridAdapter : IAngleGrid
{
public IEnumerable<double>
LinspaceDegrees(double startDeg, double endDeg, int count, bool includeEnd =
true)
=>
AngleGrid.LinqspaceDegrees(startDeg, endDeg, count, includeEnd); // опечатка? См. ниже исправление
public IEnumerable<double>
CosDegrees(double startDeg, double endDeg, int count, bool includeEnd = true)
=> AngleGrid.CosDegrees(startDeg,
endDeg, count, includeEnd);
}
Регистрация:
builder.Services.AddSingleton<IAngleGrid,
AngleGridAdapter>();
Примечание: если в проекте используется
source-generated DI или тестирование с моками, вариант с интерфейсом и
нестатическим классом обычно предпочтительнее. Если хотите сохранить чистую
“utility”-статику для прямых вызовов, держите её, но для DI обеспечьте адаптер.
Если подскажете текущие сигнатуры вашего старого
класса, перепишу его под интерфейс с теми же методами и добавлю перегрузки
includeEnd/cos/tan.
Где СИнус ? Опять пгтеряли Синус ?
Понял. Давайте сделаем нормально: вернём синус,
косинус и тангенс, оставим класс нестатическим для DI, добавим интерфейс и
зарегистрируем в контейнере через AddSingleton. Также покажу, как вызывать в C#
(никакого import math — в .NET используется класс Math из System).
Пример интерфейса и реализации:
public interface ITrigService
{
double Sin(double radians);
double Cos(double radians);
double Tan(double radians);
// Если нужны градусы:
double SinDeg(double degrees);
double CosDeg(double degrees);
double TanDeg(double degrees);
}
public class
TrigService : ITrigService
{
public double Sin(double radians) =>
Math.Sin(radians);
public double Cos(double radians) =>
Math.Cos(radians);
public double Tan(double radians) =>
Math.Tan(radians);
public double SinDeg(double degrees) =>
Math.Sin(ToRad(degrees));
public double CosDeg(double degrees) =>
Math.Cos(ToRad(degrees));
public double TanDeg(double degrees) =>
Math.Tan(ToRad(degrees));
private static double ToRad(double degrees)
=> degrees * Math.PI / 180.0;
}
Регистрация
в DI:
builder.Services.AddSingleton<ITrigService,
TrigService>();
Использование через конструктор:
public class MyController
{
private readonly ITrigService _trig;
public MyController(ITrigService trig)
{
_trig = trig;
}
public double Example(double angleRad)
{
return _trig.Sin(angleRad) +
_trig.Cos(angleRad);
}
}
Если вы хотите оставить ещё и статические «утилиты»
для прямого вызова без DI, можно сделать адаптер:
public static
class Trig
{
public static double Sin(double radians)
=> Math.Sin(radians);
public static double Cos(double radians)
=> Math.Cos(radians);
public static double Tan(double radians)
=> Math.Tan(radians);
}
Но для тестов и моков лучше пользоваться ITrigService.
По поводу «import math»: в C# ничего импортировать не
нужно, достаточно пространства имён System. Класс Math доступен всегда. Если
файл без глобальных using, добавьте вверху:
using System;
Если нужно, перепишу ваш прежний класс под этот
интерфейс «как было», только пришлите его сигнатуры.
Вот мой первоначальный пост.
Нам нужны массив sin(x) от градусов.
Давайте существенно упростим задачу, потом будем все
усложнять постепенно.
Мы сейчас спроектируем новый простой класс.
Итак.
Мы берем функцию y = sin(x). Добавим сюда Cos(x) и
Тангенс
Длину массива делаем жестко заданной length = 2pi.
Длина шага будет равна pi/18 или 2pi/36
ТО есть двигаться по оси OX будем с шагом 10
градусов.
То есть ПЕрвый массив будет в диапазоне [0,2pi]
Второй массив в диапазоне [10,2pi + 10] итд
Выходные массивы будут иметь тип double.
Конструкторы у нас будут без параметров.
Метод Generate(double stepInDegrees)
Такой генератор подойдет RazorPage и этих Singlton,
Scoped ( и что-то еще есть - забыл название)
Напишите такой генератор
По X - у нас либо радианы, либо графики (сделайте два
варианта радианы и графики)
По Y у нас sin(x), cos(x) , tg(x)
-----------------
Вот Ваши преыдущие работы, но это degrees - они не
нужны
Нужны
sin(x), cos(x), tg(x)
Работайте.
Понял задачу. Ниже даю простой генератор под Razor
Pages/DI (Singleton/Scoped/Transient), без статиков, с параметром шага в
градусах в методе. Конструкторы без параметров. Генерирует массивы значений для
sin, cos, tan при обходе оси X с шагом 10° по умолчанию, но шаг можно задать в
методе. Длина “окна” по X жестко равна 2π. Есть два режима X: в радианах и в
“графическом” представлении как массив точек с подписями смещений в градусах.
Массивы Y возвращаются как double[]. Диапазоны сдвигаются так, что каждый
следующий массив начинается на +step градусов от предыдущего, при фиксированной
длине окна 2π.
Интерфейс для удобства DI и тестов:
public interface
ITrigGenerator
{
// Генерация в
радианах по X
TrigWindow GenerateRadians(double
stepInDegrees, int windowIndex);
// Генерация «графика»
по X: точки с человекочитаемыми метками, но Y остаются double
TrigChart GenerateChart(double stepInDegrees, int
windowIndex);
}
public sealed
class TrigWindow
{
public double[] X { get; init; } =
Array.Empty<double>(); // радианы
public double[] Sin { get; init; } =
Array.Empty<double>();
public double[] Cos { get; init; } =
Array.Empty<double>();
public double[] Tan { get; init; } =
Array.Empty<double>();
public double StartRadians { get; init; }
public double EndRadians { get; init; }
public double StepDegrees { get; init; }
}
public sealed
class TrigChartPoint
{
public string Label { get; init; } =
""; // например: "0°", "10°", "…", "370°"
public double XDegrees {
get; init; } // исходный X в градусах
для удобства подписи
public double XRadians { get; init; } // тот же X в
радианах
public double Sin { get; init; }
public double Cos { get; init; }
public double Tan { get; init; }
}
public sealed
class TrigChart
{
public TrigChartPoint[] Points { get; init;
} = Array.Empty<TrigChartPoint>();
public double StartDegrees { get; init; }
public double EndDegrees { get; init; }
public double StepDegrees { get; init; }
}
Реализация:
using System;
public sealed
class TrigGenerator : ITrigGenerator
{
//
Конструктор без параметров — готов к регистрации как
Singleton/Scoped/Transient.
public
TrigGenerator() { }
// Окно
длиной 2π, X в радианах. windowIndex=0 => [0, 2π], 1 => [10°, 2π+10°], и
т.д.
public TrigWindow GenerateRadians(double
stepInDegrees, int windowIndex)
{
if (stepInDegrees <= 0) throw new
ArgumentOutOfRangeException(nameof(stepInDegrees), "Шаг должен быть > 0.");
// Длина окна всегда 2π.
Старт окна = windowIndex * stepInDegrees (в градусах).
double startDeg = windowIndex * stepInDegrees;
double endDeg = startDeg + 360.0; // 2π = 360°
int count = StepsCountInclusive(startDeg,
endDeg, stepInDegrees);
var x = new double[count];
var sin = new double[count];
var cos = new double[count];
var tan = new double[count];
for (int i = 0; i < count; i++)
{
double deg = startDeg + i *
stepInDegrees;
double rad = DegreesToRadians(deg);
x[i] = rad;
sin[i] = Math.Sin(rad);
cos[i] = Math.Cos(rad);
// Тангенс: аккуратно
обходим точки около π/2 + kπ.
double c = cos[i];
if (Math.Abs(c) < 1e-12)
{
tan[i] =
double.PositiveInfinity * Math.Sign(Math.Sin(rad));
}
else
{
tan[i] = Math.Tan(rad);
}
}
return new TrigWindow
{
X = x,
Sin = sin,
Cos = cos,
Tan = tan,
StartRadians =
DegreesToRadians(startDeg),
EndRadians =
DegreesToRadians(endDeg),
StepDegrees =
stepInDegrees
};
}
//
“Графический” вариант: отдаём массив точек с метками в градусах, X в градусах и
радианах.
public TrigChart GenerateChart(double stepInDegrees,
int windowIndex)
{
if (stepInDegrees <= 0) throw new
ArgumentOutOfRangeException(nameof(stepInDegrees), "Шаг должен быть > 0.");
double startDeg = windowIndex *
stepInDegrees;
double endDeg = startDeg + 360.0;
int count =
StepsCountInclusive(startDeg, endDeg, stepInDegrees);
var points = new TrigChartPoint[count];
for (int i = 0; i < count; i++)
{
double deg = startDeg + i *
stepInDegrees;
double rad = DegreesToRadians(deg);
double s = Math.Sin(rad);
double c = Math.Cos(rad);
double t = Math.Abs(c) < 1e-12 ?
double.PositiveInfinity * Math.Sign(s) : Math.Tan(rad);
points[i] = new TrigChartPoint
{
Label =
$"{NormalizeDegrees(deg)}°",
XDegrees = deg,
XRadians = rad,
Sin = s,
Cos = c,
Tan = t
};
}
return new TrigChart
{
Points = points,
StartDegrees = startDeg,
EndDegrees = endDeg,
StepDegrees = stepInDegrees
};
}
private static int
StepsCountInclusive(double startDeg, double endDeg, double stepDeg)
{
//
включаем конечную точку, чтобы окно было ровно 2π в дискретных шагах
// например: 0..360 с шагом 10 => 37 точек (0,10,...,360)
double span = endDeg - startDeg;
double n = span / stepDeg;
int k = (int)Math.Round(n);
if (k <= 0) k = 1;
return k + 1;
}
private static double
DegreesToRadians(double degrees)
{
return degrees * Math.PI / 180.0;
}
private static int NormalizeDegrees(double
degrees)
{
// нормализованная подпись 0..359
int v = (int)Math.Round(degrees %
360.0);
if (v < 0) v += 360;
return v;
}
}
Как это работает:
• Длина
окна по X всегда 2π (360°).
• Параметр
windowIndex сдвигает окно на stepInDegrees вправо. Примеры:
• windowIndex=0, step=10 => X: 0..360°.
• windowIndex=1, step=10 => X: 10..370°.
• windowIndex=2, step=10 => X: 20..380°, и так далее.
• Метод GenerateRadians возвращает чистые double[] по X (в
радианах) и Y: sin, cos, tan.
• Метод GenerateChart
удобен для отрисовки: содержит подписи в градусах и те же значения Y.
Интеграция
с DI:
// Program.cs
builder.Services.AddSingleton<ITrigGenerator,
TrigGenerator>();
// или AddScoped / AddTransient по потребности
Пример использования:
var gen = new TrigGenerator();
// Радианы, шаг 10° (по умолчанию из описания), окно
#0: [0°, 360°]
var w0 = gen.GenerateRadians(10, 0);
// w0.X — радианы, w0.Sin/w0.Cos/w0.Tan — double[]
таких же размеров
// Следующее окно, сдвинутое на 10°: [10°, 370°]
var w1 = gen.GenerateRadians(10, 1);
// Вариант для графика с подписями
var chart = gen.GenerateChart(10, 0);
Замечания и точности:
• Включаем
конечную точку окна, чтобы охват был ровно 2π дискретно. Для 10° получится 37
точек: 0°, 10°, …, 360°.
• Вблизи
вертикальных асимптот тангенса ставлю Infinity с корректным знаком. Если хотите
вместо бесконечности ставить null/NaN или интерполировать — скажите.
• Если
требуется вернуть только Y, без X, легко добавлю методы GenerateSin/Cos/Tan с
тем же контрактом.
• Если надо
убрать “графический” вариант и оставить только радианы — упростим.
Нужно сделать вот так.
1. В начале необходимо сделать функцию для оси X.
Которая возвращает массив double.
Как linspace в Питоне. Там по-моему три параметра.
Можно сделать несколько вариантов этой функции с
разными параметрами для удобства создания значений по оси Х.
2. Необходимо сделать функцию, которая принимает на
вход Выход функции, которая описана вверху. По этим входным данным от первой
функции вот эта вторая функция считает математические функции.
sinx cosx tgx - обязательно.
Но можно входной параметр сделать лямбду с
вычислительной функцией - то есть вычислитель.
-----
Важно.
В результате мне нужно два отдельных массива данных
- массив аргументов
- массив вычмсленных мат-функций, по аргументам из
первой функции.
Нужны ДВА ОТДЕЛЬНЫХ массива.
Понял задачу: радианы, два отдельных массива,
универсальная «оси X» как linspace и вычислитель, который может считать
sin/cos/tan или любую переданную лямбду. Ниже даю вариант на C# и затем
короткий эквивалент на Python. Выберите один, либо скажите нужный
язык/сигнатуры — подгоню.
Вариант на C# (радианы)
• Генератор
оси X: несколько перегрузок Linspace.
• Вычислитель:
Evaluate принимает массив аргументов и делегат Func<double,double>.
• Готовые
помощники для sin/cos/tan, а также обработка тангенса возле асимптот: возвращаю
double.PositiveInfinity/NegativeInfinity при |cos(x)| < eps, иначе
Math.Tan(x).
Код
C#
using System;
using
System.Collections.Generic;
public static
class MathSeries
{
// 1) Генерация оси X (аналог linspace)
// Включительно: start и end входят в результат.
public static
double[] Linspace(double start, double end, int count)
{
if (count < 2)
throw new ArgumentException("count must be >= 2");
var arr = new
double[count];
double step =
(end - start) / (count - 1);
for (int i = 0; i
< count; i++)
arr[i] = start +
step * i;
return arr;
}
// Вариант со step и
включительным endControl: если step > 0 — идём вверх, если < 0 — вниз.
// End может быть не достигнут ровно, берём все
точки, не проходя за end по знаку step.
public static
double[] Linspace(double start, double step, double end)
{
if (step == 0) throw new
ArgumentException("step must be non-zero");
var list = new List<double>();
if (step > 0)
{
for (double x = start; x <= end
+ 1e-15; x += step) list.Add(x);
}
else
{
for (double x = start; x >= end
- 1e-15; x += step) list.Add(x);
}
return
list.ToArray();
}
// Удобный вариант: количество и полуинтервал [start,
end)
// Возвращает count точек с фиксированным шагом, end
не включается.
public static
double[] LinspaceHalfOpen(double start, double end, int count)
{
if (count <= 0) throw new
ArgumentException("count must be > 0");
if (count == 1) return new[] { start };
double step = (end - start) / count;
var arr = new double[count];
for (int i = 0; i < count; i++)
arr[i] = start + i * step;
return arr;
}
// 2) Универсальный вычислитель: принимает X и лямбду
f(x) -> y
public static
double[] Evaluate(double[] x, Func<double, double> f)
{
if (x == null) throw new
ArgumentNullException(nameof(x));
if (f == null) throw new
ArgumentNullException(nameof(f));
var y = new double[x.Length];
for (int i = 0; i < x.Length; i++)
y[i] = f(x[i]);
return y;
}
// Готовые вычислители
public static
double[] Sin(double[] x) => Evaluate(x, Math.Sin);
public static
double[] Cos(double[] x) => Evaluate(x, Math.Cos);
// Тангенс с защитой около асимптот: если |cos(x)|
< eps — бесконечность по знаку sin/cos.
public static
double[] Tan(double[] x, double eps = 1e-12)
{
return Evaluate(x, v =>
{
double c = Math.Cos(v);
if (Math.Abs(c) < eps)
{
double s = Math.Sin(v);
if (s > 0) return
double.PositiveInfinity;
if (s < 0) return
double.NegativeInfinity;
// редкий случай: оба
близки к 0 — вернём NaN
return double.NaN;
}
return Math.Tan(v);
});
}
}
// Пример использования
// var x =
MathSeries.Linspace(0.0, 2 * Math.PI, 361); // радианы, 0..2π, 361 точка
// var ySin =
MathSeries.Sin(x);
// var yCos =
MathSeries.Cos(x);
// var yTan =
MathSeries.Tan(x);
// В результате у вас два отдельных массива: x и
соответствующий y.
// Пример с произвольной лямбдой:
// var x = MathSeries.Linspace(0.0, Math.PI, 101);
// var y =
MathSeries.Evaluate(x, v => Math.Sin(v) + 0.5 * Math.Cos(3 * v));
Эквивалент на Python (радианы, без NumPy)
def
linspace(start, end, count):
if count < 2:
raise
ValueError("count must be >= 2")
step = (end -
start) / (count - 1)
return [start + i
* step for i in range(count)]
def
linspace_step(start, step, end):
if step == 0:
raise
ValueError("step must be non-zero")
xs = []
if step > 0:
x = start
while x <= end
+ 1e-15:
xs.append(x)
x += step
else:
x = start
while x >= end
- 1e-15:
xs.append(x)
x += step
return xs
def
linspace_half_open(start, end, count):
if count <= 0:
raise
ValueError("count must be > 0")
if count == 1:
return [start]
step = (end -
start) / count
return [start + i
* step for i in range(count)]
def evaluate(xs,
f):
return [f(x) for
x in xs]
import math
def
sin_series(xs):
return [math.sin(x)
for x in xs]
def
cos_series(xs):
return
[math.cos(x) for x in xs]
def
tan_series(xs, eps=1e-12):
ys = []
for v in xs:
c = math.cos(v)
if abs(c) <
eps:
s = math.sin(v)
if s > 0:
ys.append(float('inf'))
elif s < 0:
ys.append(float('-inf'))
else:
ys.append(float('nan'))
else:
ys.append(math.tan(v))
return ys
Примеры:
x = linspace(0.0,
2 * math.pi, 361)
y_sin =
sin_series(x)
y_cos =
cos_series(x)
y_tan =
tan_series(x)
Итог: у вас два массива — аргументы x и
соответствующие значения y. Если нужен конкретный формат возвращаемых данных,
имена функций или исключений — пришлите точные сигнатуры, подстрою.
Как в с# пишется Пи pi ?
В C# число пи доступно как константа Math.PI.
Примеры:
• Получить
значение: var pi = Math.PI;
• Радианы из градусов: double rad = degrees * Math.PI / 180.0;
• Округление: Math.Round(Math.PI, 6);
• В
интерполированной строке: Console.WriteLine($"{Math.PI}");
Если вы пишете многоформульный код, можно завести
псевдоним:
const double PI =
Math.PI;
var dataArg =
MathSeries.Linspace(0.0, 2 * Math.PI, 36);
var data =
MathSeries.Sin(double[] dataArg) => Evaluate(dataArg, Math.Sin);
Как правильно написать вторую строку? ПЕрвая
правильно.
Во второй строке у вас синтаксис неверный. В C#
нельзя вызывать метод как MathSeries.Sin(double[] dataArg) => … и
одновременно объявлять лямбду. Нужно либо вызвать готовую функцию, либо
передать делегат в универсальный вычислитель.
Варианты корректной записи:
1. Если у
вас есть метод Evaluate, принимающий массив аргументов и делегат функции:
var data =
Evaluate(dataArg, Math.Sin);
2. Если у
MathSeries есть универсальный метод Apply или Map:
var data =
MathSeries.Apply(dataArg, Math.Sin);
// или
var data =
MathSeries.Map(dataArg, Math.Sin);
3. Если
хотите получить синус напрямую без общего Evaluate и у MathSeries есть готовая
обертка Sin(double[]):
var data =
MathSeries.Sin(dataArg);
4. Если
хотите явно лямбда-делегат:
var data =
Evaluate(dataArg, x => Math.Sin(x));
Где Evaluate можно определить так:
static double[]
Evaluate(double[] xs, Func<double, double> f)
{
var ys = new
double[xs.Length];
for (int i = 0; i
< xs.Length; i++)
ys[i] = f(xs[i]);
return ys;
}
С вашей первой строкой это будет работать:
var dataArg =
MathSeries.Linspace(0.0, 2 * Math.PI, 36);
var data =
Evaluate(dataArg, Math.Sin);
Вот так правильно
var dataArg =
MathSeries.Linspace(0.0, 2 * Math.PI, 36);
var data =
MathSeries.Evaluate(dataArg, x=>Math.Sin(x));
Да, так корректно. Можно ещё короче, используя
метод-группу:
var dataArg =
MathSeries.Linspace(0.0, 2 * Math.PI, 36);
var data =
MathSeries.Evaluate(dataArg, Math.Sin);
Обе формы эквивалентны. Если Evaluate имеет сигнатуру
вроде:
static double[]
Evaluate(double[] xs, Func<double, double> f)
то и лямбда x => Math.Sin(x), и метод-группа
Math.Sin подойдут.
Чем отличаются вот эти функции
public static
double[] Linspace(double start, double end, int count)
{
if (count < 2)
throw new ArgumentException("count must be >= 2");
var arr = new
double[count];
double step =
(end - start) / (count - 1);
for (int i = 0; i
< count; i++)
arr[i] = start +
step * i;
return arr;
}
---------
public static
double[] LinspaceHalfOpen(double start, double end, int count)
{
if (count <=
0) throw new ArgumentException("count must be > 0");
if (count == 1)
return new[] { start };
double step =
(end - start) / count;
var arr = new
double[count];
for (int i = 0; i
< count; i++)
arr[i] = start +
i * step;
return arr;
}
------------------
вроде бы сигнатура одинаковая, но результаты разные ?
Обе функции создают равномерную сетку, но разная
интерпретация интервала и шага.
Что делает Linspace:
• Интервал
закрытый: включает и start, и end.
• Шаг
вычисляется как (end - start) / (count - 1).
• При count
>= 2 последний элемент будет ровно end.
• Пример: start=0, end=10, count=6 → шаг 2 → [0, 2, 4, 6, 8, 10].
Что делает LinspaceHalfOpen:
• Интервал
полуоткрытый: включает start, не включает end.
• Шаг
вычисляется как (end - start) / count.
• Последний
элемент будет end - step, а end не попадёт в массив.
• Спец-случай
count=1 — возвращает [start].
• Пример: start=0, end=10, count=6 → шаг ≈ 1.6667 → [0, 1.6667, 3.3333, 5, 6.6667, 8.3333].
Почему результаты разные:
• Разные
формулы шага: деление на (count - 1) vs на count.
• Целевая
семантика различна: закрытая сетка [start, end] против полуоткрытой [start,
end).
Когда какую использовать:
• Нужны обе
границы, например для интерполяции на [a, b] — Linspace.
• Нужна
периодичность или дискретизация без повтора правой границы (удобно для циклов,
FFT-бинов, углового диапазона [0, 2π)) — LinspaceHalfOpen.
Замечания по точности:
• В
Linspace при больших count накопление ошибки минимально: каждый элемент
определяется по формуле start + i*step (это хорошо).
• Если
важна точная последняя точка в Linspace, иногда отдельно присваивают последний
элемент end, чтобы нивелировать накопленную погрешность округления:
arr[count - 1] = end;
Итог: у них одинаковая сигнатура, но разные
договорённости о том, входит ли правая граница, из-за чего отличаются шаг и набор
точек.
Вот хорошая функция.
// 1) Генерация оси X (аналог linspace)
// Включительно: start и end входят в результат.
public static
double[] Linspace(double start, double end, int count)
{
if (count < 2)
throw new ArgumentException("count must be >= 2");
var arr = new
double[count];
double step =
(end - start) / (count - 1);
for (int i = 0; i
< count; i++)
arr[i] = start +
step * i;
return arr;
}
Но мы хотим написать на нее похожую функцию.
public static
double[] Linspace(double start, double end, int count, int
StrartFromThisPosition)
У нас все как в Предыдущей функции, но начинаем
выдавать массив с позиции StrartFromThisPosition и таким же кол-вом членом.
То есть например у нас 5 членов. Параметр
StrartFromThisPosition = 2
И мы начинаем с позиции 2 и далее 3,4,0,1
Давайте напишем
Понятно ?
Да, понятно. Нужно вернуть массив такой же длины
count, элементы те же, что и у обычного Linspace(start, end, count), но порядок
начинается с индекса StrartFromThisPosition и «закольцовывается» по кругу.
Ниже реализация с проверками и циклическим сдвигом. Я
также поправил опечатку в имени параметра на startFromPosition, но если нужно
сохранить оригинальное имя — можно вернуть обратно.
public static
double[] Linspace(double start, double end, int count, int startFromPosition)
{
if (count < 2)
throw new ArgumentException("count
must be >= 2", nameof(count));
// Нормализуем позицию
старта в диапазон [0, count-1]
int s = ((startFromPosition % count) + count) % count;
var arr = new double[count];
double step = (end - start) / (count - 1);
// Заполняем, начиная с
позиции s, затем по кругу
// Индекс
исходной последовательности: i
// Индекс в
результирующем массиве: j, куда кладём элемент i
for (int k = 0; k < count; k++)
{
int i = (s + k) % count; // позиция в "линейном" linspace, откуда берём
int j = k; // позиция в результирующем
массиве, куда кладём
arr[j] = start + step * i;
}
return arr;
}
Пример:
• start =
0, end = 4, count = 5 дает базовую последовательность: [0, 1, 2, 3, 4]
• startFromPosition
= 2 вернет: [2, 3, 4, 0, 1]
Если вы хотите, чтобы вместо перестановки мы
«отрезали» подпоследовательность длины count, начиная с startFromPosition, а
превышающие правую границу вычислялись по формуле дальше (то есть без
циклического возврата), скажите — дам альтернативный вариант.
Теперь мы готовы написать класс для создания Бегущей
синусоиды в RazorPage
Сейчас у нас такой код просто рисует одну синосоиду в
СТранице RAzorPage.
public class
SimpleLineModel : PageModel
{
public
SimpleLineModel()
{
}
public void
OnGet()
{
ViewData["Title"]
= "Simple Line Chart";
RazorChartViewer
viewer = new RazorChartViewer(HttpContext, "chart1");
ViewData["Viewer"]
= viewer;
createChart(viewer);
}
private void
createChart(RazorChartViewer viewer)
{
var dataArg =
MathSeries.Linspace(0.0, 2 * Math.PI, 36);
var data =
MathSeries.Evaluate(dataArg, Math.Sin);
// The labels for
the line chart
string[] labels =
{ "0", "1", "2", "3", "4",
"5", "6", "7", "8", "9",
"10", "11", "12",
"13",
"14", "15", "16", "17", "18",
"19", "20", "21", "22", "23",
"24",
"25",
"26", "27","28", "29", "30",
"31","32","33", "34", "35"};
// Create a
XYChart object of size 250 x 250 pixels
XYChart c = new
XYChart(450, 450);
// Set the
plotarea at (30, 20) and of size 200 x 200 pixels
c.setPlotArea(30,
20, 400, 400);
// Add a line
chart layer using the given data
c.addLineLayer(data);
// Set the labels
on the x axis.
c.xAxis().setLabels(labels);
// Display 1 out
of 3 labels on the x-axis.
c.xAxis().setLabelStep(3);
// Output the
chart
viewer.Image =
c.makeWebImage(Chart.SVG);
// Include tool
tip for the chart
viewer.ImageMap =
c.getHTMLImageMap("", "",
"title='Hour
{xLabel}: Traffic {value} GBytes'");
}
}
-------------------------------
мы создаем класс типа SingleTone с полем CurrentIndex
= 0
И на каждый вызов Charts мы вызывать функцию
var dataArg =
MathSeries.Linspace(0.0, 2 * Math.PI, 36, CurrentIndex);
var data =
MathSeries.Evaluate(dataArg, Math.Sin);
По истечении 36 попыток, мы снова начинаем с 0.
CurrentIndex у нас храниться в классе singletone
------
Вроде понятно объснил.
Нужен класс, чтобы в Program.cs зарегистрировать его
как SingleTone и хранить там состояние нашего CurrenIndex.
И при каждом выводе страницы Синусоида будет стартовать
с увеличенного индекса и как бы сдвигаться влево.
После того как это заработает - надо будет в странице
включить автоматический Refresh страницы через 1 секунду. И у нас получиться
живая синусоида.
Понял задачу. Ниже даю готовое решение по шагам:
сервис-синглтон с CurrentIndex, регистрация в Program.cs, использование в
PageModel, и автообновление Razor Page раз в 1 секунду.
1. Сервис-синглтон
для хранения состояния индекса
Создайте класс RunningSineState. Он потокобезопасно
хранит текущее значение и выдает “циклический” индекс от 0 до 35.
public interface
IRunningSineState
{
int NextIndex(int
period);
int CurrentIndex
{ get; }
}
public class
RunningSineState : IRunningSineState
{
private int _currentIndex = 0;
// Вернёт индекс для следующего запроса
(0..period-1), затем по кругу
public int
NextIndex(int period)
{
// атомарно
увеличиваем и берём по модулю
int next = Interlocked.Increment(ref _currentIndex);
// после инкремента next
начинается с 1, поэтому нормализуем
int normalized = next % period;
return normalized;
}
public int
CurrentIndex => Volatile.Read(ref _currentIndex) % 36;
}
Примечание:
• Если
хотите начинать строго с 0 на первом рендере, можете сначала вернуть текущее
значение, а потом инкрементировать. Тогда
используйте:
public int
NextIndex(int period)
{
int cur =
Interlocked.Increment(ref _currentIndex) - 1;
if (cur < 0)
cur = 0;
return cur %
period;
}
И при инициализации _currentIndex = 0.
1. Регистрация
в Program.cs как Singleton
В Program.cs добавьте:
builder.Services.AddSingleton<irunningsinestate,
runningsinestate>();
1. Обновление
PageModel для использования состояния
Инжектим IRunningSineState и используем NextIndex для
сдвига.
public class
SimpleLineModel : PageModel
{
private readonly
IRunningSineState _state;
private const int
Points = 36;
public
SimpleLineModel(IRunningSineState state)
{
_state = state;
}
public void
OnGet()
{
ViewData["Title"] = "Simple
Line Chart";
// Получаем сдвиг по кругу 0..35
int currentIndex =
_state.NextIndex(Points);
RazorChartViewer viewer = new
RazorChartViewer(HttpContext, "chart1");
ViewData["Viewer"] = viewer;
createChart(viewer, currentIndex);
}
private void
createChart(RazorChartViewer viewer, int currentIndex)
{
//
Генерируем аргументы с фазовым сдвигом через currentIndex
//
Предполагается, что у вас есть перегрузка Linspace(start, end, count,
startFromThisPosition)
// которая
реализует циклический сдвиг. Если такой нет, даю ниже альтернативу.
var dataArg = MathSeries.Linspace(0.0, 2 * Math.PI,
Points, currentIndex);
var data = MathSeries.Evaluate(dataArg,
Math.Sin);
string[] labels = Enumerable.Range(0,
Points).Select(i => i.ToString()).ToArray();
XYChart c = new XYChart(450, 450);
c.setPlotArea(30, 20, 400, 400);
c.addLineLayer(data);
c.xAxis().setLabels(labels);
c.xAxis().setLabelStep(3);
viewer.Image = c.makeWebImage(Chart.SVG);
viewer.ImageMap =
c.getHTMLImageMap("", "", "title='Hour {xLabel}:
Traffic {value} GBytes'");
}
}
Если в вашей библиотеке нет Linspace со сдвигом
Тогда можно сдвиг реализовать явно. Идея: базовый
массив аргументов от 0 до 2π, затем “повернуть” его на currentIndex по кругу.
private static
double[] LinspaceShifted(double start, double end, int count, int shift)
{
var baseArgs =
MathSeries.Linspace(start, end, count); // равномерно [0, 2π)
shift = ((shift %
count) + count) % count;
if (shift == 0)
return baseArgs;
var rotated = new
double[count];
// порядок: [shift, shift+1, ..., count-1, 0, 1, ..., shift-1]
int idx = 0;
for (int i =
shift; i < count; i++) rotated[idx++] = baseArgs[i];
for (int i = 0; i
< shift; i++) rotated[idx++] = baseArgs[i];
return rotated;
}
И в createChart:
var dataArg =
LinspaceShifted(0.0, 2 * Math.PI, Points, currentIndex);
var data =
MathSeries.Evaluate(dataArg, Math.Sin);
Важно: Такой “сдвиг массива аргументов” визуально
даст эффект бегущей синусоиды, потому что значения sin вычисляются в новом
порядке.
Альтернатива с фазовым сдвигом
Можно вместо перестановки точек применять фазовый
сдвиг равномерной сетке: sin(x + phi). Тогда не нужно переставлять X,
достаточно изменить функцию:
double phase = 2 * Math.PI * currentIndex / Points;
var dataArg =
MathSeries.Linspace(0.0, 2 * Math.PI, Points);
var data =
MathSeries.Evaluate(dataArg, x => Math.Sin(x + phase));
Оба подхода дают “пробег” по синусоиде. Вы выбираете
тот, что соответствует вашей идее “стартовать с увеличенного индекса”.
1. Автообновление
Razor Page каждые 1 секунду
В файле .cshtml для этой страницы добавьте
мета-рефреш в секцию Head или простой скрипт:
Вариант с meta:
<head>
<meta
http-equiv="refresh" content="1" />
</head>
Или скриптом:
<script>
setTimeout(function
() { window.location.reload(); }, 1000);
</script>
Meta проще и надежнее для быстрого прототипа.
1. Итоги по
использованию
• Регистрируете
RunningSineState как Singleton.
• В
PageModel через конструктор получаете IRunningSineState.
• На каждом OnGet берёте currentIndex = _state.NextIndex(36).
• Создаёте данные или через Linspace(..., currentIndex), или через Linspace + фазовый сдвиг.
• В
Razor-странице включаете автообновление раз в 1 секунду.
Если хотите, могу адаптировать код под ваш конкретный
MathSeries (подсказать сигнатуры, если дадите её), и добавить защиту от гонок
при одновременных запросах из разных клиентов (например, отдельные состояния на
пользователя через сессию).
@* @page
@model RazorChartDirApp07.Pages.SimpleLineModel
@{
} *@
@page
@model SimpleLineModel
@using ChartDirector
@{
Layout =
"_Layout";
}
@{
if
(ViewBag.Viewer is Array)
{
// Display
multiple charts
for (int i = 0; i
< ViewBag.Viewer.Length; ++i)
{
@:@Html.Raw(ViewBag.Viewer[i].RenderHTML())
}
}
else
{
// Display one
chart only
@:@Html.Raw(ViewBag.Viewer.RenderHTML())
}
}
В какое место страницы добавить вот это
<head>
<meta
http-equiv="refresh" content="1" />
</head>
Или скриптом:
<script>
setTimeout(function
() { window.location.reload(); }, 1000);
</script>
?
Коротко:
• Метатег
refresh добавляется в секцию <head> итогового HTML-документа. В Razor это
делается через секцию Head в макете (_Layout.cshtml).
• Скрипт
перезагрузки ставьте в конец <body> (или в секцию Scripts), чтобы не
мешать рендерингу графика.
Если у вас есть макет _Layout.cshtml, правильнее не
дублировать <head> на странице, а воспользоваться секциями.
Вариант 1. Через метатег refresh в секции Head:
1. В
_Layout.cshtml убедитесь, что есть вызов @RenderSection("Head",
required: false) внутри <head>:
<head>
...
@RenderSection("Head",
required: false)
</head>
2. В вашей
странице добавьте секцию:
@section Head {
<meta
http-equiv="refresh" content="1" />
}
Вариант
2. Через JS-таймер:
1. В _Layout.cshtml убедитесь, что есть @RenderSection("Scripts", required: false) перед </body>:
<body>
...
@RenderSection("Scripts",
required: false)
</body>
2. В вашей странице:
@section Scripts
{
<script>
setTimeout(function
() { window.location.reload(); }, 1000);
</script>
}
Если у вас нет секций в макете, то:
• метатег
можно временно вставить прямо в _Layout.cshtml внутри <head>;
• скрипт —
внизу _Layout.cshtml перед закрывающим .
Советы:
• Не
вставляйте отдельный <head> прямо в контент страницы, если используется
Layout — это создаст вложенный head и некорректный HTML.
• Для
локальной автообновляемой страницы используйте JS-таймер. Он гибче: можно
останавливать, менять интервал, не трогает историю навигации.
У меня сейчас все работает.
Но конечно, видно что обновляется вся страница со
всеми менюшками.
Выглядит так себе.
Вот если бы можно было только картинку, например.
ИЛи может только эту локальную страницу.
"Для локальной автообновляемой страницы
используйте JS-таймер. Он гибче: можно останавливать, менять интервал, не
трогает историю навигации."
Что это за волшебный JS-таймер.
?
JS-таймер — это вызов setInterval или setTimeout в
браузере. Он позволяет запускать ваш код через заданные интервалы, без полной
перезагрузки страницы. С его помощью лучше делать частичное обновление: менять
только нужный блок (например, картинку-график), а не весь layout.
Базовые варианты:
• setInterval(fn,
ms): повторяет вызов fn каждые ms миллисекунд, пока вы его не остановите.
• setTimeout(fn,
ms): один раз вызовет fn через ms миллисекунд. Часто используют рекурсивно,
чтобы гибко менять интервал на лету.
Как обновлять только картинку без перезагрузки всей
страницы:
1. Если
картинка доступна по URL, который отдает свежий PNG/JPEG/SVG:
• Обновляйте
src с cache-buster параметром, чтобы браузер не брал из кэша.
Пример:
<script>
const img =
document.getElementById('chart');
let timerId =
null;
function
refreshImage() {
const baseUrl =
'/charts/sine.png'; // ваш
эндпоинт
const cacheBuster
= Date.now(); // можно Math.random()
img.src =
${baseUrl}?t=${cacheBuster};
}
function start()
{
if (timerId)
return;
timerId =
setInterval(refreshImage, 1000); // каждые 1 сек
}
function stop() {
if (!timerId)
return;
clearInterval(timerId);
timerId = null;
}
// стартуем при загрузке
document.addEventListener('DOMContentLoaded',
() => {
start();
});
<img
id="chart" src="/charts/sine.png" alt="Синусоида" />
1. Если
картинка генерируется контроллером/handler’ом:
• Точно так
же добавляйте ?t=... в src, а на сервере отдавайте свежие байты.
• Убедитесь,
что сервер не ставит сильные Cache-Control заголовки, иначе обновления могут не
доходить.
1. Если
вместо картинки вы рисуете на Canvas:
• Рисуйте
данные в <canvas> внутри setInterval/setTimeout, а данные подкачивайте
через fetch:
<canvas
id="canvas" width="800"
height="300"></canvas>
<script>
const ctx =
document.getElementById('canvas').getContext('2d');
let timerId =
null;
async function
fetchData() {
const res = await fetch('/api/sine'); // верните
массив точек или последнее значение
return await
res.json();
}
async function
tick() {
const data =
await fetchData();
draw(data); // ваша функция отрисовки на canvas
}
function start()
{
if (timerId)
return;
timerId =
setInterval(tick, 1000);
}
function stop() {
if (!timerId)
return;
clearInterval(timerId);
timerId = null;
}
document.addEventListener('visibilitychange',
() => {
if
(document.hidden) stop();
else start();
});
document.addEventListener('DOMContentLoaded',
start);
</script>
1. Если
хотите обновлять часть HTML блока:
• Используйте
fetch для получения фрагмента и заменяйте innerHTML нужного контейнера:
<div
id="partial"></div>
<script>
let timerId =
null;
async function
refreshPartial() {
const res = await
fetch('/widget/partial'); // пусть контроллер вернет HTML фрагмент
const html =
await res.text();
document.getElementById('partial').innerHTML
= html;
}
function start()
{
if (timerId)
return;
timerId =
setInterval(refreshPartial, 1000);
}
function stop() {
if (!timerId)
return;
clearInterval(timerId);
timerId = null;
}
document.addEventListener('DOMContentLoaded',
start);
</script>
Почему это лучше, чем meta refresh:
• Обновляется
только нужный элемент, меню и layout остаются.
• Можно легко
остановить обновление (кнопкой, при скрытии вкладки, при ошибках).
• Контролируете
интервал и стратегию бэкенд-запросов.
• Не
засоряет историю навигации перезагрузками.
Практические советы:
• Для
плавности используйте setTimeout вместо setInterval, если хотите адаптировать
интервал в зависимости от времени запроса:
let running =
false;
async function
loop() {
if (running)
return;
running = true;
try {
await
refreshPartial(); // или refreshImage()/tick()
} finally {
running = false;
setTimeout(loop,
1000); // старт следующего цикла
}
}
document.addEventListener('DOMContentLoaded',
loop);
• Добавьте обработку ошибок, чтобы в случае 500/нет сети не спамить запросами.
• При смене
вкладок ставьте паузу (visibilitychange), экономит ресурсы.
• Если это
ASP.NET MVC/Razor:
• Возвращайте
PartialView для фрагмента HTML.
• Либо
отдавайте JSON и рисуйте на Canvas/SVG.
• Если
нужно сервер-пуш и минимальная задержка, рассмотрите SignalR или WebSocket для
событийных обновлений без опроса. Это вообще исключит таймер, но сложнее в
настройке.
Если подскажете, что именно обновляете (URL картинки,
partial endpoint, Canvas или SVG), дам точный пример под ваш стек и разметку.
Создается и обновляется у нас вот так
namespace
RazorChartDirApp07.Pages
{
public class
SimpleLineModel : PageModel
{
private readonly
IRunningSineState _state;
private readonly
ILogger<IndexModel> _logger;
public
SimpleLineModel(IRunningSineState state, ILogger<IndexModel> logger)
{
_state = state;
_logger = logger;
}
public void
OnGet()
{
ViewData["Title"]
= "Simple Line Chart";
RazorChartViewer
viewer = new RazorChartViewer(HttpContext, "chart1");
ViewData["Viewer"]
= viewer;
createChart(viewer);
}
private void
createChart(RazorChartViewer viewer)
{
_logger.Log(LogLevel.Information,
"CurrentIndex: " + _state.CurrentIndex.ToString());
var dataArg =
MathSeries.Linspace(0.0, 2 * Math.PI, 36, _state.CurrentIndex);
var data =
MathSeries.Evaluate(dataArg, Math.Sin);
var nextIndex =
_state.NextIndex(36);
_logger.Log(LogLevel.Information,
"CurrentIndex: " + _state.CurrentIndex.ToString());
// The labels for
the line chart
string[] labels =
{ "0", "1", "2", "3", "4",
"5", "6", "7", "8", "9",
"10", "11", "12",
"13",
"14", "15", "16", "17", "18",
"19", "20", "21", "22", "23",
"24",
"25",
"26", "27","28", "29", "30",
"31","32","33", "34", "35"};
// Create a
XYChart object of size 250 x 250 pixels
XYChart c = new
XYChart(450, 450);
// Set the
plotarea at (30, 20) and of size 200 x 200 pixels
c.setPlotArea(30,
20, 400, 400);
// Add a line
chart layer using the given data
c.addLineLayer(data);
// Set the labels
on the x axis.
c.xAxis().setLabels(labels);
// Display 1 out
of 3 labels on the x-axis.
c.xAxis().setLabelStep(3);
// Output the
chart
viewer.Image =
c.makeWebImage(Chart.SVG);
// Include tool
tip for the chart
viewer.ImageMap =
c.getHTMLImageMap("", "",
"title='Hour
{xLabel}: Traffic {value} GBytes'");
}
}
}
Обновление на локальной странице,
@page
@model SimpleLineModel
@using
ChartDirector
@{
Layout =
"_Layout";
}
@{
if
(ViewBag.Viewer is Array)
{
// Display
multiple charts
for (int i = 0; i
< ViewBag.Viewer.Length; ++i)
{
@:@Html.Raw(ViewBag.Viewer[i].RenderHTML())
}
}
else
{
// Display one
chart only
@:@Html.Raw(ViewBag.Viewer.RenderHTML())
}
}
@section Scripts
{
<script>
setTimeout(function
() { window.location.reload(); }, 1000);
</script>
}
но почему то затрагивает весь окно браузера,
потому что мелькает и меню и значок REfresh браузера.
Под “JS-таймером” я имел в виду обычные функции
таймера в браузере: setInterval и setTimeout. Они просто планируют вызов вашего
кода через заданный интервал. Сейчас вы используете таймер, но дергаете
window.location.reload(), из за этого перезагружается вся страница, мигает меню
и иконка Refresh.
Если цель — автообновлять только график, оставив
страницу и меню статичными, замените полную перезагрузку на частичную подгрузку
HTML или данных и обновление DOM. Есть три рабочих варианта под Razor Pages +
ChartDirector:
1. Подгружать
готовый HTML графика из отдельного endpoint
• В
SimpleLineModel рендерить только график (как сейчас), но вынести это в
отдельную страницу/handler, например /SimpleLine?handler=Chart.
• В
основной странице оставить контейнер <div id="chartHost"></div>.
• Каждую
секунду подменять innerHTML этого контейнера свежим HTML через fetch.
Пример:
Razor (страница с меню и контейнером):
• Вместо
@Html.Raw(ViewBag.Viewer.RenderHTML()):
<div
id="chartHost"></div>
@section Scripts
{
<script>
async function
refreshChart() {
try {
const r = await
fetch('@Url.Page("SimpleLine", null, new { handler =
"Chart" }, Context.Request.Scheme)', { cache: "no-store"
});
const html =
await r.text();
document.getElementById('chartHost').innerHTML
= html;
} catch(e) {
console.error(e);
}
}
refreshChart();
const timerId =
setInterval(refreshChart, 1000);
// при навигации можно clearInterval(timerId)
</script>
}
C# (в той же Razor Page):
public
IActionResult OnGetChart()
{
ViewData["Title"]
= "Simple Line Chart";
var viewer = new
RazorChartViewer(HttpContext, "chart1");
createChart(viewer);
// Вернем только HTML графика без layout
return
Content(viewer.RenderHTML(), "text/html");
}
1. Возвращать
только данные (JSON) и перерисовывать график клиентом
• Написать
OnGetData(), который отдает массив значений синуса и метки.
• На
клиенте рисовать через Canvas/SVG библиотеку (Chart.js, ECharts, uPlot). Плюс:
вообще не дергаете ChartDirector на каждом тике; минус: надо один раз настроить
клиентскую библиотеку.
1. Если
ChartDirector отдает URL на изображение/SVG, обновлять только src
• Если
viewer.RenderHTML() вставляет <img src="..." />, то можно
менять src на тот же URL с anti-cache параметром, не перерисовывая остальной
DOM.
Пример:
• Оберните
<img /> в контейнер с id, найдите <img /> и раз в секунду делайте
img.src = baseUrl + "?t=" + Date.now();
• Если это
SVG-вставка, можно подменять весь innerHTML контейнера через fetch как в
варианте 1.
Почему мигал браузер
• window.location.reload()
полностью перезагружает документ, поэтому перерисовывается весь layout, меню,
обновляется вкладка — это норма для полной перезагрузки.
• Таймер
тут лишь триггер; “волшебство” в том, что вместо reload вы вызываете частичную
загрузку и замену нужного куска DOM.
Минимальная правка вашего кода
• Замените
блок Scripts на:
@section Scripts
{
<script>
async function
refreshChart() {
try {
const res = await
fetch('@Url.Page("SimpleLine", null, new { handler =
"Chart" }, Context.Request.Scheme)', { cache: "no-store"
});
const html =
await res.text();
document.getElementById('chartHost').innerHTML
= html;
} catch (err) {
console.error('Refresh
failed', err);
}
}
refreshChart();
const timerId =
setInterval(refreshChart, 1000);
</script>
}
• И в
разметке вместо текущего блока с RenderHTML выведите только контейнер:
<div id="chartHost"></div>
• Добавьте
в PageModel handler OnGetChart(), который создает viewer и возвращает
Content(viewer.RenderHTML(), "text/html"); как показано выше.
Так вы сохраните стабильный layout, а обновляться
будет только график. Если расскажете, какой HTML генерирует RazorChartViewer
(img или inline svg), подскажу самый простой способ адресной подмены. Если
хотите совсем без опроса, можно перейти на SignalR/WebSocket и пушить
обновления по событию.
Вот мой код.
@page
@model
SimpleLineModel
@using
ChartDirector
@{
Layout =
"_Layout";
}
@{
if (ViewBag.Viewer
is Array)
{
// Display
multiple charts
for (int i = 0; i
< ViewBag.Viewer.Length; ++i)
{
@:@Html.Raw(ViewBag.Viewer[i].RenderHTML())
}
}
else
{
// Display one
chart only
@:@Html.Raw(ViewBag.Viewer.RenderHTML())
}
}
@section Scripts
{
<script>
setTimeout(function
() { window.location.reload(); }, 1000);
</script>
}
--------------
Напишите Ваш вариант правильного кода
Проблема в том, что вы перезагружаете всю страницу.
Вместо этого обновляйте только сам график, не трогая layout, меню и т. п. В
ChartDirector RazorChartViewer умеет отдавать HTML с тегом img или
интерактивный svg. Самый универсальный вариант — подменять src у img или data у
svg через периодический запрос на сервер, добавляя cache-buster, либо дергать
action, который возвращает только HTML графика и заменять его во враппере.
Ниже два подхода. Выберите тот, который соответствует
тому, как RenderHTML выводит график у вас.
Вариант A. Если RenderHTML генерирует <img /> с
фиксированным id (обычно viewer.Id), достаточно обновлять src
В
Razor:
@page
@model
SimpleLineModel
@using
ChartDirector
@{
Layout =
"_Layout";
}
<div
id="chart-container">
@if
(ViewBag.Viewer is Array)
{
for (int i = 0; i
< ViewBag.Viewer.Length; ++i)
{
@Html.Raw(ViewBag.Viewer[i].RenderHTML())
}
}
else
{
@Html.Raw(ViewBag.Viewer.RenderHTML())
}
</div>
@section Scripts {
<script>
// Найдём все изображения графиков внутри контейнера
function
refreshCharts() {
const imgs =
document.querySelectorAll('#chart-container img');
const now =
Date.now();
imgs.forEach(img
=> {
// добавляем параметр, чтобы обойти кэш
const url = new
URL(img.src, window.location.origin);
url.searchParams.set('_t',
now.toString());
img.src =
url.toString();
});
// Аналогично для встроенных svg, если ChartDirector отдает
их через <object data="...">
const objects =
document.querySelectorAll('#chart-container
object[type="image/svg+xml"]');
objects.forEach(obj => {
const url = new URL(obj.data,
window.location.origin);
url.searchParams.set('_t',
now.toString());
obj.data =
url.toString();
});
}
// Обновляем раз в секунду, без перезагрузки страницы
setInterval(refreshCharts, 1000);
</script>
}
Пояснение: ChartDirector обычно отдает изображение
через handler-URL, например /Chart?img=... В таком случае достаточно менять
query-параметр, чтобы сервер сгенерировал свежий график, а браузер не взял его
из кэша.
Вариант B. Если RenderHTML отдает сложную разметку и
нужно полностью пересобрать HTML viewer
Сделайте отдельный endpoint, который вернет только
HTML графика, и подменяйте innerHTML контейнера.
Контроллер/страница (пример для MVC, адаптируйте под
ваши хендлеры PageModel):
public class
ChartsController : Controller
{
[HttpGet]
public
IActionResult LiveChart()
{
// Генерируем новый ChartDirector chart и viewer
var c =
ChartFactory.BuildChart(); // ваш код генерации
var viewer = new
RazorChartViewer(HttpContext, "chart1");
viewer.setChart(c,
ChartDirector.Chart.PNG);
// Возвращаем ТОЛЬКО HTML viewer
return
Content(viewer.RenderHTML(), "text/html; charset=utf-8");
}
}
В
Razor основной страницы:
@page
@model
SimpleLineModel
@using
ChartDirector
@{
Layout =
"_Layout";
}
<div
id="chart-container">
@Html.Raw(ViewBag.Viewer.RenderHTML())
</div>
@section Scripts
{
<script>
async function
refreshChartHtml() {
try {
const resp =
await fetch('/Charts/LiveChart?_t=' + Date.now(), { cache: 'no-store' });
if (!resp.ok)
return;
const html =
await resp.text();
const container =
document.getElementById('chart-container');
container.innerHTML
= html;
} catch (e) {
console.error('refresh
error', e);
}
}
setInterval(refreshChartHtml,
1000);
</script>
}
Замечания:
• Уберите window.location.reload. Обновляйте только содержимое контейнера.
• Если у
вас несколько графиков, дайте им разные id и обновляйте каждый по своему
endpoint или перебором, как в варианте A.
• Если
используете MVC Layout, Scripts-секция остаётся как есть.
• Чтобы
исключить мерцание, можно сначала создавать новый элемент вне DOM и только
потом атомарно заменять контейнер, либо менять src у существующего img, как в
варианте A, что обычно вообще не даёт мерцания.
ТОгда сделаем так.
Я Вкам пришли два листинга и Вы посмотрите, а то я не
понимаю, где что исправлять.
SimpleLine.cshtml
@page
@model
SimpleLineModel
@using
ChartDirector
@{
Layout =
"_Layout";
}
@{
if
(ViewBag.Viewer is Array)
{
// Display
multiple charts
for (int i = 0; i
< ViewBag.Viewer.Length; ++i)
{
@:@Html.Raw(ViewBag.Viewer[i].RenderHTML())
}
}
else
{
// Display one
chart only
@:@Html.Raw(ViewBag.Viewer.RenderHTML())
}
}
@section Scripts
{
<script>
setTimeout(function
() { window.location.reload(); }, 1000);
</script>
}
------------------
SimpleLine.cshtml.cs
--------------------------
using
ChartDirector;
using LinSpace;
using
Microsoft.AspNetCore.Mvc;
using
Microsoft.AspNetCore.Mvc.RazorPages;
using
RazorChartDirApp07.Services;
namespace RazorChartDirApp07.Pages
{
public class
SimpleLineModel : PageModel
{
private readonly
IRunningSineState _state;
private readonly
ILogger<IndexModel> _logger;
public
SimpleLineModel(IRunningSineState state, ILogger<IndexModel> logger)
{
_state = state;
_logger = logger;
}
public void
OnGet()
{
ViewData["Title"]
= "Simple Line Chart";
RazorChartViewer
viewer = new RazorChartViewer(HttpContext, "chart1");
ViewData["Viewer"]
= viewer;
createChart(viewer);
}
private void
createChart(RazorChartViewer viewer)
{
//
_logger.Log(LogLevel.Information, "CurrentIndex: " +
_state.CurrentIndex.ToString());
var dataArg =
MathSeries.Linspace(0.0, 2 * Math.PI, 36, _state.CurrentIndex);
var data =
MathSeries.Evaluate(dataArg, Math.Sin);
var nextIndex =
_state.NextIndex(36);
//
_logger.Log(LogLevel.Information, "CurrentIndex: " +
_state.CurrentIndex.ToString());
// The labels for
the line chart
string[] labels =
{ "0", "1", "2", "3", "4",
"5", "6", "7", "8", "9",
"10", "11", "12",
"13",
"14", "15", "16", "17", "18",
"19", "20", "21", "22", "23",
"24",
"25",
"26", "27","28", "29", "30",
"31","32","33", "34", "35"};
// Create a
XYChart object of size 250 x 250 pixels
XYChart c = new
XYChart(450, 450);
// Set the
plotarea at (30, 20) and of size 200 x 200 pixels
c.setPlotArea(30,
20, 400, 400);
// Add a line
chart layer using the given data
c.addLineLayer(data);
// Set the labels
on the x axis.
c.xAxis().setLabels(labels);
// Display 1 out
of 3 labels on the x-axis.
c.xAxis().setLabelStep(3);
// Output the
chart
viewer.Image =
c.makeWebImage(Chart.SVG);
// Include tool
tip for the chart
viewer.ImageMap =
c.getHTMLImageMap("", "",
"title='Hour
{xLabel}: Traffic {value} GBytes'");
}
}
}
-----------------
Напишите Ваш вариант обновления Графика.
Там компонент от ChartDirector задествован.
viewer.Image =
c.makeWebImage(Chart.SVG);
// Include tool
tip for the chart
viewer.ImageMap =
c.getHTMLImageMap("", "",
"title='Hour
{xLabel}: Traffic {value} GBytes'");
---------------
Ваш код и я его вставлю и попробуем
Ниже два рабочих варианта без принудительного reload
страницы каждую секунду.
Вариант A: простая периодическая подмена SVG через
fetch (подходит «из коробки» с Razor Pages и ChartDirector)
Идея: серверный обработчик на той же странице возвращает
только SVG + imagemap без layout, а клиент по таймеру запрашивает его и
заменяет содержимое контейнера. Мерцания обычно нет, так как меняется innerHTML
одного блока.
SimpleLine.cshtml
@page
@model SimpleLineModel
@using ChartDirector
@{
Layout = "_Layout";
}
<div
id="chart-container">
@{
if
(ViewBag.Viewer is Array)
{
for (int i = 0; i
< ViewBag.Viewer.Length; ++i)
{
@:@Html.Raw(ViewBag.Viewer[i].RenderHTML())
}
}
else
{
@:@Html.Raw(ViewBag.Viewer.RenderHTML())
}
}
</div>
@section Scripts {
<script>
// Период обновления в миллисекундах
const refreshMs =
1000;
async function
refreshChart() {
try {
// Поддержка anti-cache
const url =
${window.location.pathname}?handler=Partial&_=${Date.now()};
const resp =
await fetch(url, { headers: { "X-Requested-With": "fetch" }
});
if (!resp.ok)
return;
const html =
await resp.text();
// Атомарная замена: готовим off-DOM
const temp = document.createElement('div');
temp.innerHTML = html;
const container =
document.getElementById('chart-container');
if (container) {
container.innerHTML = temp.innerHTML;
}
} catch (e) {
// Лог молча,
чтобы не шуметь
// console.error(e);
}
}
// Стартовый цикл
setInterval(refreshChart,
refreshMs);
}
SimpleLine.cshtml.cs
using
ChartDirector;
using LinSpace;
using
Microsoft.AspNetCore.Mvc;
using
Microsoft.AspNetCore.Mvc.RazorPages;
using
RazorChartDirApp07.Services;
namespace
RazorChartDirApp07.Pages
{
public class
SimpleLineModel : PageModel
{
private readonly
IRunningSineState _state;
private readonly
ILogger<simplelinemodel> _logger;
public
SimpleLineModel(IRunningSineState state, ILogger<simplelinemodel>
logger)
{
_state = state;
_logger = logger;
}
// Обычная загрузка страницы
public void OnGet()
{
ViewData["Title"] =
"Simple Line Chart";
var viewer = new
RazorChartViewer(HttpContext, "chart1");
ViewData["Viewer"] = viewer;
CreateChart(viewer);
}
//
Частичный возврат только HTML графика (SVG + imagemap) без Layout
public ContentResult OnGetPartial()
{
var viewer = new
RazorChartViewer(HttpContext, "chart1");
CreateChart(viewer);
// RenderHTML возвращает <div> с <img />/<svg> и <map>, как нужно ChartDirector-у
var html = viewer.RenderHTML();
return Content(html, "text/html;
charset=utf-8");
}
private void CreateChart(RazorChartViewer
viewer)
{
var dataArg = MathSeries.Linspace(0.0,
2 * Math.PI, 36, _state.CurrentIndex);
var data = MathSeries.Evaluate(dataArg,
Math.Sin);
_state.NextIndex(36);
string[] labels = Enumerable.Range(0,
36).Select(i => i.ToString()).ToArray();
var c = new XYChart(450, 450);
c.setPlotArea(30, 20, 400, 400);
c.addLineLayer(data);
c.xAxis().setLabels(labels);
c.xAxis().setLabelStep(3);
// SVG даёт наилучшее
качество и малый размер
viewer.Image = c.makeWebImage(Chart.SVG);
// Важен ImageMap для tooltip
viewer.ImageMap =
c.getHTMLImageMap("", "", "title='Hour {xLabel}:
Traffic {value} GBytes'");
}
}
}
Как это работает
• GET
/SimpleLine: полная страница с текущим графиком.
• Каждую
секунду JS делает GET /SimpleLine?handler=Partial и заменяет содержимое
div#chart-container новым HTML, который отрендерил ChartDirector через
viewer.RenderHTML().
• Никаких
reload, страница не мерцает.
Вариант B: SignalR push без опроса (пригодно, если
хотите минимальную задержку и трафик)
Идея: при событии на сервере вы создаёте SVG и
отправляете клиентам HTML блока chart через SignalR. Клиент просто заменяет
innerHTML контейнера.
Примерно:
• Добавьте
хаб ChartHub.
• В
SimpleLineModel.CreateChart собирайте html = viewer.RenderHTML() и по событию
дергайте hubContext.Clients.All.SendAsync("chartHtml", html).
• На
клиенте подключите SignalR и подпишитесь на chartHtml, заменяя
div#chart-container.innerHTML.
Если хотите, напишу точные классы для SignalR (Hub,
Program.cs, клиентский скрипт) под ваш проект.
Спасибо, сейчас попробую Вариант А.
Отлично. Для Variant A с ImageMap (подсказками) код
может быть таким:
C# (генерация графика и картинка с тултипами)
• Создание
чарта и включение map с подсказками:
var c = new XYChart(720, 400);
// ... настройка осей/данных/слоев ...
// Пример слоя
var layer =
c.addLineLayer(data, 0x4472C4, "Traffic");
layer.setXData(labels); // если используете массив
меток по оси X // Тултипы для всей серии:
// {xLabel} — метка по оси X, {value} — значение
точки, {dataSetName} — имя набора
string imageMap = c.getHTMLImageMap(
"", // url по клику (пусто — без перехода)
"", //
target
"title='Hour
{xLabel}: {dataSetName} {value|2} GBytes'" // тултип
); // Рендерим график в <img /> и сохраняем
ImageMap
var viewer = new
WebChartViewer();
viewer.Image =
c.makeSession(Session);
viewer.ImageMap =
imageMap;
ASP.NET
Razor/ASPX (вставка
в страницу)
• В том
месте, где у вас выводится график:
@Html.Raw(viewer.RenderHTML())
Комментарии и нюансы
• Если
хотите разные подсказки для разных серий, можно задавать title на уровне слоев
или использовать замены вроде {dataSetName}, {x|hh:nn}, {value|2}.
• Форматирование:
{value|2} округлит до 2 знаков. Для дат используйте {x|yyyy-MM-dd HH:mm}.
• Если
график обновляется через замену src у <img />, не забудьте также заменить
соседний <map name="..."> и атрибут usemap картинки.
viewer.RenderHTML() вернет сразу и <img />, и соответствующий
<map>, так что проще заменять весь html-блок.
• Если
используете AJAX/SignalR, передавайте именно viewer.RenderHTML(), чтобы тултипы
оставались рабочими.
Если дадите ваш метод CreateChart/рендеринг, вставлю
точный код с учетом ваших классов и версии ChartDirector.
Вот Вам CreateChart в файле SImpleLine.cshtml.cs
using ChartDirector;
using LinSpace;
using
Microsoft.AspNetCore.Mvc;
using
Microsoft.AspNetCore.Mvc.RazorPages;
using
RazorChartDirApp07.Services;
namespace
RazorChartDirApp07.Pages
{
public class
SimpleLineModel : PageModel
{
private readonly
IRunningSineState _state;
private readonly
ILogger<IndexModel> _logger;
public
SimpleLineModel(IRunningSineState state, ILogger<IndexModel> logger)
{
_state = state;
_logger = logger;
}
public void
OnGet()
{
ViewData["Title"]
= "Simple Line Chart";
RazorChartViewer
viewer = new RazorChartViewer(HttpContext, "chart1");
ViewData["Viewer"]
= viewer;
createChart(viewer);
}
private void
createChart(RazorChartViewer viewer)
{
//
_logger.Log(LogLevel.Information, "CurrentIndex: " +
_state.CurrentIndex.ToString());
var dataArg =
MathSeries.Linspace(0.0, 2 * Math.PI, 36, _state.CurrentIndex);
var data =
MathSeries.Evaluate(dataArg, Math.Sin);
var nextIndex =
_state.NextIndex(36);
//
_logger.Log(LogLevel.Information, "CurrentIndex: " +
_state.CurrentIndex.ToString());
// The labels for
the line chart
string[] labels =
{ "0", "1", "2", "3", "4",
"5", "6", "7", "8", "9",
"10", "11", "12",
"13",
"14", "15", "16", "17", "18",
"19", "20", "21", "22", "23",
"24",
"25",
"26", "27","28", "29", "30",
"31","32","33", "34", "35"};
// Create a
XYChart object of size 250 x 250 pixels
XYChart c = new
XYChart(450, 450);
// Set the
plotarea at (30, 20) and of size 200 x 200 pixels
c.setPlotArea(30,
20, 400, 400);
// Add a line
chart layer using the given data
c.addLineLayer(data);
// Set the labels
on the x axis.
c.xAxis().setLabels(labels);
// Display 1 out
of 3 labels on the x-axis.
c.xAxis().setLabelStep(3);
// Output the
chart
viewer.Image =
c.makeWebImage(Chart.SVG);
// Include tool
tip for the chart
viewer.ImageMap =
c.getHTMLImageMap("", "",
"title='Hour
{xLabel}: Traffic {value} GBytes'");
}
}
}
_________
ПИшите код
Отлично, спасибо за код. Ниже даю готовый рабочий
вариант для Razor Pages с ChartDirector (RazorChartViewer), чтобы обновлять
график без мерцания и с рабочими тултипами. Будем возвращать готовый HTML-блок
из viewer.RenderHTML() и на клиенте подменять весь контейнер (и <img />,
и <map>), чтобы tooltips оставались корректными.
Что делаем:
• Добавим
эндпоинт /SimpleLine/ChartHtml, который рендерит только HTML графика
(viewer.RenderHTML()).
• На
странице SimpleLine.cshtml добавим контейнер для графика и JS, который по
таймеру дергает ChartHtml и заменяет innerHTML контейнера.
• В
существующем OnGet оставим начальный рендер, чтобы при первом заходе страница
отрисовалась без JS.
1. SimpleLine.cshtml.cs
Оставляем ваш метод createChart как есть. Добавим
новый handler OnGetChartHtml, который вернет PartialView с готовым HTML.
using
ChartDirector;
using LinSpace;
using
Microsoft.AspNetCore.Mvc;
using
Microsoft.AspNetCore.Mvc.RazorPages;
using
RazorChartDirApp07.Services;
namespace
RazorChartDirApp07.Pages
{
public class
SimpleLineModel : PageModel
{
private readonly
IRunningSineState _state;
private readonly
ILogger<indexmodel> _logger;
public
SimpleLineModel(IRunningSineState state, ILogger<indexmodel>
logger)
{
_state = state;
_logger = logger;
}
public RazorChartViewer Viewer { get;
private set; } = default!;
public void OnGet()
{
ViewData["Title"] =
"Simple Line Chart";
var viewer = new
RazorChartViewer(HttpContext, "chart1");
Viewer = viewer;
createChart(viewer);
ViewData["Viewer"] = viewer;
}
//
AJAX-эндпоинт: вернуть HTML графика целиком (img + map)
public IActionResult OnGetChartHtml()
{
var viewer = new
RazorChartViewer(HttpContext, "chart1");
createChart(viewer);
// Вернем просто HTML-строку
return Content(viewer.RenderHTML(),
"text/html; charset=utf-8");
}
private void createChart(RazorChartViewer
viewer)
{
var dataArg = MathSeries.Linspace(0.0,
2 * Math.PI, 36, _state.CurrentIndex);
var data = MathSeries.Evaluate(dataArg,
Math.Sin);
var nextIndex = _state.NextIndex(36);
string[] labels = {
"0","1","2","3","4","5","6","7","8","9","10","11","12",
"13","14","15","16","17","18","19","20","21","22","23","24",
"25","26","27","28","29","30","31","32","33","34","35"
};
XYChart c = new XYChart(450, 450);
c.setPlotArea(30, 20, 400, 400);
c.addLineLayer(data);
c.xAxis().setLabels(labels);
c.xAxis().setLabelStep(3);
viewer.Image = c.makeWebImage(Chart.SVG);
viewer.ImageMap =
c.getHTMLImageMap("", "", "title='Hour {xLabel}:
Traffic {value} GBytes'");
}
}
}
1. SimpleLine.cshtml
Создадим контейнер и загрузим начальный HTML через
Model.Viewer.RenderHTML(). Затем JS будет
каждые N мс запрашивать свежий HTML и обновлять контейнер, чтобы избежать
мерцания и сохранять рабочие тултипы.
@page
@model RazorChartDirApp07.Pages.SimpleLineModel
@{
ViewData["Title"]
= "Simple Line Chart";
var initialHtml =
((RazorChartViewer)ViewData["Viewer"]).RenderHTML();
}
<h2>@ViewData["Title"]</h2>
<div
id="chart-container">
@Html.Raw(initialHtml)
</div>
<script>
(function(){
const container =
document.getElementById('chart-container');
async function
refreshChart() {
try {
const resp = await fetch('@Url.Page("SimpleLine",
"ChartHtml")', {
method: 'GET',
headers: { 'Accept': 'text/html' },
cache: 'no-store'
});
if (!resp.ok) return;
const html = await resp.text();
// Без мерцания:
заменяем весь HTML блока (img + map), чтобы usemap и tooltips были корректны
container.innerHTML = html;
} catch(e) {
// можно логировать в
консоль
//
console.error(e);
}
}
// Обновляем каждые 1000 мс. Можете поставить 250-500
мс.
const intervalMs
= 1000;
setInterval(refreshChart,
intervalMs);
})();
</script>
Почему так:
• viewer.RenderHTML()
возвращает согласованные <img /> и <map> с синхронизированными
id/usemap и хит-областями для тултипов. Поэтому меняем целиком блок, а не
только src.
• SVG от
ChartDirector обновляется без мерцания, если заменять innerHTML контейнера, а
не перезагружать всю страницу.
Вариант с SignalR:
• Если
хотите вместо таймера, могу дописать хаб и клиентский код, который будет пушить
новый HTML при каждом NextIndex. Тогда на клиенте будете вызывать
container.innerHTML = html по событию. Скажите, и я пришлю точные классы Hub,
регистрацию в Program.cs и JS-клиента.
Если у вас другой путь/имя страницы, поправлю
Url.Page. Также могу завернуть ChartHtml в отдельный Razor Page
/api/chart/simpleline с IActionResult, если вам так удобнее.
Объясни этот код подробно. Все работает. Еле-еле справился.
Трудно.
@page
@model
RazorChartDirApp08.Pages.SimpleLineModel
@using
ChartDirector
@{
ViewData["Title"]
= "Simple Line Chart";
var initialHtml =
((RazorChartViewer)ViewData["Viewer"]).RenderHTML();
}
<h2>@ViewData["Title"]</h2>
<div
id="chart-container">
@Html.Raw(initialHtml)
</div>
<script>
(function(){
const container =
document.getElementById('chart-container');
async function
refreshChart() {
try {
const resp =
await fetch('@Url.Page("SimpleLine", "ChartHtml")', {
method: 'GET',
headers: {
'Accept': 'text/html' },
cache: 'no-store'
});
if (!resp.ok) return;
const html =
await resp.text();
// Без мерцания: заменяем весь HTML блока (img +
map), чтобы usemap и tooltips были корректны
container.innerHTML
= html;
} catch(e) {
// можно логировать в консоль
// console.error(e);
}
}
// Обновляем каждые 1000 мс. Можете поставить 250-500
мс.
const intervalMs
= 1000;
setInterval(refreshChart,
intervalMs);
})();
</script>
-----------------
Сейчас код работает.
Но строка
var initialHtml =
((RazorChartViewer)ViewData["Viewer"]).RenderHTML();
Подчеркнуто волнистой зеленой линией вот эта часть
"(RazorChartViewer)ViewData["Viewer"]"
?
Разберу по частям, зачем здесь каждый фрагмент, как
работает обновление, и почему редактор подчеркивает приведение типа
(RazorChartViewer)ViewData["Viewer"] зеленой волной.
Что делает страница
• Директивы
@page, @model: это Razor Page. Модель страницы —
RazorChartDirApp08.Pages.SimpleLineModel. В ней, как правило, в OnGet вы
готовите данные и кладете в ViewData["Viewer"] экземпляр
RazorChartViewer (обертка ChartDirector для Razor).
• @using ChartDirector: подключает типы ChartDirector/RazorChartViewer.
• ViewData["Title"] = "Simple
Line Chart": задает
заголовок.
• var initialHtml =
((RazorChartViewer)ViewData["Viewer"]).RenderHTML();: берёт из ViewData подготовленный на сервере Viewer, приводит к RazorChartViewer и рендерит стартовый HTML (обычно это <img ... /> плюс <map ...> для тултипов/кликабельных регионов).
• В
разметке <div id="chart-container"> @Html.Raw(initialHtml)
</div>: вставляет HTML диаграммы в контейнер.
• Скрипт:
каждые intervalMs миллисекунд делает GET на эндпоинт
@Url.Page("SimpleLine", "ChartHtml") и получает свежий HTML
диаграммы. Затем заменяет container.innerHTML целиком, чтобы одновременно
обновить <img /> и связанный <map> (иначе сломаются usemap/тултипы).
Почему обновление без мерцания
• Вы не
перерисовываете весь документ, а только содержимое одного контейнера.
• Меняете
сразу и картинку, и карту областей. Это важно: если заменить только <img
/>, то подсказки и кликабельные зоны могут отстать.
• fetch с
cache: 'no-store' гарантирует, что браузер не возьмет кэш, а запросит новый
HTML/изображение.
Про адрес @Url.Page("SimpleLine",
"ChartHtml")
• Это вызов
хендлера OnGetChartHtml в SimpleLine.cshtml.cs. Он должен вернуть ContentResult
с text/html, где вы делаете viewer.RenderHTML() заново на каждом запросе (и,
возможно, обновляете данные перед этим).
Зачем ViewData["Viewer"]
• В OnGet
вы создаете данные и на их основе создаете ChartDirector.Chart и
RazorChartViewer. Viewer вы кладете в ViewData["Viewer"], чтобы
страница могла получить готовый HTML уже при первом рендере без ожидания fetch.
Почему редактор подчеркивает
(RazorChartViewer)ViewData["Viewer"] зеленой волной
• Зеленая
волнистая — это предупреждение (warning), а не ошибка. Обычно это:
• CS8619/CS8600/CS8602/CS8605 из-за nullable reference types: ViewData возвращает object? (может быть null), и
явное приведение к RazorChartViewer без проверки на null рискует NRE. Редактор
предупреждает о потенциальном null.
• Или
IDE0004/IDE0019/IDE0020 стилистика: явное приведение может считаться
небезопасным/избыточным с точки зрения стиля, лучше использовать безопасное
«as» и проверить на null, или вынести в локальную переменную с pattern
matching.
• В Razor ViewData — это словарь Dictionary<string, object?>. Компилятор
не знает, что по ключу "Viewer" всегда лежит RazorChartViewer и не
null, поэтому и ругается.
Как убрать предупреждение корректно
• Вариант с
безопасным приведением и проверкой:
@{
var viewer =
ViewData["Viewer"] as RazorChartViewer;
if (viewer ==
null)
{
throw new
InvalidOperationException("Viewer is not provided in
ViewData["Viewer"].");
}
var initialHtml =
viewer.RenderHTML();
}
• Вариант с pattern matching:
@{
if
(ViewData["Viewer"] is RazorChartViewer viewer)
{
var initialHtml =
viewer.RenderHTML();
}
else
{
throw new
InvalidOperationException("Viewer is not provided or has wrong
type.");
}
}
• Если вы
уверены, что null быть не может, можно использовать оператор null-forgiving,
чтобы успокоить анализатор, но это на ваш риск:
@{
var initialHtml =
((RazorChartViewer)ViewData["Viewer"]!).RenderHTML();
}
• Или
задать строго типизированную модель/свойство, чтобы не тащить через ViewData:
В модели страницы:
public
RazorChartViewer Viewer { get; private set; } = default!;
public void
OnGet()
{
// ... создаете chart и viewer
Viewer = yourViewer;
}
В
Razor:
var initialHtml =
Model.Viewer.RenderHTML();
Это самый чистый и безопасный способ, а
предупреждение исчезнет, потому что тип известен.
Типичные места, где еще появляются такие
предупреждения
• Когда
используете HttpContext.Items, TempData, ViewBag/ViewData — везде, где тип
object?.
• Когда в
проекте включены nullable reference types (обычно в .csproj
<nullable>enable</nullable>), анализатор строго следит за возможными
null.
Итого
• Код у вас
правильный по логике и архитектуре: стартовый HTML — через сервер, дальше —
легкое обновление через fetch без мерцания.
• Зеленая
волна — это предупреждение анализа null-безопасности из-за приведения типа из
ViewData. Исправьте через as + проверка, pattern matching, null-forgiving
оператор, либо, лучше, используйте свойство Model.Viewer и не храните viewer в
ViewData.
А вот сообщения в консоли выводятся warn
warn:
Microsoft.WebTools.BrowserLink.Net.BrowserLinkMiddleware[3]
Unable to
configure Browser Link script injection on the response.
ЧТо делать ?
Это предупреждение связано с Browser Link в Visual
Studio. Оно безвредно и появляется, когда middleware пытается внедрить скрипт в
ответ, а тип ответа или пайплайн этому не способствует (например, JSON,
streaming, уже начатый ответ, отключены cookies, HTTPS/HTTP2 с определенными
настройками, или вы не используете стандартный Razor Layout).
Что можно сделать:
• Проще
всего: отключить Browser Link. В
Visual Studio откройте
меню Debug > Options > Projects and Solutions >
Web Projects и снимите галочку Enable Browser Link. Либо на панели VS
(обычно вверху) отключите кнопку Browser Link.
• В appsettings.Development.json установите:
{
"Logging":
{
"LogLevel":
{
"Microsoft.WebTools.BrowserLink":
"None"
}
}
}
Это просто скрывает предупреждение в логах.
• Удалите/комментируйте UseBrowserLink в Startup/Program для Development:
if
(app.Environment.IsDevelopment())
{
//
app.UseBrowserLink(); // отключить
}
• Если
хотите оставить Browser Link, но уменьшить предупреждения, не отдавайте через
тот же middleware ответы, куда нельзя внедрять скрипты. Например, для
API-эндпоинтов, которые возвращают JSON/ChartDirector изображения/stream,
держите их под /api и не пропускайте через Razor/Layout. Это не всегда избавит
от предупреждения, но сократит частоту.
• Очистите
кэш Browser Link. В Visual Studio закройте все браузеры, остановите отладку,
удалите папку %LocalAppData%\Microsoft\VisualStudio{версия}\Browser Link,
перезапустите.
• Если
используете HTTPS с HTTP/2, иногда помогает принудительно использовать HTTP/1.1
для отладки, потому что Browser Link опирается на инъекцию скрипта и не всегда
дружит с определенными комбинациями заголовков и протокола.
Итого: это не ошибка и не влияет на ваш функционал
графиков. Самый быстрый вариант — отключить Browser Link в VS или убрать
UseBrowserLink в Development.
GPT-5 Nano
Создать